diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 000000000..8d05f9329 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,64 @@ +language: en-US +early_access: false +tone_instructions: >- + Prioritize actionable correctness, security, and runtime regressions. Keep + feedback concise. Do not request explanatory code comments; preserve license + headers and compiler-consumed annotations. +reviews: + profile: chill + request_changes_workflow: false + high_level_summary: true + high_level_summary_in_walkthrough: true + poem: false + sequence_diagrams: false + auto_review: + enabled: true + auto_incremental_review: true + auto_pause_after_reviewed_commits: 0 + drafts: false + pre_merge_checks: + docstrings: + mode: "off" + title: + mode: "off" + description: + mode: "off" + finishing_touches: + docstrings: + enabled: false + unit_tests: + enabled: false + simplify: + enabled: false + autofix: + enabled: false + fix_ci: + enabled: false + resolve_merge_conflict: + enabled: false + path_instructions: + - path: "**/*" + instructions: >- + For cleanup changes, compare executable behavior against the PR merge + base. Check for removed guards, changed return values, weakened compiler + flags, and deleted regression tests. A passing APK build alone does not + establish behavior preservation. Avoid style-only or comment-coverage + requests. + - path: "app/src/main/**" + instructions: >- + Review Android lifecycle, external-display visibility, input cancellation, + permissions, threading, and Compose state. For JNI, native, and Rust + changes, check ABI compatibility, resource ownership, synchronization, + and error paths. Preserve persisted user settings and existing runtime + contracts. + - path: "tools/**" + instructions: >- + Check shell quoting, error handling, path and filesystem semantics, and + Python helper behavior. Preserve MSI same-file protection and strict + compiler checks. Verify downstream patches apply to their pinned sources + and retain intended executable behavior, including embedded scripts. + - path: ".github/workflows/**" + instructions: >- + Check job permissions, triggers, merge validation, build variants, + signing, artifact paths, and release conditions. Verify checks apply to + the current PR head and keep review configuration advisory. 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 39ad49004..bfed4335f 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; } @@ -49,30 +43,26 @@ 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->mono ? 0.0f : 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 * (p->mono ? 1.0f : 0.5f)) / (float)out_h : 1.0f; out[20] = p->mono ? 1.0f : 0.0f; - out[21] = out[22] = out[23] = 0.0f; /* reserved */ + 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 = { @@ -81,8 +71,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; @@ -99,9 +89,8 @@ void vkp_cardboard_set(const struct cardboard_params *p) { c.mono ? " (one eye)" : "", c.ipd_m * 1000.0f, c.screen_width_m, c.screen_dist_m, c.k1, c.k2, c.fov_deg, c.lens_inset, c.chroma); } -/* 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); @@ -123,7 +112,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; @@ -179,15 +167,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; @@ -239,9 +226,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(); @@ -300,7 +284,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, @@ -450,4 +433,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 7d0411ea0..eb2628b2e 100644 --- a/app/src/main/cpp/waylandcomp/src/cardboard.frag +++ b/app/src/main/cpp/waylandcomp/src/cardboard.frag @@ -1,56 +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; vec4 extra; } 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; @@ -63,7 +43,6 @@ void main() { bool mono = pc.extra.x > 0.5; float eye = mono || uv.x < 0.5 ? 0.0 : 1.0; vec2 local = mono ? uv : vec2(uv.x * 2.0 - eye, uv.y); - // Lens centres sit inset towards the nose by view.x of a viewport. float cx = mono ? 0.5 : 0.5 + (eye < 0.5 ? pc.view.x : -pc.view.x); vec2 p = local - vec2(cx, 0.5); float tanH = pc.lens.z; @@ -71,14 +50,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 527dfa16d..a5cc791e6 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,43 +7,29 @@ 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; int mono; float display_hz; }; -/* 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 43ebde085..caebe0b5e 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 @@ - // 1115.1.0 #pragma once const uint32_t cardboard_frag_code[] = { 0x07230203,0x00010000,0x0008000b,0x00000185,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 a1550af9a..512809def 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,26 @@ 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 uint8_t g_keys_down[0x300 / 8]; 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 +440,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 +468,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 +481,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 +503,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 +510,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 +528,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 +538,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 +558,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 +578,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 +591,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 +616,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 +637,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,21 +654,16 @@ 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 frame (now_ns), to let a replacement take over. */ -static int64_t g_hud_last_ns; -/* That window committed a frame the screen has not drawn yet. */ +static int64_t g_hud_last_ns, g_hud_alive_ns; 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) { - if (s->dmabuf != buffer || s->dmabuf_buf != b) { + const int repeat = s->dmabuf == buffer && s->dmabuf_buf == b; + if (!repeat) { drop_dmabuf(s, 1); s->dmabuf = buffer; s->dmabuf_buf = b; @@ -810,11 +682,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]; @@ -826,15 +693,13 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re s->buf_h = b->height; s->buf_alpha = (b->format & 0xff) == 'A'; s->has_content = b->img != NULL; - g_stat_dmabuf++; + if (!repeat) g_stat_dmabuf++; const int announced_now = !s->announced_vulkan; if (!s->announced_vulkan) { char name[160]; 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, @@ -849,15 +714,8 @@ 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_last_ns > 1000000000LL || + (!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))) { char name[160]; describe(s, name, sizeof(name)); @@ -866,8 +724,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); @@ -882,14 +738,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_last_ns = now_ns(); g_hud_fresh = 1; droiddeck_on_game_frame(); } + 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; } @@ -905,7 +759,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); @@ -928,9 +781,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)); } @@ -943,20 +793,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) @@ -969,12 +811,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); @@ -1010,12 +846,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); @@ -1036,9 +868,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); @@ -1071,7 +900,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); @@ -1083,7 +911,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; @@ -1102,10 +929,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); @@ -1132,7 +955,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) { @@ -1142,18 +964,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); @@ -1170,7 +989,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; @@ -1184,7 +1002,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); @@ -1265,7 +1083,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, @@ -1297,7 +1114,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)); @@ -1331,8 +1147,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) { @@ -1345,7 +1159,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); @@ -1402,7 +1215,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, @@ -1413,9 +1226,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, @@ -1466,18 +1276,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)); @@ -1491,9 +1295,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, @@ -1504,10 +1305,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); @@ -1573,7 +1370,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, @@ -1607,11 +1403,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, @@ -1661,7 +1452,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) { @@ -1710,14 +1500,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)); } @@ -1745,7 +1531,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; }; @@ -1755,21 +1540,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; @@ -1778,8 +1557,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)); @@ -1823,8 +1600,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; @@ -1850,13 +1625,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) { @@ -1868,19 +1636,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) || @@ -1890,7 +1654,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) @@ -1898,9 +1661,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; @@ -1915,39 +1675,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 || @@ -1960,11 +1697,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); @@ -1978,7 +1713,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; } @@ -1987,9 +1722,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] && @@ -2003,11 +1735,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; @@ -2036,7 +1767,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); @@ -2044,7 +1775,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; @@ -2068,7 +1798,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); @@ -2085,11 +1814,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) { @@ -2100,7 +1829,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); } @@ -2108,13 +1837,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); @@ -2125,10 +1851,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 " @@ -2146,25 +1872,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)); @@ -2181,10 +1893,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) { @@ -2194,24 +1903,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) @@ -2219,24 +1912,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 { @@ -2254,24 +1939,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), @@ -2296,7 +1975,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) @@ -2307,14 +1985,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++; @@ -2324,25 +1998,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) { @@ -2353,14 +2020,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); @@ -2404,8 +2068,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 : "."); @@ -2454,9 +2116,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)) @@ -2467,7 +2126,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) { @@ -2519,59 +2177,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) { @@ -2587,13 +2224,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; @@ -2624,7 +2259,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; @@ -2654,7 +2288,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); @@ -2674,7 +2307,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; @@ -2706,7 +2338,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) { @@ -2751,7 +2382,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, @@ -2829,7 +2459,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); @@ -2871,8 +2500,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; @@ -2891,12 +2518,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; @@ -2955,8 +2577,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; @@ -2965,24 +2585,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; @@ -2990,7 +2604,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; @@ -3082,9 +2696,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++) { @@ -3100,14 +2711,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); @@ -3188,7 +2798,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)); @@ -3201,7 +2810,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) @@ -3213,16 +2821,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])); @@ -3239,13 +2846,12 @@ 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. */ + if (evdev < 0x300) { + uint8_t bit = (uint8_t)(1u << (evdev & 7)); + if (pressed && (g_keys_down[evdev >> 3] & bit)) return; + if (pressed) g_keys_down[evdev >> 3] |= bit; else g_keys_down[evdev >> 3] &= (uint8_t)~bit; + } 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); @@ -3270,10 +2876,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; @@ -3287,7 +2889,6 @@ 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; int vsync = 0; @@ -3295,11 +2896,11 @@ static int on_input_readable(int fd, uint32_t mask, void *data) { 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: vsync = 1; vsync_ns = ((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; @@ -3309,8 +2910,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 }; @@ -3325,7 +2924,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 }; @@ -3333,7 +2931,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 }; @@ -3341,7 +2938,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 }; @@ -3349,9 +2945,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 }; @@ -3359,15 +2952,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; @@ -3385,10 +2974,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) { @@ -3398,24 +2985,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); @@ -3438,10 +3017,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; @@ -3455,12 +3034,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; @@ -3510,7 +3083,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; } @@ -3524,14 +3097,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 }; @@ -3543,8 +3112,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(); @@ -3553,10 +3120,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]; @@ -3574,33 +3137,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], @@ -3612,7 +3172,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 2c56dc7f7..af5b69cd9 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, DROIDDECK_HEADPOSE_MONO = 1u << 2, }; @@ -33,11 +20,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; float display_hz; uint32_t reserved[11]; }; 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 03d538c2f..64eca97bd 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, @@ -475,7 +400,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) { @@ -485,7 +409,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; @@ -495,9 +418,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); @@ -505,8 +425,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); @@ -514,9 +432,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) { @@ -525,7 +441,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, @@ -536,38 +451,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); @@ -575,8 +484,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); @@ -585,18 +492,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 @@ -604,7 +508,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) { @@ -612,13 +515,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); @@ -631,25 +532,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); @@ -657,14 +554,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) { @@ -675,8 +570,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; @@ -690,7 +583,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; @@ -699,7 +591,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; @@ -708,7 +599,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) { @@ -718,15 +608,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) { @@ -734,10 +620,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 6560c11d7..21ad7dd56 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)) @@ -29,7 +24,6 @@ class App : Application() { com.droiddeck.launcher.glasses.Glasses.init(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 c44980c5b..fa0d13a6d 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) @@ -116,18 +108,17 @@ class MainActivity : ComponentActivity() { private var frameGenLabel by mutableStateOf("") private var lossless by mutableStateOf(Lossless.State.NONE) private var showRemove by mutableStateOf(false) + private var showResetSettings by mutableStateOf(false) private var showNonAdreno by mutableStateOf<LinuxRuntimeInstaller.Release?>(null) private var glThread by mutableStateOf(true) private var noGlError by mutableStateOf(true) 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) @@ -166,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) @@ -199,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) } } @@ -234,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) } @@ -250,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) @@ -270,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() @@ -289,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)) { @@ -300,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({ @@ -326,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 -> @@ -334,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("") @@ -350,6 +331,8 @@ class MainActivity : ComponentActivity() { private var touchMode by mutableStateOf(SessionPrefs.TOUCH_AUTO) private var suspendPolicy by mutableStateOf(SessionPrefs.SUSPEND_MANUAL) private var steamDownloadsInBackground by mutableStateOf(false) + private var zramCompression by mutableStateOf(false) + private var zramSupported by mutableStateOf<Boolean?>(null) private var oscMode by mutableStateOf(SessionPrefs.OSC_AUTO) private var backActionsInverted by mutableStateOf(false) private var gameStorage by mutableStateOf("") @@ -383,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 { @@ -403,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) @@ -416,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 @@ -450,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 @@ -528,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) @@ -601,18 +578,14 @@ class MainActivity : ComponentActivity() { FrontEndActions( onPlay = { startSteamSession() }, onPlaySteamVr = { startSteamSession(steamVr = true) }, - // 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) } } @@ -622,7 +595,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() }) }, @@ -650,7 +622,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) @@ -658,7 +629,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) @@ -673,7 +643,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() }, @@ -692,7 +661,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 }, @@ -726,6 +694,10 @@ class MainActivity : ComponentActivity() { } }, "clear-logs").start() }, + onResetSettings = { + if (SessionState.running) android.widget.Toast.makeText(this, R.string.setup_reset_settings_session_running, android.widget.Toast.LENGTH_LONG).show() + else showResetSettings = true + }, onOffline = { OfflineMode.setEnabled(this, !OfflineMode.enabled(this)) offline = OfflineMode.enabled(this) @@ -736,7 +708,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() } }, @@ -871,6 +842,10 @@ class MainActivity : ComponentActivity() { onConfirm = { showRemove = false; removeRuntime() }, onDismiss = { showRemove = false }, ) + if (showResetSettings) com.droiddeck.launcher.ui.ResetSettingsDialog( + onConfirm = { gameProfiles -> resetSettings(gameProfiles) }, + onDismiss = { showResetSettings = false }, + ) flood?.let { f -> com.droiddeck.launcher.ui.LaunchFlood(f.from, onProgress = { floodProgress = it }) { launchFlooded(f) } } returning?.let { r -> com.droiddeck.launcher.ui.FloodReturn(androidx.compose.ui.graphics.Color(r.color), r.to, onProgress = { floodProgress = it }) { @@ -885,25 +860,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) @@ -959,7 +926,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)) { @@ -968,8 +934,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) { @@ -984,25 +948,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 { @@ -1019,8 +975,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() @@ -1047,10 +1001,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 @@ -1106,7 +1058,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 @@ -1119,7 +1070,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() { @@ -1143,7 +1093,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) @@ -1186,7 +1135,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, @@ -1196,7 +1144,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) }, @@ -1231,9 +1178,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() } } @@ -1282,7 +1227,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( @@ -1294,6 +1238,8 @@ class MainActivity : ComponentActivity() { touchMode = touchMode, suspendPolicy = suspendPolicy, steamDownloadsInBackground = steamDownloadsInBackground, + zramCompression = if (mode == SessionService.MODE_STEAM) zramCompression else null, + zramSupported = zramSupported, pipSupported = com.droiddeck.launcher.session.SessionPipController.supported(this), pipAutoEnter = pipAutoEnter, unfoldedControls = if (mode == SessionService.MODE_STEAM && @@ -1352,6 +1298,7 @@ class MainActivity : ComponentActivity() { SessionPrefs.setSteamDownloadsInBackground(this, enabled) steamDownloadsInBackground = enabled }, + onZramCompression = { on -> SessionPrefs.setZramCompression(this, on); zramCompression = on }, onPipAutoEnter = { on -> SessionPrefs.setPipAutoEnter(this, on); pipAutoEnter = on }, onUnfoldedControls = { on -> SessionPrefs.setUnfoldedControls(this, on); unfoldedControls = on }, onOsc = { o -> SessionPrefs.setOscMode(this, o); oscMode = o }, @@ -1364,8 +1311,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) } @@ -1448,17 +1393,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() @@ -1468,19 +1408,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() @@ -1489,8 +1426,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({ @@ -1537,9 +1472,10 @@ 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) + ui.post { zramSupported = zram; zramCompression = zramOn } drivers.refreshDrivers() val storage = GameStorage.options(this) val deckyInstalled = if (mode == SessionService.MODE_STEAM) DeckyManager.installed(this) else null @@ -1556,7 +1492,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) @@ -1569,7 +1504,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) @@ -1610,17 +1544,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 { @@ -1634,15 +1562,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 @@ -1656,8 +1578,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 } @@ -1669,7 +1589,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 } @@ -1677,14 +1596,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() @@ -1743,6 +1658,21 @@ class MainActivity : ComponentActivity() { } } + private fun resetSettings(gameProfiles: Boolean) { + if (SessionState.running) return + val scopes = steamGames.map { it.profileKey } + Thread({ + val ok = com.droiddeck.launcher.session.SettingsReset.resetAll(this, gameProfiles, scopes) + ui.post { + if (isDestroyed) return@post + com.droiddeck.launcher.core.AppLanguage.setChosen(this, com.droiddeck.launcher.core.AppLanguage.SYSTEM) + if (!ok) android.widget.Toast.makeText(this, R.string.setup_reset_settings_partial, android.widget.Toast.LENGTH_LONG).show() + com.droiddeck.launcher.ui.Motion.refresh(this) + recreate() + } + }, "reset-settings").start() + } + private fun removeRuntime() { if (runtimeChangesBlocked()) return val removal = LinuxRuntimeInstaller.beginUninstall(this) ?: return @@ -1752,16 +1682,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 @@ -1791,7 +1715,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 e4c675cc3..f8893281c 100644 --- a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt @@ -115,17 +115,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) @@ -135,7 +124,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 @@ -145,20 +133,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 @@ -169,23 +147,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) @@ -199,22 +170,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 { @@ -228,7 +189,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 @@ -259,17 +219,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) @@ -294,24 +248,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 var activeCardboard = Cardboard.OFF private var glasses by mutableStateOf(Glasses.state.value) @@ -333,8 +281,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)) } } @@ -355,7 +301,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 @@ -368,12 +313,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 @@ -384,11 +323,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) @@ -418,7 +352,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))) } @@ -435,9 +368,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), @@ -447,18 +377,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) @@ -471,7 +395,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() } } @@ -480,7 +403,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) { @@ -489,8 +411,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) @@ -499,8 +419,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 @@ -518,7 +436,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() @@ -544,12 +461,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 @@ -562,18 +476,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) { @@ -601,7 +508,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() }, @@ -613,7 +519,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() @@ -625,9 +530,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. val glassesVr = glassesOutputActive && glasses.canControl LaunchedEffect(cardboard.enabled, steamVrSwitchedPicture, glassesVr) { while (cardboard.enabled || steamVrSwitchedPicture || glassesVr) { @@ -647,7 +549,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) }, @@ -687,7 +588,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 -> @@ -736,7 +636,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) @@ -842,7 +741,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() @@ -896,7 +794,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 @@ -941,7 +838,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) } @@ -959,7 +855,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()) @@ -1081,7 +976,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) @@ -1150,7 +1044,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 @@ -1163,8 +1056,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) @@ -1175,8 +1066,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() @@ -1203,8 +1092,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 @@ -1233,21 +1120,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 @@ -1300,7 +1175,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() @@ -1315,16 +1189,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) @@ -1345,7 +1214,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) @@ -1354,7 +1222,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) @@ -1363,7 +1230,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 @@ -1379,21 +1245,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()) @@ -1401,16 +1258,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { val size = live ?: if (SessionState.running) previous else glassesOutputSize() ?: 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)) @@ -1418,9 +1271,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { activeCardboard = effectiveCardboard(SessionPrefs.cardboard(this)) pushCardboard(activeCardboard) 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) @@ -1430,9 +1280,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) @@ -1455,6 +1302,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { ) Log.i(TAG, "hdr: " + (if (on) "on" else if (wanted) "wanted but ${probe.reason}" else "off") + " · display ${probe.formats.ifEmpty { "SDR" }}") } + com.droiddeck.launcher.gpu.DriverSetupLog.write(this, "compositor.start driver=$driverId") CompositorHost.startOrAttach( holder.surface, runtimeDir.path, driverId?.let { turnip.driverPath(it) }, driverId?.let { turnip.libraryName(it) }, @@ -1465,10 +1313,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( @@ -1497,12 +1341,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 @@ -1514,18 +1352,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 @@ -1547,11 +1379,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 { @@ -1565,7 +1392,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 @@ -1573,13 +1399,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 { glassesScreen?.hz?.takeIf { it > 1f }?.let { return it } val display = if (Build.VERSION.SDK_INT >= 30) display else windowManager.defaultDisplay @@ -1590,10 +1409,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({ @@ -1606,7 +1421,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) @@ -1627,9 +1441,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 @@ -1664,10 +1476,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)) @@ -1679,7 +1487,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 { @@ -1694,7 +1501,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) @@ -1708,14 +1514,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 @@ -1723,13 +1527,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? { @@ -1737,7 +1534,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) @@ -1761,11 +1557,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) @@ -1776,7 +1569,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) @@ -1784,7 +1576,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() @@ -1825,27 +1616,17 @@ 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.repeatCount > 0) return true 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) { @@ -1882,7 +1663,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) @@ -1934,7 +1714,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) @@ -1987,17 +1766,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 @@ -2005,14 +1778,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 @@ -2033,7 +1800,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 @@ -2056,7 +1822,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) } @@ -2086,8 +1851,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 } @@ -2138,13 +1901,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 @@ -2169,7 +1925,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() @@ -2197,7 +1952,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) @@ -2221,14 +1975,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 @@ -2243,7 +1995,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 @@ -2262,8 +2013,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 { @@ -2280,8 +2029,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 -> @@ -2299,19 +2046,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 @@ -2320,10 +2065,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) { @@ -2348,14 +2089,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 @@ -2363,12 +2102,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 @@ -2381,13 +2118,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) @@ -2415,8 +2149,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() @@ -2426,12 +2158,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) @@ -2445,13 +2173,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 } @@ -2537,7 +2260,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() updateGlassesScreen() @@ -2550,7 +2272,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { super.startActivityForResult(intent, requestCode, options) } - // ── Lifecycle ─────────────────────────────────────────────────────────────────────────── override fun onStart() { super.onStart() @@ -2568,8 +2289,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) updateGlassesScreen() SessionService.setActivityVisible(this, false) @@ -2587,7 +2306,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.getBooleanExtra(EXTRA_STEAMVR, false) && SessionState.mode == SessionService.MODE_STEAM) { @@ -2631,14 +2349,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() @@ -2649,8 +2364,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 (glassesOutputActive) { if (action == "start" && glassesSettings.auto3d && glasses.can3d && !glasses.sbs) { @@ -2678,7 +2391,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() @@ -2686,7 +2398,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } } - /** 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) @@ -2706,7 +2417,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { autoStartSteamVr(now) return } - // 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() @@ -2717,7 +2427,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) @@ -2729,7 +2438,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 = activeCardboard cardboard = c @@ -2771,8 +2479,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 onGlasses = glassesScreen != null val c = activeCardboard val want = (resumed || (onGlasses && started)) && c.enabled && c.headTracking @@ -2892,7 +2598,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 @@ -2918,11 +2623,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() } @@ -2935,8 +2638,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) @@ -2946,7 +2647,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() @@ -2960,8 +2660,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) @@ -2983,7 +2681,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 @@ -2994,20 +2691,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 @@ -3023,7 +2712,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 @@ -3036,7 +2724,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { companion object { const val EXTRA_RETURN_HOME = "returnHome" const val EXTRA_STEAMVR = "steamVr" - /** 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" @@ -3052,7 +2739,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private const val DRAWER_REPEAT_ACCELERATION_MS = 1_400L private val steamVrWorker = java.util.concurrent.ThreadPoolExecutor(0, 1, 30, java.util.concurrent.TimeUnit.SECONDS, java.util.concurrent.LinkedBlockingQueue()) { Thread(it, "steamvr") } - /** 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( @@ -3073,5 +2759,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 e43ba8669..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 > Session > 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) { @@ -33,7 +23,7 @@ object AgentAccess { fun requireCommands(context: Context) { if (!commandsAllowed(context)) throw AgentException( "AGENT_COMMANDS_DISABLED", - "Debugging tools are off: turn on Setup > Session > Debugging tools on the device", + "Debugging tools are off: turn on Setup > Diagnostics > Debugging tools on the device", ) } @@ -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 e3f9e4afb..d536e1dd7 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) @@ -188,8 +179,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) @@ -277,11 +266,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 @@ -314,7 +301,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 fc08bd5c0..f052df93c 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 4021e1804..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,31 +6,20 @@ 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", "es", "fr", "ru", "ja", "ko", "zh-CN", "zh-TW", "zh-HK") + 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", + "ja", "ko", "zh-CN", "zh-TW", "zh-HK", + ) 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 @@ -40,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( @@ -49,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 } @@ -75,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 } @@ -88,18 +63,23 @@ 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)) } - /** Which of the app's languages [locale] reads as, or null when the app does not ship it. */ + private val latinAmerica = setOf( + "419", "AR", "BO", "BR", "BZ", "CL", "CO", "CR", "CU", "DO", "EC", "GT", "HN", "MX", "NI", "PA", + "PE", "PR", "PY", "SV", "US", "UY", "VE", + ) + fun supportedTag(locale: Locale): String? = when (locale.language) { - "en", "es", "fr", "ru", "ja", "ko" -> 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 + "in", "id" -> "id" + "es" -> if (locale.country in latinAmerica) "es-419" else "es" + "pt" -> if (locale.country.isEmpty() || locale.country == "BR") "pt-BR" else "pt-PT" "zh" -> when { locale.script == "Hans" -> "zh-CN" locale.country == "HK" || locale.country == "MO" -> "zh-HK" @@ -109,16 +89,38 @@ 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" - "es" -> "Español" + "id" -> "Bahasa Indonesia" + "ms" -> "Bahasa Melayu" + "cs" -> "Čeština" + "da" -> "Dansk" + "de" -> "Deutsch" + "es" -> "Español (España)" + "es-419" -> "Español (Latinoamérica)" "fr" -> "Français" + "it" -> "Italiano" + "hu" -> "Magyar" + "nl" -> "Nederlands" + "nb" -> "Norsk bokmål" + "pl" -> "Polski" + "pt-BR" -> "Português (Brasil)" + "pt-PT" -> "Português (Portugal)" + "ro" -> "Română" + "fi" -> "Suomi" + "sv" -> "Svenska" + "vi" -> "Tiếng Việt" + "tr" -> "Türkçe" + "el" -> "Ελληνικά" + "bg" -> "Български" "ru" -> "Русский" + "uk" -> "Українська" + "ar" -> "العربية" + "hi" -> "हिन्दी" + "th" -> "ไทย" "ja" -> "日本語" "ko" -> "한국어" "zh-CN" -> "简体中文" 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 new file mode 100644 index 000000000..51c3b2494 --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/core/ZramSupport.kt @@ -0,0 +1,34 @@ +package com.droiddeck.launcher.core + +import android.app.ActivityManager +import android.content.Context +import android.os.Build +import java.io.File + +object ZramSupport { + private const val GIB = 1L shl 30 + + fun supported(): Boolean = kernelAtLeast(System.getProperty("os.version").orEmpty(), 5, 4) && swapTotalKb() > 0 + + fun recommended(context: Context): Boolean { + if (!supported()) return false + val memory = ActivityManager.MemoryInfo() + context.getSystemService(ActivityManager::class.java).getMemoryInfo(memory) + val advertised = if (Build.VERSION.SDK_INT >= 34) memory.advertisedMem else 0L + return belowTwelveGb(advertised, memory.totalMem) + } + + fun belowTwelveGb(advertisedBytes: Long, totalBytes: Long): Boolean = + if (advertisedBytes > 0) advertisedBytes < 11 * GIB else totalBytes < 10 * GIB + + fun kernelAtLeast(release: String, major: Int, minor: Int): Boolean { + val parts = Regex("^(\\d+)\\.(\\d+)").find(release)?.groupValues ?: return false + val ma = parts[1].toInt() + val mi = parts[2].toInt() + return ma > major || (ma == major && mi >= minor) + } + + fun swapTotalKb(meminfo: List<String> = runCatching { File("/proc/meminfo").readLines() }.getOrDefault(emptyList())): Long = + meminfo.firstOrNull { it.startsWith("SwapTotal:") } + ?.substringAfter(':')?.trim()?.substringBefore(' ')?.toLongOrNull() ?: 0L +} 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 14f85cbee..90de91e87 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, @@ -30,12 +16,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, fovOverrideDeg: Float = 0f, displayHz: Float = 0f) { val l = lens WaylandCompositor.nativeSetCardboard(enabled, source, ipdMm / 1000f, screenDistanceDm / 10f, @@ -47,18 +27,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), VITURE("viture", 0f, 0f, 0f, 0f, 0f, 23.3f); @@ -83,21 +55,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) @@ -106,15 +69,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 @@ -125,57 +82,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, @@ -185,7 +118,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) @@ -200,7 +132,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 new file mode 100644 index 000000000..0fb72fbac --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/gpu/DriverSetupLog.java @@ -0,0 +1,28 @@ +package com.droiddeck.launcher.gpu; + +import android.content.Context; +import android.util.Log; +import java.io.File; +import java.io.FileWriter; + +public final class DriverSetupLog { + private DriverSetupLog() {} + + public static synchronized void write(Context context, String stage) { + Log.i("DriverSetup", stage); + append(context.getFilesDir(), stage); + try { append(context.getExternalFilesDir(null), stage); } + catch (Exception ignored) { } + } + + private static void append(File directory, String stage) { + if (directory == null) return; + try { + directory.mkdirs(); + File file = new File(directory, "driver-setup.txt"); + try (FileWriter out = new FileWriter(file, file.length() < 131072)) { + out.write(System.currentTimeMillis() + " " + stage + "\n"); + } + } 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 082f5d2b1..afe822617 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 glasses: Boolean = false, 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) @@ -41,7 +32,6 @@ class HeadTracker(context: Context, private val glasses: Boolean = false, privat val usesGlasses: Boolean get() = glasses - /** 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" @@ -86,7 +76,6 @@ class HeadTracker(context: Context, private val glasses: Boolean = false, privat WaylandCompositor.nativeSetCardboardPose(HeadPose.IDENTITY, false) } - /** Make where the viewer looks now straight ahead (see HeadPose.recenter). */ fun recenter() { recenterPending = true if (glasses) synchronized(glassesLock) { if (started) 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 27083a544..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,40 +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"; - /** Adreno 7xx (8 Gen 2/3, e.g. the Pocket FIT's A750). */ + private static final String DRIVER_A6XX = "turnip26.3.0-a6xx"; 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)); - /** Overrides the pick, for a device we cannot reach: a7xx, a8xx or system. */ + DRIVER_A7XX, DRIVER_A8XX, com.droiddeck.launcher.session.QuestExperimental.DRIVER, + DRIVER_A6XX, com.droiddeck.launcher.session.QuestExperimental.DRIVER_A6XX)); 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 = @@ -75,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() + "/"; } @@ -85,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", "") : ""; @@ -110,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<>(); @@ -133,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)); @@ -147,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(); @@ -160,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; @@ -174,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; @@ -190,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); @@ -205,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()); @@ -214,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)); @@ -243,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); @@ -269,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; @@ -282,19 +230,16 @@ 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); if (id == null) return null; File dir = new File(contentDir, id); if (!new File(dir, "meta.json").isFile()) { FileUtils.delete(dir); if (!TarZst.extractAsset(context, "graphics_driver/adrenotools-" + id + ".tzst", dir)) { Log.e(TAG, "could not unpack " + id); + DriverSetupLog.write(context, "install.unpack-failed driver=" + id); FileUtils.delete(dir); return null; } @@ -305,14 +250,10 @@ public final class TurnipDriver { return null; } Log.i(TAG, "graphics driver " + id + " (" + library + ")"); + DriverSetupLog.write(context, "install.ready driver=" + id + " library=" + library); 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()) { @@ -338,23 +279,22 @@ public final class TurnipDriver { if (forced.startsWith("system")) return null; if (forced.startsWith("a8")) return DRIVER_A8XX; if (forced.startsWith("a7")) return DRIVER_A7XX; + if (forced.startsWith("a6")) return DRIVER_A6XX; } 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); if (generation >= '8') return DRIVER_A8XX; if (generation == '7') return DRIVER_A7XX; + 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 48e54c791..494533f43 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/KeyboardHost.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/KeyboardHost.kt @@ -2,26 +2,17 @@ package com.droiddeck.launcher.input import android.app.Activity import android.content.Context +import android.os.Build import android.text.InputType import android.view.KeyCharacterMap import android.view.KeyEvent import android.view.View +import android.view.WindowInsets import android.view.inputmethod.BaseInputConnection 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, @@ -36,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) } @@ -44,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 @@ -53,7 +42,10 @@ class KeyboardHost( return KeyConnection() } - fun toggle() = if (shown) hide() else show() + fun toggle() = if (shown && imeVisible()) hide() else show() + + private fun imeVisible() = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || + rootWindowInsets?.isVisible(WindowInsets.Type.ime()) != false fun show() { shown = true @@ -75,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) @@ -120,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 f0dc72158..50ffb17a8 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/OnScreenControls.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/OnScreenControls.kt @@ -184,7 +184,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 350a61934..5a717150e 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchControls.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchControls.kt @@ -13,6 +13,7 @@ import android.util.Log import android.view.MotionEvent import android.view.View import android.widget.Toast +import com.droiddeck.launcher.R import com.droiddeck.launcher.input.SteamTouchConfig.Element import java.util.concurrent.Executors import kotlin.math.abs @@ -21,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, @@ -41,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 } @@ -68,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 @@ -112,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() @@ -129,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 @@ -161,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) { @@ -194,7 +170,6 @@ class SteamTouchControls( } } - /** Re-reads the config for the current app (after a save). */ fun reloadConfig() { configApp = -1 refreshState() @@ -215,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 @@ -239,7 +211,6 @@ class SteamTouchControls( } } - // ---- Geometry ---- private fun unit() = min(width / 1280f, height / 720f) @@ -247,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 @@ -262,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() @@ -280,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() @@ -296,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() @@ -335,11 +297,10 @@ class SteamTouchControls( if (editing) { text.textSize = r * 0.3f text.color = Color.argb(alpha / 2, 255, 255, 255) - canvas.drawText("Trackpad", x, y - (text.descent() + text.ascent()) / 2, text) + canvas.drawText(context.getString(R.string.deck_surface_trackpad), x, y - (text.descent() + text.ascent()) / 2, text) } } 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) @@ -363,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) || @@ -371,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() @@ -383,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 @@ -395,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) @@ -411,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() @@ -419,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), @@ -434,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 { @@ -468,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) @@ -490,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 { @@ -504,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)) { @@ -520,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()) { @@ -560,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 @@ -579,7 +519,7 @@ class SteamTouchControls( SteamTouchConfig.STEAM -> "STEAM" SteamTouchConfig.THUMB -> "…" SteamTouchConfig.KEYBOARD -> "⌨" - SteamTouchConfig.PASTE -> "Paste" + SteamTouchConfig.PASTE -> context.getString(R.string.touch_paste) SteamTouchConfig.SELECT -> "⧉" SteamTouchConfig.START -> "☰" SteamTouchConfig.BUMPER_LEFT -> "LB" @@ -594,7 +534,6 @@ class SteamTouchControls( else -> "?" } - // ---- The report ---- private fun dpadBits(e: Element): Long { var bits = 0L @@ -603,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 @@ -632,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() @@ -652,7 +590,6 @@ class SteamTouchControls( invalidate() } - // ---- Touch ---- @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent): Boolean { @@ -693,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) @@ -725,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 @@ -743,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>() @@ -751,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 -> @@ -815,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) @@ -831,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() @@ -849,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) @@ -880,44 +808,42 @@ class SteamTouchControls( } private fun controlName(type: Int) = when (type) { - SteamTouchConfig.DPAD -> "D-pad" - SteamTouchConfig.JOYSTICK_LEFT -> "Left stick" - SteamTouchConfig.JOYSTICK_RIGHT -> "Right stick" - SteamTouchConfig.TRACKPAD_LEFT -> "Left trackpad" - SteamTouchConfig.TRACKPAD_RIGHT -> "Right trackpad" - SteamTouchConfig.TRACKPAD_CENTER -> "Trackpad" + SteamTouchConfig.DPAD -> context.getString(R.string.touch_dpad) + SteamTouchConfig.JOYSTICK_LEFT -> context.getString(R.string.touch_left_stick) + SteamTouchConfig.JOYSTICK_RIGHT -> context.getString(R.string.touch_right_stick) + SteamTouchConfig.TRACKPAD_LEFT -> context.getString(R.string.deck_left_trackpad) + SteamTouchConfig.TRACKPAD_RIGHT -> context.getString(R.string.deck_right_trackpad) + SteamTouchConfig.TRACKPAD_CENTER -> context.getString(R.string.deck_surface_trackpad) SteamTouchConfig.STEAM -> "Steam" - SteamTouchConfig.THUMB -> "Menu" - SteamTouchConfig.KEYBOARD -> "Keyboard" + SteamTouchConfig.THUMB -> context.getString(R.string.touch_menu) + SteamTouchConfig.KEYBOARD -> context.getString(R.string.touch_keyboard) 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 val list = android.widget.LinearLayout(context).apply { orientation = android.widget.LinearLayout.VERTICAL; setPadding((12 * density).toInt(), 0, (12 * density).toInt(), 0) } var close: () -> Unit = {} val copied = prefs.getString("copiedLayout", null) - fun item(name: String, enabled: Boolean = true, action: () -> Unit) = list.addView(android.widget.Button(context).apply { - text = name + fun item(name: Int, enabled: Boolean = true, action: () -> Unit) = list.addView(android.widget.Button(context).apply { + setText(name) isAllCaps = false isEnabled = enabled setOnClickListener { close(); action(); invalidate() } }) - item("Colour and opacity…") { pickColor() } - item("Copy this layout") { + item(R.string.touch_color_opacity) { pickColor() } + item(R.string.touch_copy_layout) { prefs.edit().putString("copiedLayout", (editElements + hidden).joinToString(";") { "${it.type},${it.visible},${it.x},${it.y},${it.xScale},${it.yScale}" }).apply() - Toast.makeText(context, "Layout copied; paste it into any game's touch layout", Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.touch_layout_copied, Toast.LENGTH_SHORT).show() } - item("Paste copied layout", copied != null) { pasteLayout(copied ?: "") } - item("Reset to Steam's default") { resetEditSet() } - close = SteamTouchIconPicker.showPanel(host, "Layout", list, listOf("Close" to {})) + item(R.string.touch_paste_layout, copied != null) { pasteLayout(copied ?: "") } + item(R.string.touch_reset_layout) { resetEditSet() } + 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(',') @@ -931,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) @@ -964,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 @@ -985,16 +901,15 @@ class SteamTouchControls( RectF(frame.left + l * s, frame.top + t * s, frame.left + r * s, frame.top + b * s) private fun text(canvas: Canvas, str: String, x: Float, y: Float, size: Float, color: Int = Color.WHITE, bold: Boolean = true, - align: Paint.Align = Paint.Align.CENTER) { + align: Paint.Align = Paint.Align.CENTER, maxWidth: Float = 0f) { val p = Paint(Paint.ANTI_ALIAS_FLAG).apply { this.color = color; textSize = size; textAlign = align typeface = if (bold) android.graphics.Typeface.DEFAULT_BOLD else android.graphics.Typeface.DEFAULT } + if (maxWidth > 0f) p.measureText(str).let { if (it > maxWidth) p.textSize = size * maxWidth / it } 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) { @@ -1004,11 +919,10 @@ class SteamTouchControls( canvas.drawRoundRect(r, radius, radius, Paint(Paint.ANTI_ALIAS_FLAG).apply { color = line2; style = Paint.Style.STROKE; strokeWidth = 1.2f * s }) } val ink = when { !enabled -> Color.argb(110, 242, 244, 247); primary -> onAccent; else -> onSurface } - text(canvas, text, r.centerX(), r.centerY(), 14f * s, ink) + text(canvas, text, r.centerX(), r.centerY(), 14f * s, ink, maxWidth = r.width() - 16f * s) 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 { @@ -1044,7 +958,7 @@ class SteamTouchControls( BIG_PICTURE -> "Steam" else -> gameNames[appId] ?: run { val app = appId - gameNames[app] = "app $app" + gameNames[app] = context.getString(R.string.touch_app_id, app) loader.execute { val name = SteamTouchConfig.gameName(context, app) ?: return@execute handler.post { gameNames[app] = name; invalidate() } @@ -1053,15 +967,13 @@ 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, "Touch controls", f.left + 124f * s, f.top + 30f * s, 14f * s, onSurface, align = Paint.Align.LEFT) + text(canvas, context.getString(R.string.touch_title), f.left + 124f * s, f.top + 30f * s, 14f * s, onSurface, align = Paint.Align.LEFT) text(canvas, gameName(), f.left + 124f * s, f.top + 46f * s, 11f * s, muted, bold = false, align = Paint.Align.LEFT) closeButton(canvas, f, s) { menuOpen = false } val controller = options.inputMode != SteamTouchConfig.INPUT_MOUSE @@ -1071,13 +983,12 @@ class SteamTouchControls( m -> SteamTouchConfig.INPUT_MOUSE else -> SteamTouchConfig.INPUT_CONTROLLER })) - tile(canvas, ref(f, s, 45f, 81f, 249f, 271f), s, "Touch Controller", controller, { setModes(!controller, mouse || controller) }) { c, r, col -> + tile(canvas, ref(f, s, 45f, 81f, 249f, 271f), s, context.getString(R.string.touch_controller), controller, { setModes(!controller, mouse || controller) }) { c, r, col -> drawPhonePad(c, r, col) } - tile(canvas, ref(f, s, 271f, 81f, 475f, 271f), s, "Mouse", mouse, { setModes(controller || mouse, !mouse) }) { c, r, col -> + 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) } @@ -1090,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)) @@ -1102,17 +1012,18 @@ class SteamTouchControls( val speed = speeds.indexOfFirst { abs(it - options.trackpadSensitivity) < 0.2f }.coerceAtLeast(1) iconButton(canvas, ref(f, s, 412f, 278f, 475f, 318f), mouse && relative, { setOptions(options.copy(trackpadSensitivity = speeds[(speed + 1) % speeds.size])) - }) { c, r, col -> text(c, listOf("Slow", "Normal", "Fast")[speed], r.centerX(), r.centerY(), r.height() * 0.32f, col) } + }) { c, r, col -> + val name = context.getString(listOf(R.string.touch_speed_slow, R.string.touch_speed_normal, R.string.touch_speed_fast)[speed]) + text(c, name, r.centerX(), r.centerY(), r.height() * 0.32f, col, maxWidth = r.width() - 6f * s) + } drawVolume(canvas, f, s) - blueButton(canvas, ref(f, s, 45f, 329f, 249f, 372f), "Edit layout", s, primary = true) { menuOpen = false; startEditing() } - blueButton(canvas, ref(f, s, 271f, 329f, 475f, 372f), "DroidDeck menu", s) { menuOpen = false; onMenu() } + blueButton(canvas, ref(f, s, 45f, 329f, 249f, 372f), context.getString(R.string.touch_edit_layout), s, primary = true) { menuOpen = false; startEditing() } + blueButton(canvas, ref(f, s, 271f, 329f, 475f, 372f), context.getString(R.string.touch_droiddeck_menu), s) { menuOpen = false; onMenu() } } 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) @@ -1131,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) } @@ -1166,26 +1076,23 @@ 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 }) canvas.drawRoundRect(r, radius, radius, Paint(Paint.ANTI_ALIAS_FLAG).apply { 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) - // A switch, top right. + text(canvas, title, r.left + 16f * s, r.top + 22f * s, 14f * s, onSurface, align = Paint.Align.LEFT, maxWidth = r.width() - 72f * s) 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, Paint(Paint.ANTI_ALIAS_FLAG).apply { color = if (on) onAccent else muted }) val artRect = RectF(r.centerX() - 58f * s, r.top + 62f * s, r.centerX() + 58f * s, r.top + 140f * s) art(canvas, artRect, if (on) onSurface else muted) - text(canvas, if (on) "On" else "Off", r.left + 16f * s, r.bottom - 18f * s, 12f * s, if (on) accent else muted, bold = false, align = Paint.Align.LEFT) + text(canvas, context.getString(if (on) R.string.touch_on else R.string.touch_off), r.left + 16f * s, r.bottom - 18f * s, 12f * s, if (on) accent else muted, bold = false, align = Paint.Align.LEFT) hots += Hot(r, toggle) } - // -- The layout tray (Steam Link: CVirtualController's tray) -- private fun drawTray(canvas: Canvas) { val (f, s) = panelFrame() @@ -1194,9 +1101,9 @@ class SteamTouchControls( closeButton(canvas, f, s) { save() } val sets = SteamTouchConfig.actionSets(config) val setName = sets.firstOrNull { it.first == editSet }?.second - val title = if (sets.size > 1 && setName != null) "${gameName()} · $setName" else "${gameName()} ${config.title ?: "Layout"}" - text(canvas, title, f.centerX(), f.top + 30f * s, 14f * s, onSurface) - text(canvas, "Drag controls onto the screen", f.centerX(), f.top + 47f * s, 11f * s, muted, bold = false) + val title = if (sets.size > 1 && setName != null) "${gameName()} · $setName" else "${gameName()} ${config.title ?: context.getString(R.string.ctrl_layout)}" + text(canvas, title, f.centerX(), f.top + 30f * s, 14f * s, onSurface, maxWidth = f.width() - 200f * s) + text(canvas, context.getString(R.string.touch_drag_hint), f.centerX(), f.top + 47f * s, 11f * s, muted, bold = false, maxWidth = f.width() - 200f * s) if (sets.size > 1) { arrowPill(canvas, ref(f, s, 120f, 26f, 150f, 44f), -1) { stepEditSet(-1) } arrowPill(canvas, ref(f, s, 474f, 26f, 504f, 44f), 1) { stepEditSet(1) } @@ -1241,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(), @@ -1251,8 +1157,8 @@ class SteamTouchControls( shader = android.graphics.RadialGradient(wheel.centerX(), wheel.centerY(), wheel.width() / 2f, Color.WHITE, Color.TRANSPARENT, android.graphics.Shader.TileMode.CLAMP) }) hots += Hot(wheel, { pickColor() }) - blueButton(canvas, ref(f, s, 45f, 329f, 232f, 372f), "Arrange", s, primary = true) { trayOpen = false } - blueButton(canvas, ref(f, s, 254f, 329f, 441f, 372f), "Discard", s) { stopEditing() } + blueButton(canvas, ref(f, s, 45f, 329f, 232f, 372f), context.getString(R.string.touch_arrange), s, primary = true) { trayOpen = false } + blueButton(canvas, ref(f, s, 254f, 329f, 441f, 372f), context.getString(R.string.touch_discard), s) { stopEditing() } } private fun arrowPill(canvas: Canvas, r: RectF, dir: Int, onTap: () -> Unit) { @@ -1265,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 } @@ -1281,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() @@ -1290,7 +1194,10 @@ class SteamTouchControls( return RectF(x - r, y - r, x + r, y + r) } - private val contextLabels = listOf("Icon", "W−", "W+", "H−", "H+", "Hide") + private val contextLabels by lazy { + listOf(R.string.touch_icon, R.string.touch_narrower, R.string.touch_wider, R.string.touch_shorter, R.string.touch_taller, R.string.touch_hide) + .map(context::getString) + } private fun contextRects(): List<RectF> { val e = editElements.getOrNull(selected) ?: return emptyList() @@ -1313,11 +1220,11 @@ class SteamTouchControls( canvas.drawRoundRect(m, m.width() * 0.15f, m.width() * 0.15f, p) drawDots(canvas, m.centerX(), m.centerY() - m.height() * 0.1f, m.width() * 0.5f, Color.WHITE) hots += Hot(m, { trayOpen = true }) - if (saving) text(canvas, "Saving…", width / 2f, height * 0.08f, 22f * max(unit(), 0.6f)) + if (saving) text(canvas, context.getString(R.string.touch_saving), width / 2f, height * 0.08f, 22f * max(unit(), 0.6f)) contextRects().forEachIndexed { i, r -> canvas.drawRoundRect(r, r.height() * 0.3f, r.height() * 0.3f, Paint(Paint.ANTI_ALIAS_FLAG).apply { color = surface }) canvas.drawRoundRect(r, r.height() * 0.3f, r.height() * 0.3f, Paint(Paint.ANTI_ALIAS_FLAG).apply { color = line2; style = Paint.Style.STROKE; strokeWidth = 1.2f }) - text(canvas, contextLabels[i], r.centerX(), r.centerY(), r.height() * 0.4f, onSurface, bold = false) + text(canvas, contextLabels[i], r.centerX(), r.centerY(), r.height() * 0.4f, onSurface, bold = false, maxWidth = r.width() - 6f) hots += Hot(r, { contextTool(i) }) } } @@ -1332,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 @@ -1422,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 @@ -1441,26 +1345,26 @@ 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) val ref = visual?.ref val binding = visual?.binding if (ref == null || binding == null) { - Toast.makeText(context, "This control has no binding in Steam to put an icon on", Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.touch_no_binding, Toast.LENGTH_SHORT).show() return } - val name = if (e.type == SteamTouchConfig.DPAD) "D-pad " + listOf("up", "down", "left", "right")[selectedArm] else label(e.type) + val name = if (e.type == SteamTouchConfig.DPAD) { + context.getString(listOf(R.string.touch_dpad_up, R.string.touch_dpad_down, R.string.touch_dpad_left, R.string.touch_dpad_right)[selectedArm]) + } else label(e.type) val host = parent as? android.view.ViewGroup ?: return - SteamTouchIconPicker(context, host, appId, "Icon for $name", binding) { picked -> + SteamTouchIconPicker(context, host, appId, context.getString(R.string.touch_icon_for, name), binding) { picked -> pendingBindings[ref] = picked rebuildVisuals() invalidate() }.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 @@ -1480,9 +1384,9 @@ class SteamTouchControls( } }, android.widget.LinearLayout.LayoutParams(dp(30), dp(30)).apply { marginEnd = dp(6) }) } - root.addView(android.widget.TextView(context).apply { text = "Colour" }) + root.addView(android.widget.TextView(context).apply { setText(R.string.ctrl_color) }) root.addView(android.widget.HorizontalScrollView(context).apply { addView(swatchRow) }) - root.addView(android.widget.TextView(context).apply { text = "Opacity"; setPadding(0, dp(12), 0, 0) }) + root.addView(android.widget.TextView(context).apply { setText(R.string.ctrl_opacity); setPadding(0, dp(12), 0, 0) }) val opacityRow = android.widget.LinearLayout(context) listOf(20, 35, 50, 65, 80, 100).forEach { percent -> opacityRow.addView(android.widget.Button(context).apply { @@ -1497,7 +1401,7 @@ class SteamTouchControls( } root.addView(android.widget.HorizontalScrollView(context).apply { addView(opacityRow) }) val host = parent as? android.view.ViewGroup ?: return - SteamTouchIconPicker.showPanel(host, "Touch layout colour", root, listOf("Done" to {})) + SteamTouchIconPicker.showPanel(host, context.getString(R.string.touch_layout_color), root, listOf(context.getString(R.string.touch_done) to {})) } private fun save() { @@ -1511,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) @@ -1528,11 +1430,11 @@ class SteamTouchControls( handler.post { saving = false if (file == null) { - Toast.makeText(context, "Could not save the touch layout", Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.touch_save_failed, Toast.LENGTH_SHORT).show() invalidate() return@post } - Toast.makeText(context, "Touch layout saved to Steam", Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.touch_saved, Toast.LENGTH_SHORT).show() onSaved() stopEditing() reloadConfig() @@ -1540,7 +1442,6 @@ class SteamTouchControls( } } - /** Called after a save: the client reads the config again when the controller reconnects. */ var onSaved: () -> Unit = {} companion object { @@ -1548,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 @@ -1563,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) @@ -1581,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 ce7f28fd6..17d665f50 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchIconPicker.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchIconPicker.kt @@ -21,13 +21,9 @@ import android.widget.HorizontalScrollView import android.widget.ImageView import android.widget.LinearLayout 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, @@ -46,11 +42,10 @@ 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 { - hint = "Label (optional)" + setHint(R.string.touch_label_hint) setText(current.label) inputType = InputType.TYPE_CLASS_TEXT setSingleLine() @@ -77,21 +72,22 @@ class SteamTouchIconPicker( setPadding(0, dp(8), 0, 0) addView(preview, LinearLayout.LayoutParams(dp(56), dp(56))) addView(TextView(context).apply { - text = if (names.isEmpty()) "Steam's icons were not found" else "${names.size} icons from Steam" + text = if (names.isEmpty()) context.getString(R.string.touch_icons_missing) + else context.resources.getQuantityString(R.plurals.touch_icons_found, names.size, names.size) setPadding(dp(12), 0, 0, 0) }) }) - left.addView(paletteRow("Icon colour", { foreground }) { foreground = it; adapter.notifyDataSetChanged(); updatePreview(preview) }) - left.addView(paletteRow("Button colour", { background }) { background = it; adapter.notifyDataSetChanged(); updatePreview(preview) }) + left.addView(paletteRow(context.getString(R.string.touch_icon_color), { foreground }) { foreground = it; adapter.notifyDataSetChanged(); updatePreview(preview) }) + left.addView(paletteRow(context.getString(R.string.touch_button_color), { background }) { background = it; adapter.notifyDataSetChanged(); updatePreview(preview) }) root.addView(left, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)) root.addView(grid, LinearLayout.LayoutParams(0, dp(250), 1.2f).apply { marginStart = dp(16) }) updatePreview(preview) showPanel(host, title, root, listOf( - "No icon" to { + context.getString(R.string.touch_no_icon) to { onPicked(current.copy(label = label.text.toString().replace(",", " ").trim(), icon = "", foreground = "", background = "")) }, - "Cancel" to {}, - "Use" to { + context.getString(R.string.common_cancel) to {}, + context.getString(R.string.common_use) to { onPicked(current.copy(label = label.text.toString().replace(",", " ").trim(), icon = icon, foreground = if (icon.isEmpty()) "" else foreground, background = if (icon.isEmpty()) "" else background)) }, @@ -130,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) @@ -146,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 { @@ -187,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 { @@ -202,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 @@ -215,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 { @@ -264,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 a58bb7f93..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,33 +6,35 @@ 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" + const val DRIVER_A6XX = "turnip26.3.0-quest2" + + internal fun quest2(model: String, device: String, manufacturer: String): Boolean = + model.equals("Quest 2", ignoreCase = true) || + ((manufacturer.equals("Oculus", ignoreCase = true) || manufacturer.equals("Meta", ignoreCase = true)) && + device == "hollywood") - // Quest 2 has an A6xx GPU and cannot use this bundled A7xx driver. internal fun supported(model: String, device: String, manufacturer: String): Boolean = - model.equals("Quest 3", ignoreCase = true) || model.equals("Quest 3S", ignoreCase = true) || + quest2(model, device, manufacturer) || model.equals("Quest 3", ignoreCase = true) || model.equals("Quest 3S", ignoreCase = true) || ((manufacturer.equals("Oculus", ignoreCase = true) || manufacturer.equals("Meta", ignoreCase = true)) && (device == "eureka" || device == "panther")) fun isSupported(): Boolean = supported(Build.MODEL, Build.DEVICE, Build.MANUFACTURER) + fun isQuest2(): Boolean = quest2(Build.MODEL, Build.DEVICE, Build.MANUFACTURER) fun initialize(context: Context) { if (!isSupported()) return val prefs = context.getSharedPreferences("session", Context.MODE_PRIVATE) - if (!prefs.getBoolean("questDefaultsV1", false)) { - // Auto would download an unwrapped display driver and lose the compatibility path. - // Apply once so deliberate changes made during headset testing remain possible. + val defaultsKey = if (isQuest2()) "quest2DefaultsV1" else "questDefaultsV1" + if (!prefs.getBoolean(defaultsKey, false)) { prefs.edit() .putString("gpuDriverMode", SessionPrefs.GPU_DRIVERS_MANUAL) - .putString("androidDriver", DRIVER) + .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("questDefaultsV1", true) + .putBoolean(defaultsKey, true) .apply() } val report = buildString { @@ -51,7 +53,7 @@ object QuestExperimental { appendLine("androidDriver=${SessionPrefs.androidDriver(context)}") appendLine("steamSurfaceControlWorkaround=${SessionPrefs.hdr(context, "steam")}") appendLine("GPUOBJ fallback installed in Android driver and ARM64 libblsession") - appendLine("Experimental Quest 3/3S compatibility; 2D panel application") + appendLine("Experimental Quest 2/3/3S compatibility; 2D panel application") } Log.i(TAG, report) runCatching { File(context.filesDir, "quest-startup.txt").writeText(report) } 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 3d15d3958..8e9566f99 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 ff286eebc..9ed3ec818 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -7,11 +7,11 @@ import com.droiddeck.launcher.R import com.droiddeck.launcher.core.CpuAllocationDefaults import com.droiddeck.launcher.core.GameEnvironment import com.droiddeck.launcher.core.TextureFiltering +import com.droiddeck.launcher.core.ZramSupport 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) @@ -31,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 @@ -60,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) { @@ -79,14 +76,21 @@ 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]. */ + /** + * Hidden A/B switch for the Epic installer: "adaptive" (default: native parallel fetch written + * straight into staged files), "lookahead" (the native ordered-lookahead engine that caches each + * chunk and copies it into staged files), "batch" (the older whole-cache native fetch, then + * native assembly; needs about twice the game's size free) or "java" (the bounded Java writer). + * No UI; set it in the `session` preferences for benchmarks. + */ + fun storesEpicMode(context: Context): String = prefs(context).getString("storesEpicMode", "adaptive") ?: "adaptive" + fun storesOpenTab(context: Context): String = prefs(context).getString("storesOpenTab", STORES_OPEN_LIBRARY)?.takeIf { it == STORES_OPEN_STORE } ?: STORES_OPEN_LIBRARY @@ -94,7 +98,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) { @@ -104,25 +107,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) { @@ -131,11 +127,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) { @@ -147,9 +141,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) { @@ -160,12 +151,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 @@ -176,12 +161,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) @@ -194,11 +173,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 @@ -206,10 +180,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 @@ -217,7 +187,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() @@ -225,9 +194,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) @@ -236,20 +202,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) { @@ -260,7 +220,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) { @@ -275,7 +234,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( @@ -285,37 +243,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) { @@ -339,10 +284,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 { @@ -352,7 +293,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() @@ -368,28 +308,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) { @@ -402,11 +326,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) { @@ -429,43 +348,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 @@ -474,24 +371,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", "") ?: "" @@ -500,12 +388,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", "") ?: "" @@ -513,11 +396,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) @@ -525,7 +403,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) @@ -533,24 +410,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 -> @@ -573,24 +439,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) { @@ -605,18 +457,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) @@ -629,10 +475,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] } @@ -642,17 +484,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() } @@ -663,31 +500,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) @@ -695,7 +525,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", "") ?: "" @@ -703,14 +532,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() @@ -728,7 +555,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 } @@ -738,21 +564,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 @@ -762,25 +582,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) { @@ -789,28 +596,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) } @@ -834,9 +632,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) @@ -846,10 +642,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) @@ -859,21 +653,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 } @@ -885,7 +674,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) @@ -894,7 +682,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 @@ -903,7 +690,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 @@ -980,16 +766,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) @@ -998,7 +779,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) { @@ -1008,8 +788,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 @@ -1022,7 +800,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 @@ -1057,15 +834,16 @@ object SessionPrefs { }.apply() } - // ── Game storage ──────────────────────────────────────────────────────────────────────── + fun zramCompression(context: Context): Boolean { + val prefs = prefs(context) + return if (prefs.contains("zramCompression")) prefs.getBoolean("zramCompression", false) else ZramSupport.recommended(context) + } + + fun setZramCompression(context: Context, on: Boolean) { + prefs(context).edit().putBoolean("zramCompression", on).apply() + } + - /** - * 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 e35be45a9..720d13e8a 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,153 +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) } @@ -728,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() @@ -740,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() @@ -765,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, @@ -813,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"), @@ -825,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))) @@ -898,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") @@ -908,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() @@ -919,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() @@ -928,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()) { @@ -985,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") @@ -993,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>>() @@ -1025,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(')') @@ -1138,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 } + "/" @@ -1159,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) @@ -1187,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) { @@ -1273,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() } @@ -1289,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" } @@ -1411,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 } @@ -1425,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 @@ -1449,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) } @@ -1466,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) @@ -1489,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) } @@ -1510,13 +1275,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 { @@ -1554,13 +1312,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) { @@ -1569,11 +1320,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 { @@ -1605,10 +1351,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") @@ -1632,7 +1374,6 @@ class SessionService : Service() { override fun onBind(intent: Intent?): IBinder? = null - // ── Keeping the process alive ─────────────────────────────────────────────────────────── private fun acquireLocks() { try { @@ -1651,7 +1392,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 { @@ -1661,8 +1402,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") } } @@ -1682,11 +1421,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) @@ -1783,7 +1520,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()) { @@ -1797,25 +1533,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) } @@ -1836,10 +1558,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 @@ -1850,19 +1570,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" @@ -1903,7 +1618,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 @@ -1922,7 +1636,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 f1c41b75a..912ac32bc 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,28 +43,19 @@ 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 @Volatile var steamVr = false - /** 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 @@ -79,57 +65,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 @@ -138,7 +107,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 new file mode 100644 index 000000000..1d4e6aec1 --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/session/SettingsReset.kt @@ -0,0 +1,67 @@ +package com.droiddeck.launcher.session + +import android.content.Context +import android.util.Log +import com.droiddeck.launcher.core.GameEnvironment +import com.droiddeck.launcher.input.ControllerPrefs +import com.droiddeck.launcher.runtime.LinuxRuntime + +object SettingsReset { + private const val TAG = "SettingsReset" + + internal val SESSION_KEYS = setOf( + "orientation", "launcherFullscreen", "animations", "theme", "runSteamAtStartup", "addedGamesArt", + "gameStoresEnabled", "gameStoresSpeedTier", "gameStoresParallel", "storesOpenTab", "storesShowMature", + "hud", "hudDeck", "backActionsInverted", "touch", "osc", "shape", "steamController", + "gamepadLeft", "gamepadRight", "pipAutoEnter", "unfoldedControls", "stretch16x9", + "gpuDriverMode", "androidDriver", "linuxDriver", "linuxDriver.steam", "linuxDriver.desktop", + "tuSysmem", "glThread", "noGlError", "zinkLazy", "mangoapp", "gpuClockPin", "gamescopeRealtime", + "directAudio", "mic", "wifiDiscovery", "clientCpusOverride", "clientCpus", "gameCpus", + "noXalia", "fastSync", "fsyncFirst", "syncFallback", "prootNoSeccomp", "prootFastPath", + "guestHostname", "fexPreset", "forceSsbs", "logs", "storageDiagnostics", + "upscaler", "upscaleSharpness", "screenEffects", "textureAnisotropy", "textureLodBias", + "steamDownloadsInBackground", "steam_offline", + "questDefaultsV1", + ) + + internal val SESSION_PREFIXES = listOf( + "resolutionCap.", "displayResolution.", "customRes.", "hdr.", "fpsLimit.", "suspendPolicy.", + ) + + private val STEAM_TOUCH_KEYS = listOf("feedback", "autoFade", "gyro", "shake") + + internal fun isSessionSetting(key: String) = key in SESSION_KEYS || SESSION_PREFIXES.any { key.startsWith(it) } + + fun resetAll(context: Context, gameProfiles: Boolean, gameScopes: Collection<String> = emptyList()): Boolean { + var ok = true + val session = context.getSharedPreferences("session", Context.MODE_PRIVATE) + session.edit().apply { session.all.keys.filter { isSessionSetting(it) }.forEach { remove(it) } }.commit() + ControllerPrefs.resetAll(context) + context.getSharedPreferences("app-ui", Context.MODE_PRIVATE).edit().clear().commit() + context.getSharedPreferences("frame_gen", Context.MODE_PRIVATE).edit().clear().commit() + context.getSharedPreferences("steam_touch", Context.MODE_PRIVATE).edit() + .apply { STEAM_TOUCH_KEYS.forEach { remove(it) } }.commit() + com.droiddeck.launcher.agent.AgentAccess.setCommandsEnabled(context, false) + + if (gameProfiles) { + val scopes = (gameScopes + runCatching { GameEnvironmentStore.read(context).games.keys }.getOrDefault(emptySet()) + + SessionPrefs.gameTextureFiltering(context).keys) + .filter { it.isNotEmpty() && GameEnvironment.validScope(it) }.toSet() + scopes.forEach { scope -> + runCatching { GameProfileManager.reset(context, scope) } + .onFailure { ok = false; Log.w(TAG, "Could not reset the profile of $scope", it) } + } + session.edit().remove("gameTextureFiltering").commit() + runCatching { GameEnvironmentStore.save(context, GameEnvironmentStore.read(context).copy(shared = emptyMap())) } + .onFailure { ok = false; Log.w(TAG, "Could not clear the Default profile's environment", it) } + } + + QuestExperimental.initialize(context) + runCatching { OfflineMode.apply(context, LinuxRuntime.rootDir(context)) } + .onFailure { ok = false; Log.w(TAG, "Could not turn offline mode off", it) } + runCatching { GameEnvironmentStore.publish(context) } + .onFailure { ok = false; Log.w(TAG, "Could not update game environment", it) } + Log.i(TAG, "settings reset (game profiles: $gameProfiles, ok: $ok)") + return ok + } +} 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..25850f747 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, ) { @@ -90,20 +68,15 @@ class StoreGameSidecar( const val VERSION = 1 const val STATE_INSTALLED = "installed" const val STATE_INSTALLING = "installing" + const val STATE_UPDATING = "updating" 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 +84,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 +96,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 +110,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 +126,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..d36f88678 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,86 @@ 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 { + const val SPACE_UNKNOWN = -1L 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 } + val freeBytes: Long get() = availableBytes(root) + } + + data class StorageCheck( + val enough: Boolean, + val requiredBytes: Long, + val availableBytes: Long, + val errorMessage: String? = null + ) + + @JvmStatic + fun availableBytes(file: File): Long = probeAvailableBytes(file, { StatFs(it.path).availableBytes }, { it.usableSpace }) + + internal fun probeAvailableBytes(file: File, statBytes: (File) -> Long, usableBytes: (File) -> Long): Long { + var dir: File? = if (file.isDirectory) file else file.parentFile + if (dir == null) dir = file + while (dir != null && !dir.exists()) dir = dir.parentFile + val existing = dir ?: return SPACE_UNKNOWN + return try { + statBytes(existing).coerceAtLeast(SPACE_UNKNOWN) + } catch (e: Exception) { + StoresState.logLine("storage: StatFs failed for store install target (${e.javaClass.simpleName})") + try { + // File's zero also means an unavailable measurement; only StatFs establishes zero. + usableBytes(existing).takeIf { it > 0L } ?: SPACE_UNKNOWN + } catch (fallback: Exception) { + StoresState.logLine("storage: usable-space fallback failed for store install target (${fallback.javaClass.simpleName})") + SPACE_UNKNOWN + } + } + } + + @JvmStatic + fun insufficientSpace(required: Long, available: Long): Boolean = required > 0L && available >= 0L && available < required + + /** + * The catalog size is the known minimum for an install. Epic's actual chunk-cache requirement + * is determined from its manifest before chunk downloads begin. + */ + fun estimatedBytesNeeded(item: CatalogItem): Long = StoreSizes.size(item).coerceAtLeast(0L) + + /** Pre-install storage check to detect insufficient space before starting. */ + fun checkStorage(context: Context, item: CatalogItem, targetRoot: File): StorageCheck { + val size = estimatedBytesNeeded(item) + val existing = existingFolder(context, item.store, item.id, item.title) + val freeTarget = availableBytes(existing ?: targetRoot) + // Backends account for completed files on resume; the catalog cannot describe remaining work. + // Comparing the full catalog size to remaining free space would reject valid resumes. + if (item.store == Store.EPIC || existing != null) return StorageCheck(enough = true, requiredBytes = size, availableBytes = freeTarget) + if (size <= 0L) return StorageCheck(enough = true, requiredBytes = 0L, availableBytes = freeTarget) + if (insufficientSpace(size, freeTarget)) { + val msg = context.getString(com.droiddeck.launcher.R.string.stores_storage_insufficient, formatBytes(size), formatBytes(freeTarget)) + return StorageCheck(enough = false, requiredBytes = size, availableBytes = freeTarget, errorMessage = msg) + } + return StorageCheck(enough = true, requiredBytes = size, availableBytes = freeTarget) } - /** 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 +95,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 +104,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 +114,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 +124,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 +140,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 +157,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..0e629a6e7 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreInstalls.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreInstalls.kt @@ -7,62 +7,52 @@ import com.droiddeck.launcher.frontend.AddedGames import com.droiddeck.launcher.frontend.LibraryCache import com.droiddeck.launcher.frontend.Library import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption -/** - * 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 + val before = StoreGameSidecar.read(folder) + if (before?.isInstalled == true || before?.state == StoreGameSidecar.STATE_UPDATING) 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. - */ + /** Epic commits verified files individually, not a whole-game transaction. Keep updates resumable. */ + @JvmStatic + fun markUpdating(folder: File) { + val before = StoreGameSidecar.read(folder) ?: return + if (!before.isInstalled) return + val stage = File.createTempFile(".droiddeck-update-", ".stage", folder) + try { + java.io.FileOutputStream(stage).use { + it.write(before.copy(state = StoreGameSidecar.STATE_UPDATING, installVersion = "").toJson().toString(2).toByteArray(Charsets.UTF_8)) + it.fd.sync() + } + Files.move(stage.toPath(), StoreGameSidecar.file(folder).toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING) + } finally { + stage.delete() + } + } + 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 +65,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 +76,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,19 +83,14 @@ 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({ - val keep = StoreGameSidecar.read(folder)?.isInstalled == true + val sidecar = StoreGameSidecar.read(folder) + val keep = sidecar?.isInstalled == true || sidecar?.state == StoreGameSidecar.STATE_UPDATING caches.forEach { deleteTree(it) } if (!keep) deleteTree(folder) - StoresState.logLine(if (keep) "removed the partial update of ${folder.name}" else "removed ${folder.name}") + StoresState.logLine(if (keep) "removed update caches for ${folder.name}; verified file swaps are retained" else "removed ${folder.name}") StoresState.notifyLibraryChanged(app) }, "store-cancel-clean").start() } 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..74af04fb8 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,38 +159,42 @@ 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) + val folder = StoreInstallRoot.folderFor(app, item.store, item.id, item.title, root ?: StoreInstallRoot.installRoot(app)) + val target = folder.parentFile!!.parentFile!! + val check = StoreInstallRoot.checkStorage(app, item, target) + if (!check.enough) { + val errorMsg = check.errorMessage ?: app.getString(com.droiddeck.launcher.R.string.stores_storage_insufficient, formatBytes(check.requiredBytes), formatBytes(check.availableBytes)) + val entry = com.droiddeck.launcher.stores.download.DownloadEntry( + item.store, + item.id, + item.title, + cover = item.imageUrl, + bytesTotal = item.sizeBytes, + location = StoreInstallRoot.labelFor(app, folder) + ) + com.droiddeck.launcher.stores.download.SavedQueue.remember(app, item, target) + com.droiddeck.launcher.stores.download.DownloadQueue.recordFailed(app, entry, errorMsg) { + install(app, item, target) + } + return + } com.droiddeck.launcher.stores.download.SavedQueue.remember(app, item, target) 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 +211,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 +227,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 +241,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 9f16663a5..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,12 +41,11 @@ 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 { if (cached.isNotEmpty()) publish(app, cached) - if (!quiet) StoresState.status[store] = "Fetching your library…" + if (!quiet) StoresState.status[store] = app.getString(com.droiddeck.launcher.R.string.stores_sync_library) } val result = AmazonLibrary.sync(app, force) { line -> if (!quiet) StoresState.post { StoresState.status[store] = line } } 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) { @@ -94,7 +85,7 @@ object AmazonBackend : StoreBackend { private val cancelled = AtomicBoolean(false) override fun run(handle: DownloadQueue.JobHandle): String? { - handle.stage(DownloadStage.MANIFEST, "Checking sign-in…") + handle.stage(DownloadStage.MANIFEST, app.getString(com.droiddeck.launcher.R.string.stores_sync_sign_in)) val token = AmazonCredentialStore.getValidAccessToken(app) ?: throw AmazonDownloadManager.InstallException("Not signed in to Amazon Games") StoreInstalls.begin(folder, Store.AMAZON, game.productId, game.title, item.tallImageUrl ?: item.imageUrl, item.imageUrl) var downloading = false @@ -111,7 +102,7 @@ object AmazonBackend : StoreBackend { override fun onSizes(downloadBytes: Long, diskBytes: Long) { handle.diskSize(diskBytes) } }) ?: return null if (cancelled.get()) return null - handle.stage(DownloadStage.INSTALL, "Registering with Steam…") + handle.stage(DownloadStage.INSTALL, app.getString(com.droiddeck.launcher.R.string.stores_registering)) AmazonLibrary.rememberSize(app, game.productId, result.bytes) AmazonPrefs.get(app).edit().putString("version_${game.productId}", result.versionId).apply() val spec = AmazonLaunchHelper.buildLaunchSpec(folder, game.title) @@ -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..6145f0250 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 { @@ -77,9 +69,10 @@ public final class AmazonDownloadManager { if (manifestBytes == null) throw new InstallException("The manifest could not be downloaded"); AmazonManifest.ParsedManifest manifest = AmazonManifest.parse(manifestBytes); cb.onLog("amazon: " + manifest.allFiles.size() + " files, " + fmt(manifest.totalInstallSize)); - long usable = installDir.getUsableSpace(); - if (manifest.totalInstallSize > 0 && usable > 0 && manifest.totalInstallSize > usable) - throw new InstallException("Not enough free space: need " + fmt(manifest.totalInstallSize) + ", only " + fmt(usable) + " free"); + long remainingBytes = remainingInstallBytes(manifest.allFiles, installDir); + long usable = com.droiddeck.launcher.stores.StoreInstallRoot.availableBytes(installDir); + if (com.droiddeck.launcher.stores.StoreInstallRoot.insufficientSpace(remainingBytes, usable)) + throw new InstallException("Not enough free space: need " + fmt(remainingBytes) + ", only " + fmt(usable) + " free"); cb.onSizes(manifest.totalInstallSize, manifest.totalInstallSize); cb.onBytes(0, manifest.totalInstallSize, 0); if (cancel.get()) return null; @@ -134,7 +127,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 { @@ -184,7 +176,7 @@ public final class AmazonDownloadManager { AtomicLong lastSpeedMs, AtomicLong lastSpeedBytes, AtomicLong speedBps, Callback cb, AtomicBoolean cancel) { File destFile = new File(installDir, file.unixPath()); File tmpFile = new File(installDir, file.unixPath() + ".tmp"); - if (destFile.exists() && destFile.length() == file.size) { totalDownloaded.addAndGet(file.size); return true; } + if (fileComplete(file, destFile)) { totalDownloaded.addAndGet(file.size); return true; } String fileUrl = AmazonApiClient.appendPath(baseUrl, "files/" + file.hashHex()); for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) { destFile.getParentFile().mkdirs(); @@ -211,7 +203,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 +231,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); @@ -256,6 +246,21 @@ public final class AmazonDownloadManager { public static boolean isInstalled(File installDir) { return new File(installDir, COMPLETE_MARKER).exists(); } + static long remainingInstallBytes(List<AmazonManifest.ManifestFile> files, File installDir) { + long remaining = 0; + for (AmazonManifest.ManifestFile file : files) { + if (!fileComplete(file, new File(installDir, file.unixPath()))) remaining += file.size; + } + return remaining; + } + + private static boolean fileComplete(AmazonManifest.ManifestFile file, File output) { + if (!output.isFile() || output.length() != file.size) return false; + if (file.hashAlgorithm != 0 || file.hashBytes.length == 0) return true; + try { return Arrays.equals(sha256(output), file.hashBytes); } + catch (Exception e) { return false; } + } + private static byte[] sha256(File file) throws Exception { MessageDigest digest = MessageDigest.getInstance("SHA-256"); try (FileInputStream fis = new FileInputStream(file)) { 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 197d6a816..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 @@ -3,6 +3,7 @@ package com.droiddeck.launcher.stores.amazon import android.content.Context import android.content.SharedPreferences import android.util.Log +import com.droiddeck.launcher.R import com.droiddeck.launcher.stores.CatalogItem import com.droiddeck.launcher.stores.Store import com.droiddeck.launcher.stores.StoreNet @@ -13,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" @@ -66,45 +61,43 @@ object AmazonLibrary { val p = AmazonPrefs.get(app) val cachedList = cached(app) if (!force && cachedList.isNotEmpty() && System.currentTimeMillis() - p.getLong(LAST_SYNC_KEY, 0L) < THROTTLE_MS) { - backfillPosters(p, cachedList, onStatus) + backfillPosters(app, p, cachedList, onStatus) return SyncResult.Throttled } - onStatus("Checking sign-in…") + onStatus(app.getString(R.string.stores_sync_sign_in)) val creds = AmazonCredentialStore.load(app) ?: return SyncResult.NotLoggedIn val token = AmazonCredentialStore.getValidAccessToken(app) ?: return SyncResult.Failed(StoresState.notSignedInLine(Store.AMAZON)) - onStatus("Fetching game list…") - // Refused with the stored token (revoked, or an expiry read wrong): once more on a fresh one. + onStatus(app.getString(R.string.stores_sync_game_list)) 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("Amazon Games did not answer for your library: refresh, or sign in again") else SyncResult.Ok(cachedList) - if (all.isEmpty()) return if (cachedList.isEmpty()) SyncResult.Failed("No games found in your Amazon library") else SyncResult.Ok(cachedList) + if (all == null) return if (cachedList.isEmpty()) SyncResult.Failed(app.getString(R.string.stores_sync_amazon_no_answer)) else SyncResult.Ok(cachedList) + if (all.isEmpty()) return if (cachedList.isEmpty()) SyncResult.Failed(app.getString(R.string.stores_sync_no_games, Store.AMAZON.label)) else SyncResult.Ok(cachedList) val games = all.filter { !(it.isDLC && it.parentProductId.isNotEmpty()) }.ifEmpty { all } .sortedWith { a, b -> a.title.compareTo(b.title, ignoreCase = true) } for (fresh in games) cachedList.firstOrNull { it.productId == fresh.productId }?.let { fresh.versionId = it.versionId } saveCache(p, games) p.edit().putLong(LAST_SYNC_KEY, System.currentTimeMillis()).apply() - backfillPosters(p, games, onStatus) + backfillPosters(app, p, games, onStatus) Log.i(TAG, "sync: entitlements=${all.size} games=${games.size}") return SyncResult.Ok(games) } catch (e: Exception) { Log.w(TAG, "sync failed: ${e.message}") - return SyncResult.Failed(e.message ?: "Sync failed") + return SyncResult.Failed(e.message ?: context.getString(R.string.stores_sync_failed)) } finally { syncing.set(false) } } - private fun backfillPosters(p: SharedPreferences, games: List<AmazonGame>, onStatus: (String) -> Unit) { + private fun backfillPosters(app: Context, p: SharedPreferences, games: List<AmazonGame>, onStatus: (String) -> Unit) { val missing = games.filter { it.productId.isNotEmpty() && !p.contains("vcover_${it.productId}") }.take(POSTER_BUDGET) if (missing.isEmpty()) return - onStatus("Fetching cover art…") + onStatus(app.getString(R.string.stores_sync_cover_art)) val pool = Executors.newFixedThreadPool(4) val futures = missing.map { g -> pool.submit(Callable { g.productId to StoreNet.sgdbPoster(g.title) }) } pool.shutdown() 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..e2549a3be 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) @@ -128,8 +88,7 @@ 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). */ + private class Item(var entry: DownloadEntry, val factory: (() -> DownloadJob)? = null, val retry: (() -> Unit)? = null, var job: DownloadJob? = null, val cancelled: AtomicBoolean = AtomicBoolean(false), var pauseRequested: Boolean = false) { 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) { @@ -185,6 +137,32 @@ object DownloadQueue { advance(app) } + /** Immediately records a failed download (e.g. from pre-flight storage check). */ + fun recordFailed(context: Context, entry: DownloadEntry, error: String, retry: (() -> Unit)? = null) { + val app = context.applicationContext + val now = System.currentTimeMillis() + synchronized(lock) { + if (items[entry.key]?.entry?.isActive == true) return + items.remove(entry.key) + heldPaused.remove(entry.key) + items[entry.key] = Item( + entry.copy( + state = DownloadState.FAILED, + error = error, + speedBps = 0, + etaSeconds = -1, + startedAt = now, + finishedAt = now + ), + retry = retry + ) + renumber() + publishLocked() + } + StoresState.logLine("${entry.store.label}: \"${entry.name}\" failed: $error") + StoreDownloadService.ended(app, entry.copy(state = DownloadState.FAILED, error = error), installed = false, gamesKey = null) + } + fun pause(key: String) { val item = synchronized(lock) { items[key] } ?: return synchronized(lock) { @@ -199,14 +177,21 @@ object DownloadQueue { fun resume(context: Context, key: String) { val app = context.applicationContext + val retry: (() -> Unit)? synchronized(lock) { val item = items[key] ?: return if (item.entry.state != DownloadState.PAUSED && item.entry.state != DownloadState.FAILED) return + retry = item.retry + if (retry == null && item.factory == null) return items.remove(key) - items[key] = Item(item.entry.copy(state = DownloadState.QUEUED, speedBps = 0, etaSeconds = -1, error = null, startedAt = System.currentTimeMillis()), item.factory) + if (retry == null) { + items[key] = Item(item.entry.copy(state = DownloadState.QUEUED, speedBps = 0, etaSeconds = -1, error = null, startedAt = System.currentTimeMillis()), item.factory) + } renumber() publishLocked() } + // A preflight retry rechecks storage and enqueues the real backend, never a running placeholder. + if (retry != null) { retry(); return } StoresState.logLine("resumed \"${entry(key)?.name}\"") advance(app) } @@ -230,31 +215,24 @@ 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() + Thread({ runCatching { item.factory?.invoke()?.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 if (item.entry.state == DownloadState.FAILED || item.entry.state == DownloadState.PAUSED) { - Thread({ runCatching { item.factory().cancel(deleteFiles = true) } }, "store-dl-clear").start() + Thread({ runCatching { item.factory?.invoke()?.cancel(deleteFiles = true) } }, "store-dl-clear").start() } synchronized(lock) { items.remove(key); publishLocked() } 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 +240,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 +264,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 +282,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,14 +304,12 @@ 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 var error: String? = null try { - val job = item.factory() + val job = checkNotNull(item.factory).invoke() synchronized(lock) { item.job = job } StoresState.logLine("${item.entry.store.label}: fetching manifest for \"${item.entry.name}\"") result = job.run(handle) @@ -367,7 +333,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..a32bceeb6 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,23 +235,25 @@ 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) val code = entry.key.hashCode() - val n = Notification.Builder(app, DONE_CHANNEL) + val builder = Notification.Builder(app, DONE_CHANNEL) .setSmallIcon(if (installed) android.R.drawable.stat_sys_download_done else android.R.drawable.stat_notify_error) .setContentTitle(app.getString(if (installed) R.string.stores_notification_installed else R.string.stores_notification_failed, entry.name)) .setAutoCancel(true) .setContentIntent(if (installed && gamesKey != null) openPage(app, gamesKey, code + 10) else openDownloads(app, code + 11)) - .build() + if (!installed && !entry.error.isNullOrBlank()) { + builder.setContentText(entry.error) + builder.style = Notification.BigTextStyle().bigText(entry.error) + } + val n = builder.build() runCatching { app.getSystemService(NotificationManager::class.java)?.notify(ID + 1 + (code and 0xFFFF), n) } } } } -/** 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 57b5c4812..5603ba742 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,12 +41,11 @@ 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 { if (cached.isNotEmpty()) StoresState.library[store] = com.droiddeck.launcher.stores.mergeItems(StoresState.library[store], cached) - if (!quiet) StoresState.status[store] = "Fetching your library…" + if (!quiet) StoresState.status[store] = app.getString(com.droiddeck.launcher.R.string.stores_sync_library) } val result = EpicLibrary.sync(app, force) { line -> if (!quiet) StoresState.post { StoresState.status[store] = line } } 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,27 +98,21 @@ 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) override fun run(handle: DownloadQueue.JobHandle): String? { - handle.stage(DownloadStage.MANIFEST, "Checking sign-in…") + handle.stage(DownloadStage.MANIFEST, app.getString(com.droiddeck.launcher.R.string.stores_sync_sign_in)) val token = EpicCredentialStore.getValidAccessToken(app) ?: throw EpicDownloadManager.InstallException("Not signed in to Epic Games") StoreInstalls.begin(folder, Store.EPIC, item.id, item.title, item.tallImageUrl ?: item.imageUrl, item.imageUrl) if (scratch == null && hasChunks(File(folder, ".chunks"))) handle.log("epic: resuming \"${item.title}\" with the chunks already beside it") @@ -146,12 +136,14 @@ object EpicBackend : StoreBackend, EpicLaunchSupport { EpicLibrary.rememberSize(app, item.id, diskBytes) } override fun onStage(stage: String, done: Long, total: Long, items: Int, itemsTotal: Int) { + // Assembly overlaps chunk fetching; only the final install event advances the UI. + if (stage == "stream") return handle.stage(when (stage) { "check" -> DownloadStage.MANIFEST; "verify" -> DownloadStage.VERIFY; else -> DownloadStage.INSTALL }) handle.stageProgress(done, total, items, itemsTotal, bytes = stage == "install") } }) ?: return null if (cancelled.get()) return null - handle.stage(DownloadStage.INSTALL, "Registering with Steam…") + handle.stage(DownloadStage.INSTALL, app.getString(com.droiddeck.launcher.R.string.stores_registering)) EpicLibrary.rememberSize(app, item.id, result.bytes) val deploymentId = runCatching { JSONObject(manifestJson).optString("deploymentId", "") }.getOrDefault("") val exe = StoreExe.pick(folder, result.launchExe, item.title) @@ -161,7 +153,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 +168,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 +176,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..88307fd25 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 @@ -4,13 +4,15 @@ import android.content.Context; import android.util.Log; import com.droiddeck.launcher.stores.StoreLog; +import com.droiddeck.launcher.stores.StoreInstallRoot; +import com.droiddeck.launcher.stores.StoreInstalls; +import com.droiddeck.launcher.stores.StoresNative; import com.droiddeck.launcher.stores.download.StoreDownloadTier; import org.json.JSONArray; import org.json.JSONObject; import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -21,6 +23,10 @@ import java.net.URL; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Iterator; @@ -29,41 +35,59 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.TimeoutException; 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"; private static final String UA = EpicAuthClient.USER_AGENT; + /** Keep reusable chunk data bounded even when a manifest shares chunks across distant files. */ + static final long MAX_RETAINED_CHUNK_CACHE_BYTES = 64L * 1024 * 1024; + static final int PREFETCH_PARTS = 8; + static final String STAGING_DIRECTORY = ".droiddeck-epic-staging"; + private static final String STAGING_OWNER = "DroidDeck Epic staging v1\n"; + /** Most the adaptive engine caches of shared chunks; more only saves the occasional refetch. */ + static final long ADAPTIVE_CACHE_MAX_BYTES = 1024L * 1024 * 1024; + /** Its budget when free space cannot be read. */ + static final long ADAPTIVE_CACHE_DEFAULT_BYTES = 256L * 1024 * 1024; + /** + * Below this, internal scratch is too small to be worth a cache of its own: the shared chunks + * then go beside the game (still released at their last use) rather than failing the install. + */ + static final long ADAPTIVE_CACHE_MIN_SCRATCH_BYTES = MAX_RETAINED_CHUNK_CACHE_BYTES; + /** Chunks being decoded, written or released at any moment, beyond the budget itself. */ + static final long ADAPTIVE_TRANSIENT_BYTES = 64L * 1024 * 1024; + + static final String MODE_ADAPTIVE = "adaptive"; + static final String MODE_LOOKAHEAD = "lookahead"; + static final String MODE_BATCH = "batch"; + static final String MODE_JAVA = "java"; + + /** Which writer {@link #install} asks for and how wide the native fetch may go. */ + static final class NativeOptions { + final String mode; + final int maxWorkers, processWorkers; + final String tierId; + NativeOptions(String mode, int maxWorkers, int processWorkers, String tierId) { + this.mode = mode; this.maxWorkers = maxWorkers; this.processWorkers = processWorkers; this.tierId = tierId; + } + /** The default: the adaptive engine at the Fast tier's window. */ + static NativeOptions defaults() { return new NativeOptions(MODE_ADAPTIVE, 32, 2, "fast"); } + } public interface Callback { 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 +123,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 +130,18 @@ 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<>(); + /** The manifest as downloaded, for the native engines (which parse it themselves); null = Java only. */ + byte[] rawBytes = null; } - /** 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 +153,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; @@ -153,22 +168,51 @@ public final class EpicDownloadManager { Manifest manifest = parseManifest(manifestBytes); if (manifest == null) throw new InstallException("The manifest could not be parsed"); manifest.cdnUrls = cdnUrls; + manifest.rawBytes = manifestBytes; cb.onLog("epic: chunkDir=" + manifest.chunkDir + " chunks=" + manifest.uniqueChunks.size() + " files=" + manifest.files.size() + " exe=" + manifest.launchExe); + StoreDownloadTier tier = StoreDownloadTier.Companion.current(ctx); + String mode = com.droiddeck.launcher.session.SessionPrefs.INSTANCE.storesEpicMode(ctx); + NativeOptions options = new NativeOptions(mode, Math.max(1, Math.min(128, tier.getNetworkWindow())), Math.max(2, tier.getProcessWorkers()), tier.getId()); + return installManifest(manifest, installDirPath, chunkCacheDirPath, installTags, cancel, cb, options); + } catch (InstallException e) { + throw e; + } catch (Exception e) { + Log.w(TAG, "install failed", e); + throw new InstallException(e.getClass().getSimpleName() + (e.getMessage() != null ? ": " + e.getMessage() : "")); + } + } + static Result installManifest(Manifest manifest, String installDirPath, String chunkCacheDirPath, + List<String> installTags, AtomicBoolean cancel, Callback cb) throws InstallException { + return installManifest(manifest, installDirPath, chunkCacheDirPath, installTags, cancel, cb, NativeOptions.defaults()); + } + + static Result installManifest(Manifest manifest, String installDirPath, String chunkCacheDirPath, + List<String> installTags, AtomicBoolean cancel, Callback cb, + NativeOptions nativeOptions) throws InstallException { + Result result = installSelected(manifest, installDirPath, chunkCacheDirPath, installTags, cancel, cb, nativeOptions); + // A finished install leaves no staging; the owned namespace is recreated by the next run. + if (result != null) releaseOwnedStaging(new File(installDirPath)); + return result; + } + + private static Result installSelected(Manifest manifest, String installDirPath, String chunkCacheDirPath, + List<String> installTags, AtomicBoolean cancel, Callback cb, + NativeOptions nativeOptions) throws InstallException { + try { + List<CdnUrl> cdnUrls = manifest.cdnUrls; 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(); + File stagingDir = recoverOwnedStaging(installDir, manifest.files); List<FileInfo> selected = resolveInstallFiles(manifest, installTags); cb.onLog("epic: " + selected.size() + "/" + manifest.files.size() + " files for tags " + (installTags == null ? "(all)" : installTags.toString())); 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,117 +235,159 @@ 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(); - if (cachePath.isEmpty()) { - long free = installDir.getUsableSpace(); - if (free > 0 && cacheMissing + pendingBytes > free) throw new InstallException("Not enough free space: need " + fmt(cacheMissing + pendingBytes) + " (" + fmt(pendingBytes) + " of game files + " + fmt(cacheMissing) + " of download cache), only " + fmt(free) + " free"); - } else { - long freeCache = chunkCacheDir.getUsableSpace(), freeGame = installDir.getUsableSpace(); - if (freeCache > 0 && cacheMissing > freeCache) throw new InstallException("Not enough internal space for the download cache: need " + fmt(cacheMissing) + ", only " + fmt(freeCache) + " free"); - if (freeGame > 0 && pendingBytes > freeGame) throw new InstallException("Not enough free space: need " + fmt(pendingBytes) + ", only " + fmt(freeGame) + " free"); + Map<String, ChunkInfo> chunksByGuid = new LinkedHashMap<>(); + for (ChunkInfo c : needed) chunksByGuid.put(c.guidStr(), c); + if (!chunkCacheDir.isDirectory() && !chunkCacheDir.mkdirs()) throw new InstallException("Could not create Epic chunk cache"); + long pendingBytes = 0; + Map<String, Long> oldFileBytes = new LinkedHashMap<>(); + for (FileInfo f : pending) { + pendingBytes += f.fileSize(); + File output = new File(installDir, f.filename.replace("\\", "/")); + if (output.isFile()) oldFileBytes.put(f.filename, output.length()); } - cb.onLog("epic: cache " + (cachePath.isEmpty() ? "beside the game" : "scratch") + ", " + fmt(cacheMissing) + " to fetch for " + fmt(pendingBytes) + " of files"); - long totalBytes = 0; - for (ChunkInfo c : needed) totalBytes += Math.max(c.fileSize, 1); - final long fTotalBytes = totalBytes; - final int totalChunks = needed.size(); - final AtomicLong completedBytes = new AtomicLong(0); - final AtomicInteger completedCount = new AtomicInteger(0); - final AtomicInteger failCount = new AtomicInteger(0); - final AtomicLong lastSpeedMs = new AtomicLong(System.currentTimeMillis()); - final AtomicLong lastSpeedBytes = new AtomicLong(0); - final AtomicLong speedBps = new AtomicLong(0); - cb.onSizes(fTotalBytes, planned); - cb.onBytes(0, fTotalBytes, 0); - cb.onLog("epic: " + fmt(fTotalBytes) + " in " + totalChunks + " chunks"); - - boolean javaPool = true; - final int[] pendingIdx = pendingIndices(manifest, pending); - boolean engine = com.droiddeck.launcher.stores.StoresNative.INSTANCE.getAvailable() && pendingIdx != null; - if (engine) { - NativeOutcome r = runNativePool(ctx, manifestBytes, pendingIdx, needed, fTotalBytes, installDirPath, cachePath, cdnUrls, cancel, cb, - completedBytes, completedCount, lastSpeedMs, lastSpeedBytes, speedBps); - if (r.started) { - javaPool = false; - if (r.cancelled) return null; - if (!r.success) { cb.onLog("epic: engine failed: " + r.error); failCount.incrementAndGet(); } - } else cb.onLog("epic: engine not started (" + r.error + "); using the built-in pool"); + NativeOptions options = nativeOptions != null ? nativeOptions : NativeOptions.defaults(); + String mode = options.mode; + if (!MODE_LOOKAHEAD.equals(mode) && !MODE_BATCH.equals(mode) && !MODE_JAVA.equals(mode)) mode = MODE_ADAPTIVE; + boolean nativeAvailable = StoresNative.INSTANCE.ensureLoaded(); + if (!nativeAvailable && !MODE_JAVA.equals(mode)) { + cb.onLog("epic: native engine unavailable; mode=java-bounded"); + mode = MODE_JAVA; + } else if (MODE_JAVA.equals(mode)) { + cb.onLog("epic: mode=java-bounded chosen by storesEpicMode"); } - if (javaPool) { - cb.onLog("epic: engine=built-in (8 threads)"); - ExecutorService pool = Executors.newFixedThreadPool(8, com.droiddeck.launcher.stores.download.DownloadQueue.workerFactory("epic-dl")); - for (ChunkInfo chunk : needed) { - final ChunkInfo fc = chunk; - pool.submit(() -> { - if (cancel.get()) return; - File cached = new File(chunkCacheDir, fc.guidStr()); - if (!cached.exists() && !downloadChunkStreaming(fc, manifest.chunkDir, cdnUrls, cached)) { - cb.onLog("FAIL chunk=" + fc.guidStr()); - failCount.incrementAndGet(); - return; - } - long done = completedBytes.addAndGet(Math.max(fc.fileSize, 1)); - int cnt = completedCount.incrementAndGet(); - sampleSpeed(done, lastSpeedMs, lastSpeedBytes, speedBps); - cb.onProgress("Downloading chunks (" + cnt + "/" + totalChunks + ")" + speedSuffix(speedBps.get()), (int) (done * 80L / Math.max(1, fTotalBytes))); - cb.onBytes(done, fTotalBytes, speedBps.get()); - }); - } - pool.shutdown(); - try { - while (!pool.awaitTermination(250, TimeUnit.MILLISECONDS)) { - if (cancel.get()) { pool.shutdownNow(); pool.awaitTermination(5, TimeUnit.SECONDS); return null; } - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - pool.shutdownNow(); - throw new InstallException("interrupted"); - } + if (MODE_ADAPTIVE.equals(mode)) { + NativeAttempt attempt = installAdaptive(manifest, pending, pendingBytes, oldFileBytes, planned, installDir, cachePath, chunkCacheDir, stagingDir, options, cancel, cb); + if (attempt.handled) return attempt.result; + // Its space plan did not fit: the lookahead engine sizes its window to what is free. + mode = MODE_LOOKAHEAD; + } else if (MODE_BATCH.equals(mode)) { + return installBatch(manifest, pending, needed, pendingBytes, planned, installDir, cachePath, chunkCacheDir, options, cancel, cb); + } + boolean lookahead = MODE_LOOKAHEAD.equals(mode); + Map<String, Long> cachedChunkBytes = prepareBoundedCache(chunkCacheDir, needed); + boolean cacheOnInstallVolume = cachePath.isEmpty(); + long peakGameAdditional = lookahead ? 0 : cacheOnInstallVolume + ? estimatePeakAdditionalBytes(pending, chunksByGuid, cachedChunkBytes, true, oldFileBytes) + : estimateStagedFilePeak(pending, oldFileBytes); + long peakCacheAdditional = lookahead || cacheOnInstallVolume + ? 0 : estimatePeakAdditionalBytes(pending, chunksByGuid, cachedChunkBytes, false); + long freeGame = StoreInstallRoot.availableBytes(installDir); + // The lookahead engine plans its own window against both free-space readings. + if (!lookahead && cacheOnInstallVolume) { + if (StoreInstallRoot.insufficientSpace(peakGameAdditional, freeGame)) throw new InstallException("Not enough free space for safe staged Epic replacement: need up to " + fmt(peakGameAdditional) + " additional, only " + fmt(freeGame) + " free"); + } else if (!lookahead) { + long freeCache = StoreInstallRoot.availableBytes(chunkCacheDir); + if (StoreInstallRoot.insufficientSpace(peakCacheAdditional, freeCache)) throw new InstallException("Not enough internal space for the bounded Epic download cache: need up to " + fmt(peakCacheAdditional) + " additional, only " + fmt(freeCache) + " free"); + if (StoreInstallRoot.insufficientSpace(peakGameAdditional, freeGame)) throw new InstallException("Not enough free space for safe staged Epic replacement: need " + fmt(peakGameAdditional) + ", only " + fmt(freeGame) + " free"); } 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) { - if (r.cancelled) return null; - if (!r.success) throw new InstallException(r.error.isEmpty() ? "assembly failed" : r.error); - cb.onProgress("Complete", 100); - return new Result(manifest.launchExe, manifest.buildVersion, planned).withPrereq(manifest); - } - cb.onLog("epic: assembler not started (" + r.error + "); writing the files here"); - } - int totalFiles = pending.size(), doneFiles = 0; - long writtenBytes = 0, writeTotal = 0; - for (FileInfo f : pending) writeTotal += f.fileSize(); - for (FileInfo file : pending) { - if (cancel.get()) return null; - String relPath = file.filename.replace("\\", "/"); - File outFile = new File(installDir, relPath); - File parent = outFile.getParentFile(); - if (parent != null) parent.mkdirs(); - cb.onProgress("Writing: " + (relPath.contains("/") ? relPath.substring(relPath.lastIndexOf('/') + 1) : relPath), 80 + (int) (doneFiles * 20L / totalFiles)); - try (FileOutputStream fos = new FileOutputStream(outFile); BufferedOutputStream bos = new BufferedOutputStream(fos, 65536)) { - for (ChunkPart part : file.parts) { - File cachedChunk = new File(chunkCacheDir, part.guidStr()); - if (!cachedChunk.exists()) throw new InstallException("Missing chunk " + part.guidStr() + " for " + relPath); - byte[] data = readFile(cachedChunk); - bos.write(data, part.offset, part.size); + if (lookahead) { + JSONObject request = nativeRequest(manifest, pending, chunksByGuid, cachedChunkBytes, + oldFileBytes, installDir, chunkCacheDir, stagingDir, cacheOnInstallVolume); + request.put("free_game", StoreInstallRoot.availableBytes(installDir)); + request.put("free_cache", StoreInstallRoot.availableBytes(chunkCacheDir)); + final long nativeTotal = pendingBytes; + final long nativeStarted = System.nanoTime(); + cb.onSizes(nativeTotal, planned); + cb.onBytes(0, nativeTotal, 0); + EpicNative.Result outcome = EpicNative.installAdaptive(request.toString(), cancel, new EpicNative.InstallListener() { + @Override public void onPlan(int chunks, long bytes, String plan) { + cb.onLog("epic: mode=lookahead engine=lookahead-rust " + plan); } + @Override public void onProgress(long done, long total, int files, int filesTotal) { + long elapsedMs = Math.max(1, (System.nanoTime() - nativeStarted) / 1_000_000); + cb.onBytes(done, total, done * 1000 / elapsedMs); + cb.onProgress("Downloading chunks…", (int) (done * 80L / Math.max(1, total))); + cb.onStage("stream", done, total, files, filesTotal); + } + @Override public void onLog(String line) { cb.onLog(line); } + @Override public void onComplete(boolean success, String error, long bytes) {} + @Override public boolean beforeSwap() { + try { + if (cancel.get()) return false; + StoreInstalls.markUpdating(installDir); + return !cancel.get(); + } catch (Exception e) { + cb.onLog("epic: cannot mark updating: " + e.getMessage()); + return false; + } + } + }); + if (outcome.cancelled || cancel.get()) return null; + if (!outcome.success) throw new InstallException("Native Epic install failed: " + outcome.error); + long lookaheadMs = Math.max(1, (System.nanoTime() - nativeStarted) / 1_000_000); + cb.onLog("epic: summary mode=lookahead files=" + pending.size() + " bytes_written=" + nativeTotal + " elapsed_ms=" + lookaheadMs + + " avg_MBps=" + String.format(java.util.Locale.ROOT, "%.2f", nativeTotal * 1000.0 / lookaheadMs / 1048576.0)); + cb.onStage("install", nativeTotal, nativeTotal, pending.size(), pending.size()); + cb.onProgress("Complete", 100); + return new Result(manifest.launchExe, manifest.buildVersion, planned).withPrereq(manifest); + } + cb.onLog("epic: cache " + (cacheOnInstallVolume ? "beside the game" : "scratch") + ", peak game additional=" + fmt(peakGameAdditional) + (cacheOnInstallVolume ? "" : ", peak cache additional=" + fmt(peakCacheAdditional)) + " for " + fmt(pendingBytes) + " of files"); + final long fTotalBytes = pendingBytes; + final int totalChunks = needed.size(); + cb.onSizes(fTotalBytes, planned); + cb.onBytes(0, fTotalBytes, 0); + cb.onLog("epic: " + fmt(fTotalBytes) + " to write using " + totalChunks + " unique chunks"); + cb.onLog("epic: engine=bounded-java (8-worker prefetch; native availability does not change cache semantics)"); + long completedBytes = 0, writtenBytes = 0; + long transferStarted = System.nanoTime(); + int completedCount = 0, doneFiles = 0, totalFiles = pending.size(); + Set<String> reservedPaths = new LinkedHashSet<>(); + for (FileInfo file : manifest.files) reservedPaths.add(new File(installDir, file.filename.replace("\\", "/")).getCanonicalPath()); + boolean updateRecorded = false; + try (ChunkPrefetch prefetch = new ChunkPrefetch(pending, chunksByGuid, cachedChunkBytes, + chunkCacheDir, manifest.chunkDir, cdnUrls, cancel)) { + for (FileInfo file : pending) { + if (cancel.get()) return null; + String relPath = file.filename.replace("\\", "/"); + File outFile = new File(installDir, relPath); + cb.onProgress("Downloading chunks: " + (relPath.contains("/") ? relPath.substring(relPath.lastIndexOf('/') + 1) : relPath), (int) (completedBytes * 80L / Math.max(1, fTotalBytes))); + MessageDigest fileSha = expectedDigest(file.sha1); + File partFile = createStagingFile(outFile, reservedPaths, stagingDir); + try { + try (FileOutputStream fos = new FileOutputStream(partFile); BufferedOutputStream bos = new BufferedOutputStream(fos, 65536)) { + for (ChunkPart part : file.parts) { + if (cancel.get()) throw new IOException("cancelled"); + String guid = part.guidStr(); + File cachedChunk = prefetch.await(guid); + completedCount++; + cb.onProgress("Downloading chunks (part " + completedCount + ")", (int) (completedBytes * 80L / Math.max(1, fTotalBytes))); + copyChunkPart(cachedChunk, part, bos, fileSha, cancel, relPath); + completedBytes += part.size & 0xFFFFFFFFL; + long elapsedMs = Math.max(1, (System.nanoTime() - transferStarted) / 1_000_000); + cb.onBytes(completedBytes, fTotalBytes, completedBytes * 1000 / elapsedMs); + prefetch.consume(); + } + bos.flush(); + fos.getFD().sync(); + } + if (cancel.get()) return null; + if (partFile.length() != file.fileSize()) throw new InstallException("Size mismatch for " + relPath); + if (fileSha != null && !MessageDigest.isEqual(fileSha.digest(), file.sha1)) { + throw new InstallException("SHA-1 mismatch for " + relPath); + } + // Once a swap can mix versions, the old installed record is no longer truthful. + if (!updateRecorded) { + StoreInstalls.markUpdating(installDir); + updateRecorded = true; + } + if (cancel.get()) return null; + Files.move(partFile.toPath(), outFile.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + if (cancel.get()) return null; + throw new InstallException("Could not assemble or atomically replace " + relPath + ": " + e.getMessage()); + } finally { + partFile.delete(); + } + doneFiles++; + writtenBytes += file.fileSize(); + cb.onStage("stream", writtenBytes, pendingBytes, doneFiles, totalFiles); } - doneFiles++; - writtenBytes += file.fileSize(); - cb.onStage("install", writtenBytes, writeTotal, doneFiles, totalFiles); } deleteDir(chunkCacheDir); + long boundedMs = Math.max(1, (System.nanoTime() - transferStarted) / 1_000_000); + cb.onLog("epic: summary mode=java-bounded files=" + doneFiles + " bytes_written=" + writtenBytes + " elapsed_ms=" + boundedMs + " avg_MBps=" + String.format(java.util.Locale.ROOT, "%.2f", writtenBytes * 1000.0 / boundedMs / 1048576.0)); + cb.onStage("install", writtenBytes, pendingBytes, doneFiles, totalFiles); cb.onProgress("Complete", 100); return new Result(manifest.launchExe, manifest.buildVersion, planned).withPrereq(manifest); } catch (InstallException e) { @@ -312,10 +398,17 @@ public final class EpicDownloadManager { } } - private static final class NativeOutcome { boolean started, success, cancelled; String error = ""; } + /** What a native attempt did: {@code handled} = the install ended there ({@code result} null when cancelled). */ + private static final class NativeAttempt { + final boolean handled; + final Result result; + private NativeAttempt(boolean handled, Result result) { this.handled = handled; this.result = result; } + static final NativeAttempt FALLBACK = new NativeAttempt(false, null); + static NativeAttempt done(Result result) { return new NativeAttempt(true, result); } + } - /** 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) { + /** The pending files as indices into the manifest's file list, what the native runs take; null when one is not in it. */ + 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); int[] pendingIdx = new int[pending.size()]; @@ -327,63 +420,698 @@ 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) { - NativeOutcome r = new NativeOutcome(); - try { - String[] prefixes = new String[cdnUrls.size()]; - for (int i = 0; i < cdnUrls.size(); i++) prefixes[i] = cdnUrls.get(i).baseUrl + cdnUrls.get(i).cloudDir; - final int totalChunks = needed.size(); - StoreDownloadTier tier = StoreDownloadTier.Companion.current(ctx); - int workers = Math.max(1, Math.min(128, tier.getNetworkWindow())); - int process = Math.max(2, tier.getProcessWorkers()); - cb.onLog("epic: engine=native workers=" + workers + " process_workers=" + process + " tier=" + tier.getId() + (cachePath.isEmpty() ? "" : " cache=scratch")); - EpicNative.Result res = EpicNative.run(manifestBytes, installDirPath, cachePath, prefixes, pendingIdx, totalChunks, totalBytes, "", workers, process, cancel, - new EpicNative.Listener() { - @Override public void onPlan(int chunksTotal, long bytesTotal, String chunkDir) { cb.onLog("epic: plan chunks=" + chunksTotal + " bytes=" + bytesTotal + " dir=" + chunkDir); } - @Override public void onProgress(long bytesDone, long bytesTotal, int chunksDone, int chunksTotal) { - completedBytes.set(bytesDone); - completedCount.set(chunksDone); - sampleSpeed(bytesDone, lastSpeedMs, lastSpeedBytes, speedBps); - cb.onProgress("Downloading chunks (" + chunksDone + "/" + totalChunks + ")" + speedSuffix(speedBps.get()), (int) (bytesDone * 80L / Math.max(1, totalBytes))); - cb.onBytes(bytesDone, totalBytes, speedBps.get()); + private static String[] cdnPrefixes(List<CdnUrl> cdnUrls) { + String[] prefixes = new String[cdnUrls.size()]; + for (int i = 0; i < cdnUrls.size(); i++) prefixes[i] = cdnUrls.get(i).baseUrl + cdnUrls.get(i).cloudDir; + return prefixes; + } + + /** + * Where the adaptive engine's shared-chunk cache goes and how big it may get, and what each + * volume must hold. A card install keeps the cache on internal scratch (each byte hits the + * card once); when internal storage cannot hold a useful cache, the cache goes beside the game + * instead of failing. {@code error} names the storage that is short, and by how much. + */ + static final class AdaptiveSpacePlan { + boolean cacheBesideGame; + long cacheBudget; + /** + * Most planned bytes of replaced files the engine may stage at once (each sits beside its + * old copy until its swap); -1 = nothing is replaced, no limit. + */ + long overlapBudget = -1; + long gameNeed; + long scratchNeed; + String error; + } + + /** The part of free space the shared cache may use, after {@code need}: half the headroom, capped. */ + static long adaptiveCacheBudget(long free, long need) { + if (free < 0) return ADAPTIVE_CACHE_DEFAULT_BYTES; + long headroom = free - need - ADAPTIVE_TRANSIENT_BYTES; + if (headroom <= 0) return 0; + return Math.min(ADAPTIVE_CACHE_MAX_BYTES, headroom / 2); + } + + /** + * The game volume's net growth at its worst point when files are swapped in order and each + * replaced file's old copy is released at its swap: the running Σ(new − old) after each file. + * What a staged file costs while it sits beside its old copy is the overlap budget's part. + */ + static long settledNetPeak(List<FileInfo> files, Map<String, Long> oldFileBytes) { + long running = 0, peak = 0; + for (FileInfo file : files) { + Long old = oldFileBytes.get(file.filename); + running += file.fileSize() - (old == null ? 0 : old); + peak = Math.max(peak, running); + } + return peak; + } + + /** The largest pending file whose destination has data (the engine's smallest overlap budget). */ + static long largestReplacedFile(List<FileInfo> files, Map<String, Long> oldFileBytes) { + long largest = 0; + for (FileInfo file : files) { + Long old = oldFileBytes.get(file.filename); + if (old != null && old > 0) largest = Math.max(largest, file.fileSize()); + } + return largest; + } + + /** + * The cache placement and budget as before; for an update, the replacement-overlap budget is + * what the game volume has left after the net growth and the cache, never below the largest + * replaced file (one at a time is the minimum that still progresses). The check is the + * lookahead planner's: the need must not exceed the free reading, no extra margin. + */ + static AdaptiveSpacePlan planAdaptiveSpace(long netPeak, long largestReplaced, long freeGame, boolean scratchRequested, long freeScratch) { + AdaptiveSpacePlan plan = new AdaptiveSpacePlan(); + long minimum = netPeak + largestReplaced; + long scratchBudget = scratchRequested ? adaptiveCacheBudget(freeScratch, 0) : 0; + plan.cacheBesideGame = !scratchRequested || (freeScratch >= 0 && scratchBudget < ADAPTIVE_CACHE_MIN_SCRATCH_BYTES); + long cacheOnGame; + if (plan.cacheBesideGame) { + plan.cacheBudget = adaptiveCacheBudget(freeGame, minimum); + cacheOnGame = plan.cacheBudget + ADAPTIVE_TRANSIENT_BYTES; + plan.scratchNeed = 0; + } else { + plan.cacheBudget = scratchBudget; + cacheOnGame = 0; + plan.scratchNeed = plan.cacheBudget + ADAPTIVE_TRANSIENT_BYTES; + } + long overlap = 0; + if (largestReplaced > 0) { + overlap = freeGame < 0 ? largestReplaced : Math.max(largestReplaced, freeGame - netPeak - cacheOnGame); + plan.overlapBudget = overlap; + } + plan.gameNeed = netPeak + overlap + cacheOnGame; + if (StoreInstallRoot.insufficientSpace(plan.gameNeed, freeGame)) { + plan.error = "Not enough free space on the game's storage: need " + fmt(plan.gameNeed) + + (plan.cacheBesideGame && scratchRequested ? " (download cache moved beside the game: internal storage is full)" : "") + + ", only " + fmt(freeGame) + " free (short by " + fmt(plan.gameNeed - freeGame) + ")"; + } else if (!plan.cacheBesideGame && StoreInstallRoot.insufficientSpace(plan.scratchNeed, freeScratch)) { + plan.error = "Not enough internal space for the download cache: need " + fmt(plan.scratchNeed) + ", only " + fmt(freeScratch) + + " free (short by " + fmt(plan.scratchNeed - freeScratch) + ")"; + } + return plan; + } + + /** + * The adaptive native install; the library is loaded. Hands back {@code FALLBACK} (nothing + * fetched or written) only when its space plan does not fit, so the caller can run the + * lookahead engine, which narrows its window to the free space. Any other failure is final. + */ + private static NativeAttempt installAdaptive(Manifest manifest, List<FileInfo> pending, long pendingBytes, Map<String, Long> oldFileBytes, + long planned, File installDir, String cachePath, File chunkCacheDir, File stagingDir, + NativeOptions options, AtomicBoolean cancel, Callback cb) throws Exception { + int[] pendingIdx = pendingIndices(manifest, pending); + if (manifest.rawBytes == null || pendingIdx == null) { + throw new InstallException("Native Epic install failed: the manifest is not available to the native engine"); + } + long netPeak = settledNetPeak(pending, oldFileBytes); + long largestReplaced = largestReplacedFile(pending, oldFileBytes); + long freeGame = StoreInstallRoot.availableBytes(installDir); + boolean scratchRequested = !cachePath.isEmpty(); + long freeScratch = scratchRequested ? StoreInstallRoot.availableBytes(chunkCacheDir) : -1; + AdaptiveSpacePlan space = planAdaptiveSpace(netPeak, largestReplaced, freeGame, scratchRequested, freeScratch); + if (space.error != null) { + cb.onLog("epic: adaptive space plan does not fit (" + space.error + "); mode=lookahead"); + return NativeAttempt.FALLBACK; + } + String engineCache = space.cacheBesideGame ? "" : cachePath; + File engineCacheDir = engineCache.isEmpty() ? new File(installDir, ".chunks") : chunkCacheDir; + if (space.cacheBesideGame && scratchRequested) { + cb.onLog("epic: internal storage cannot hold the shared-chunk cache (" + fmt(freeScratch) + " free); cache beside the game"); + } + if (cancel.get()) return NativeAttempt.done(null); + cb.onLog("epic: mode=adaptive tier=" + options.tierId + " workers=" + options.maxWorkers + " process_workers=" + options.processWorkers + + " cache=" + (engineCache.isEmpty() ? "beside the game" : "scratch") + " budget=" + fmt(space.cacheBudget) + + " overlap_budget=" + (space.overlapBudget < 0 ? "none" : fmt(space.overlapBudget)) + + " game need=" + fmt(space.gameNeed) + " of " + fmt(freeGame) + (space.cacheBesideGame ? "" : ", scratch need=" + fmt(space.scratchNeed) + " of " + fmt(freeScratch))); + cb.onSizes(pendingBytes, planned); + cb.onBytes(0, pendingBytes, 0); + final int totalFiles = pending.size(); + final File fInstallDir = installDir; + final Object lock = new Object(); + final long started = System.nanoTime(); + final long[] state = new long[] {0, 0, System.currentTimeMillis(), 0, 0}; // bytes, files, speed ms, speed bytes, speed bps + EpicNative.Result res = EpicNative.stream(manifest.rawBytes, installDir.getPath(), engineCache, stagingDir.getCanonicalPath(), cdnPrefixes(manifest.cdnUrls), pendingIdx, pendingBytes, + space.cacheBudget, space.overlapBudget, "", options.maxWorkers, options.processWorkers, cancel, new EpicNative.StreamListener() { + @Override public void onStreamProgress(long bytesWritten, long bytesTotal, int filesDone, int filesTotal, long filesBytes) { + synchronized (lock) { + if (bytesWritten < state[0] && filesDone <= state[1]) return; // a late, older sample + state[0] = Math.max(state[0], bytesWritten); + long nowMs = System.currentTimeMillis(); + if (nowMs - state[2] >= 500) { + state[4] = (state[0] - state[3]) * 1000L / Math.max(1, nowMs - state[2]); + state[2] = nowMs; + state[3] = state[0]; + } + cb.onBytes(state[0], pendingBytes, state[4]); + cb.onProgress("Downloading chunks (" + filesDone + "/" + totalFiles + " files)" + speedSuffix(state[4]), + (int) (state[0] * 80L / Math.max(1, pendingBytes))); + if (filesDone > state[1]) { + state[1] = filesDone; + cb.onStage("stream", filesBytes, pendingBytes, filesDone, totalFiles); + } } - @Override public void onLog(String line) { cb.onLog(line); } - @Override public void onComplete(boolean success, String error, long bytesCredited) {} - }); - r.started = res.started; r.success = res.success; r.cancelled = res.cancelled; r.error = res.error == null ? "" : res.error; - } catch (Throwable t) { - r.started = false; - r.error = t.getClass().getSimpleName() + ": " + t.getMessage(); - } - return r; + } + @Override public void onLog(String line) { cb.onLog(line); } + @Override public boolean onBeforeReplace() { + // Once a swap can mix versions, the old installed record is no longer truthful. + try { + StoreInstalls.markUpdating(fInstallDir); + return true; + } catch (Exception e) { + cb.onLog("epic: could not mark the install as updating: " + e.getMessage()); + return false; + } + } + @Override public void onComplete(boolean success, String error, long bytes) {} + }); + if (!res.started) throw new InstallException("Native Epic install failed: " + res.error); + long elapsedMs = Math.max(1, (System.nanoTime() - started) / 1_000_000); + if (res.cancelled) return NativeAttempt.done(null); + if (!res.success) throw new InstallException("Native Epic install failed: " + res.error); + deleteDir(engineCacheDir); + deleteDir(chunkCacheDir); + cb.onLog("epic: summary mode=adaptive files=" + totalFiles + " bytes_written=" + pendingBytes + " elapsed_ms=" + elapsedMs + + " avg_MBps=" + String.format(java.util.Locale.ROOT, "%.2f", pendingBytes * 1000.0 / elapsedMs / 1048576.0)); + cb.onStage("install", pendingBytes, pendingBytes, totalFiles, totalFiles); + cb.onProgress("Complete", 100); + return NativeAttempt.done(new Result(manifest.launchExe, manifest.buildVersion, planned).withPrereq(manifest)); } - /** 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(); + /** + * The older native batch install, kept for A/B benchmarks: every chunk fetched into the cache + * in parallel, then the files assembled from it. It needs the whole cache and the files at + * once (about twice the game), and it writes files in place rather than staged, so an + * existing install is marked updating before anything is fetched. + */ + private static Result installBatch(Manifest manifest, List<FileInfo> pending, List<ChunkInfo> needed, long pendingBytes, + long planned, File installDir, String cachePath, File chunkCacheDir, NativeOptions options, + AtomicBoolean cancel, Callback cb) throws InstallException { + int[] pendingIdx = pendingIndices(manifest, pending); + if (manifest.rawBytes == null || pendingIdx == null) { + throw new InstallException("Native Epic install failed: the manifest is not available to the native engine"); + } + long cacheMissing = 0, totalBytes = 0; + for (ChunkInfo c : needed) { + if (!new File(chunkCacheDir, c.guidStr()).isFile()) cacheMissing += chunkWindowSize(c); + totalBytes += Math.max(c.fileSize, 1); + } + long freeGame = StoreInstallRoot.availableBytes(installDir); + if (cachePath.isEmpty()) { + long need = cacheMissing + pendingBytes; + if (StoreInstallRoot.insufficientSpace(need, freeGame)) throw new InstallException("Not enough free space for the batch Epic download: need " + fmt(need) + " (" + fmt(pendingBytes) + " of game files + " + fmt(cacheMissing) + " of download cache), only " + fmt(freeGame) + " free (short by " + fmt(need - freeGame) + ")"); + } else { + long freeCache = StoreInstallRoot.availableBytes(chunkCacheDir); + if (StoreInstallRoot.insufficientSpace(cacheMissing, freeCache)) throw new InstallException("Not enough internal space for the batch download cache: need " + fmt(cacheMissing) + ", only " + fmt(freeCache) + " free (short by " + fmt(cacheMissing - freeCache) + ")"); + if (StoreInstallRoot.insufficientSpace(pendingBytes, freeGame)) throw new InstallException("Not enough free space on the game's storage: need " + fmt(pendingBytes) + ", only " + fmt(freeGame) + " free (short by " + fmt(pendingBytes - freeGame) + ")"); + } + if (cancel.get()) return null; + final long fTotal = totalBytes; + final int totalChunks = needed.size(); + final int totalFiles = pending.size(); + cb.onLog("epic: mode=batch tier=" + options.tierId + " workers=" + options.maxWorkers + " process_workers=" + options.processWorkers + (cachePath.isEmpty() ? "" : " cache=scratch") + " cache_missing=" + fmt(cacheMissing)); + cb.onSizes(pendingBytes, planned); + final AtomicLong lastMs = new AtomicLong(System.currentTimeMillis()), lastBytes = new AtomicLong(), speed = new AtomicLong(); + final long started = System.nanoTime(); + EpicNative.Result fetch = EpicNative.run(manifest.rawBytes, installDir.getPath(), cachePath, cdnPrefixes(manifest.cdnUrls), pendingIdx, totalChunks, fTotal, "", + options.maxWorkers, options.processWorkers, cancel, new EpicNative.Listener() { + @Override public void onPlan(int chunksTotal, long bytesTotal, String chunkDir) { cb.onLog("epic: plan chunks=" + chunksTotal + " bytes=" + bytesTotal + " dir=" + chunkDir); } + @Override public void onProgress(long bytesDone, long bytesTotal, int chunksDone, int chunksTotal) { + sampleSpeed(bytesDone, lastMs, lastBytes, speed); + cb.onProgress("Downloading chunks (" + chunksDone + "/" + totalChunks + ")" + speedSuffix(speed.get()), (int) (bytesDone * 80L / Math.max(1, fTotal))); + cb.onBytes(bytesDone, fTotal, speed.get()); + } + @Override public void onLog(String line) { cb.onLog(line); } + @Override public void onComplete(boolean success, String error, long bytes) {} + }); + if (!fetch.started) throw new InstallException("Native Epic install failed: " + fetch.error); + if (fetch.cancelled) return null; + if (!fetch.success) throw new InstallException("Epic batch download failed: " + fetch.error); + // Assembly overwrites files in place: the old installed record stops being truthful now. + try { StoreInstalls.markUpdating(installDir); } catch (Exception e) { throw new InstallException("Could not mark the install as updating: " + e.getMessage()); } + EpicNative.Result asm = EpicNative.assemble(manifest.rawBytes, installDir.getPath(), cachePath, pendingIdx, cancel, new EpicNative.Listener() { + @Override public void onPlan(int chunksTotal, long bytesTotal, String chunkDir) {} + @Override public void onProgress(long bytesDone, long bytesTotal, int done, int total) { + cb.onStage("stream", bytesDone, bytesTotal, done, Math.max(total, totalFiles)); + cb.onProgress("Writing files (" + done + "/" + Math.max(total, totalFiles) + ")", 80 + (int) (done * 20L / Math.max(1, Math.max(total, totalFiles)))); + } + @Override public void onLog(String line) { cb.onLog(line); } + @Override public void onComplete(boolean success, String error, long bytes) {} + }); + if (asm.cancelled) return null; + if (!asm.started || !asm.success) throw new InstallException(asm.error == null || asm.error.isEmpty() ? "Epic batch assembly failed" : asm.error); + long elapsedMs = Math.max(1, (System.nanoTime() - started) / 1_000_000); + cb.onLog("epic: summary mode=batch files=" + totalFiles + " bytes_written=" + pendingBytes + " elapsed_ms=" + elapsedMs + + " avg_MBps=" + String.format(java.util.Locale.ROOT, "%.2f", pendingBytes * 1000.0 / elapsedMs / 1048576.0)); + deleteDir(chunkCacheDir); + cb.onStage("install", pendingBytes, pendingBytes, totalFiles, totalFiles); + cb.onProgress("Complete", 100); + return new Result(manifest.launchExe, manifest.buildVersion, planned).withPrereq(manifest); + } + + private static void sampleSpeed(long done, AtomicLong lastSpeedMs, AtomicLong lastSpeedBytes, AtomicLong speedBps) { + long nowMs = System.currentTimeMillis(), prevMs = lastSpeedMs.get(); + if (nowMs - prevMs >= 500 && lastSpeedMs.compareAndSet(prevMs, nowMs)) { + long prevB = lastSpeedBytes.getAndSet(done); + long dt = nowMs - prevMs; + if (dt > 0) speedBps.set((done - prevB) * 1000L / dt); + } + } + + private static String speedSuffix(long bps) { + if (bps <= 0) return ""; + return " " + (bps >= 1048576 ? String.format(java.util.Locale.ROOT, "%.1f MB/s", bps / 1048576.0) : (bps / 1024) + " KB/s"); + } + + static File recoverOwnedStaging(File installDir, List<FileInfo> manifestFiles) throws IOException { + File staging = new File(installDir, STAGING_DIRECTORY); + Path expected = installDir.getCanonicalFile().toPath().resolve(STAGING_DIRECTORY); + if (!staging.getCanonicalFile().toPath().equals(expected) || + (Files.exists(staging.toPath(), LinkOption.NOFOLLOW_LINKS) && + !Files.isDirectory(staging.toPath(), LinkOption.NOFOLLOW_LINKS))) { + throw new IOException("Unsafe Epic staging namespace; refusing to traverse it"); + } + String namespace = expected.toString(); + for (FileInfo file : manifestFiles) { + String path = new File(installDir, file.filename.replace("\\", "/")).getCanonicalPath(); + if (path.equals(namespace) || path.startsWith(namespace + File.separator)) { + throw new IOException("Manifest conflicts with Epic staging namespace"); + } + } + File owner = new File(staging, "owner"); + if (Files.exists(staging.toPath(), LinkOption.NOFOLLOW_LINKS)) { + if (!Files.isRegularFile(owner.toPath(), LinkOption.NOFOLLOW_LINKS) || + !owner.getCanonicalFile().toPath().equals(expected.resolve("owner")) || + owner.length() != STAGING_OWNER.length() || + !STAGING_OWNER.equals(new String(Files.readAllBytes(owner.toPath()), StandardCharsets.UTF_8))) { + throw new IOException("Unowned Epic staging namespace; refusing to delete it"); + } + File[] files = staging.listFiles(); + if (files == null) throw new IOException("Could not inspect Epic staging"); + for (File file : files) { + if (file.getName().equals("owner")) continue; + if (!Files.isRegularFile(file.toPath(), LinkOption.NOFOLLOW_LINKS) || + !file.getCanonicalFile().toPath().equals(expected.resolve(file.getName())) || + !file.getName().endsWith(".stage")) throw new IOException("Unexpected Epic staging entry"); + } + for (File file : files) { + if (file.getName().equals("owner")) continue; + if (!file.delete()) throw new IOException("Could not recover orphan Epic staging"); + } + } else { + if (!staging.mkdir()) throw new IOException("Could not create Epic staging namespace"); + try (FileOutputStream out = new FileOutputStream(owner)) { + out.write(STAGING_OWNER.getBytes(StandardCharsets.UTF_8)); + out.getFD().sync(); + } + } + return staging; + } + + /** + * Remove the owned staging namespace when it holds nothing but its own marker. Same no-follow + * and canonical-path checks as {@link #recoverOwnedStaging}; anything unexpected is left alone. + */ + static void releaseOwnedStaging(File installDir) { try { - cb.onLog("epic: assembler=native files=" + totalFiles + (cachePath.isEmpty() ? "" : " cache=scratch")); - EpicNative.Result res = EpicNative.assemble(manifestBytes, installDirPath, cachePath, pendingIdx, cancel, new EpicNative.Listener() { - @Override public void onPlan(int chunksTotal, long bytesTotal, String chunkDir) {} - @Override public void onProgress(long bytesDone, long bytesTotal, int done, int total) { - cb.onStage("install", bytesDone, bytesTotal, done, Math.max(total, totalFiles)); - cb.onProgress("Writing files (" + done + "/" + Math.max(total, totalFiles) + ")", 80 + (int) (done * 20L / Math.max(1, Math.max(total, totalFiles)))); + File staging = new File(installDir, STAGING_DIRECTORY); + if (!Files.isDirectory(staging.toPath(), LinkOption.NOFOLLOW_LINKS)) return; + Path expected = installDir.getCanonicalFile().toPath().resolve(STAGING_DIRECTORY); + if (!staging.getCanonicalFile().toPath().equals(expected)) return; + String[] names = staging.list(); + if (names == null || names.length != 1 || !names[0].equals("owner")) return; + File owner = new File(staging, "owner"); + if (!Files.isRegularFile(owner.toPath(), LinkOption.NOFOLLOW_LINKS) + || owner.length() != STAGING_OWNER.length() + || !STAGING_OWNER.equals(new String(Files.readAllBytes(owner.toPath()), StandardCharsets.UTF_8))) return; + if (owner.delete()) staging.delete(); + } catch (IOException e) { + Log.w(TAG, "staging namespace kept: " + e.getMessage()); + } + } + + private static JSONObject nativeRequest(Manifest manifest, List<FileInfo> files, + Map<String, ChunkInfo> chunks, Map<String, Long> cached, Map<String, Long> old, + File install, File cache, File staging, boolean sameVolume) throws Exception { + JSONObject request = new JSONObject(); + request.put("schema_version", 1); + request.put("install_dir", install.getCanonicalPath()); + request.put("cache_dir", cache.getCanonicalPath()); + request.put("staging_dir", staging.getCanonicalPath()); + request.put("same_volume", sameVolume); + request.put("chunk_dir", manifest.chunkDir); + request.put("max_workers", 32); + JSONArray hosts = new JSONArray(); + for (CdnUrl cdn : manifest.cdnUrls) hosts.put(cdn.baseUrl + cdn.cloudDir); + request.put("hosts", hosts); + JSONArray chunkList = new JSONArray(); + for (ChunkInfo chunk : chunks.values()) { + JSONObject item = new JSONObject(); + item.put("guid", chunk.guidStr()); + item.put("path", chunk.getPath(manifest.chunkDir)); + item.put("size", chunkWindowSize(chunk)); + item.put("exact_size", chunk.windowSize > 0); + item.put("wire_size", Math.max(1, chunk.fileSize)); + item.put("sha1", hexDigest(chunk.sha1)); + item.put("cached", cached.containsKey(chunk.guidStr()) ? cached.get(chunk.guidStr()) : -1); + chunkList.put(item); + } + request.put("chunks", chunkList); + JSONArray fileList = new JSONArray(); + for (FileInfo file : files) { + JSONObject item = new JSONObject(); + item.put("path", file.filename.replace("\\", "/")); + item.put("sha1", hexDigest(file.sha1)); + item.put("old_size", old.containsKey(file.filename) ? old.get(file.filename) : 0); + JSONArray parts = new JSONArray(); + for (ChunkPart part : file.parts) { + parts.put(new JSONObject().put("guid", part.guidStr()).put("offset", part.offset & 0xFFFFFFFFL).put("size", part.size & 0xFFFFFFFFL)); + } + item.put("parts", parts); + fileList.put(item); + } + request.put("files", fileList); + return request; + } + + private static String hexDigest(byte[] digest) { + if (digest == null || digest.length != 20) return ""; + StringBuilder out = new StringBuilder(); + for (byte b : digest) out.append(String.format("%02x", b & 255)); + return out.toString(); + } + + private static File createStagingFile(File output, Set<String> reservedPaths, File staging) throws IOException { + File parent = output.getParentFile(); + if (!parent.isDirectory() && !parent.mkdirs()) throw new IOException("Could not create output directory"); + // Owned staging is on the output volume, never a manifest's literal "<name>.part". + while (true) { + File stage = File.createTempFile("file-", ".stage", staging); + if (!reservedPaths.contains(stage.getCanonicalPath())) return stage; + if (!stage.delete()) throw new IOException("Could not clear staging collision"); + } + } + + static long estimateStagedFilePeak(List<FileInfo> files, Map<String, Long> oldFileBytes) { + long delta = 0, peak = 0; + for (FileInfo file : files) { + delta += file.fileSize(); + peak = Math.max(peak, delta); + delta -= oldFileBytes.containsKey(file.filename) ? oldFileBytes.get(file.filename) : 0; + } + return peak; + } + + /** + * Discard abandoned/orphaned cache entries and retain only verified chunks this install uses. + * A bounded run consumes each chunk at its last part reference, so stale chunks from an older + * all-at-once run must not silently occupy the space needed by the game files. + */ + static Map<String, Long> prepareBoundedCache(File cacheDir, List<ChunkInfo> needed) throws InstallException { + Map<String, ChunkInfo> byGuid = new LinkedHashMap<>(); + for (ChunkInfo chunk : needed) byGuid.put(chunk.guidStr(), chunk); + Map<String, Long> valid = new LinkedHashMap<>(); + File[] entries = cacheDir.listFiles(); + if (entries == null) throw new InstallException("Could not inspect Epic chunk cache"); + for (File entry : entries) { + ChunkInfo chunk = byGuid.get(entry.getName()); + if (chunk == null || !isValidCachedChunk(chunk, entry)) { + deleteDir(entry); + if (entry.exists()) throw new InstallException("Could not clear stale Epic cache entry " + entry.getName()); + continue; + } + valid.put(entry.getName(), entry.length()); + } + trimCacheToBound(cacheDir, valid); + return valid; + } + + /** Evict oldest reusable chunks until retained cache data is under the fixed disk ceiling. */ + private static void trimCacheToBound(File cacheDir, Map<String, Long> cacheSizes) throws InstallException { + for (String guid : evictOldestReusableChunks(cacheSizes).keySet()) { + File chunk = new File(cacheDir, guid); + if (chunk.exists() && !chunk.delete() && chunk.exists()) { + throw new InstallException("Could not release Epic cache chunk " + guid); + } + } + } + + /** + * The writer and space planner use the same oldest-first retained-cache policy. Entries leave + * this map as soon as their last reference is consumed; an oversized incoming chunk is + * therefore allowed to coexist with the retained cache until its current part is written. + */ + private static Map<String, Long> evictOldestReusableChunks(Map<String, Long> cacheSizes) { + long retained = 0; + for (long bytes : cacheSizes.values()) retained += bytes; + Map<String, Long> evicted = new LinkedHashMap<>(); + java.util.Iterator<Map.Entry<String, Long>> iterator = cacheSizes.entrySet().iterator(); + while (retained > MAX_RETAINED_CHUNK_CACHE_BYTES && iterator.hasNext()) { + Map.Entry<String, Long> entry = iterator.next(); + retained -= entry.getValue(); + evicted.put(entry.getKey(), entry.getValue()); + iterator.remove(); + } + return evicted; + } + + private static boolean consumeChunkReference(Map<String, Integer> refs, String guid) { + Integer count = refs.get(guid); + if (count == null) return false; + if (count <= 1) { + refs.remove(guid); + return true; + } + refs.put(guid, count - 1); + return false; + } + + /** + * Pure disk schedule shared by execution and preflight. The next eight part references pin + * at most eight GUIDs (including across file boundaries); other reusable data is oldest-first + * capped at 64 MiB. Incoming downloads reserve their entire decoded size before submission. + */ + static final class PrefetchSchedule { + final List<String> parts = new ArrayList<>(); + final Map<String, Long> resident; + final Map<String, Integer> refs; + final Map<String, ChunkInfo> chunks; + int cursor; + + PrefetchSchedule(List<FileInfo> files, Map<String, ChunkInfo> chunks, Map<String, Long> cached) { + for (FileInfo file : files) for (ChunkPart part : file.parts) parts.add(part.guidStr()); + this.chunks = chunks; + resident = new LinkedHashMap<>(cached); + refs = chunkReferences(files); + } + + Set<String> window() { + return new LinkedHashSet<>(parts.subList(cursor, Math.min(parts.size(), cursor + PREFETCH_PARTS))); + } + + List<String> fill() { + List<String> fetch = new ArrayList<>(); + for (String guid : window()) if (!resident.containsKey(guid)) { + ChunkInfo chunk = chunks.get(guid); + if (chunk == null) throw new IllegalArgumentException("Missing chunk metadata " + guid); + resident.put(guid, chunkWindowSize(chunk)); + fetch.add(guid); + } + return fetch; + } + + List<String> consume() { + List<String> release = new ArrayList<>(); + String guid = parts.get(cursor++); + if (consumeChunkReference(refs, guid)) { + resident.remove(guid); + release.add(guid); + } + Set<String> pinned = window(); + long reusable = 0; + for (Map.Entry<String, Long> entry : resident.entrySet()) { + if (!pinned.contains(entry.getKey())) reusable += entry.getValue(); + } + Iterator<Map.Entry<String, Long>> it = resident.entrySet().iterator(); + while (reusable > MAX_RETAINED_CHUNK_CACHE_BYTES && it.hasNext()) { + Map.Entry<String, Long> entry = it.next(); + if (pinned.contains(entry.getKey())) continue; + reusable -= entry.getValue(); + release.add(entry.getKey()); + it.remove(); + } + return release; + } + + long bytes() { + long bytes = 0; + for (long size : resident.values()) bytes += size; + return bytes; + } + } + + private static final class ChunkPrefetch implements AutoCloseable { + final PrefetchSchedule schedule; + final File cacheDir; + final AtomicBoolean cancel; + final AtomicBoolean stopped = new AtomicBoolean(); + final ExecutorService workers = Executors.newFixedThreadPool(PREFETCH_PARTS); + final Map<String, Future<Boolean>> jobs = new LinkedHashMap<>(); + final Set<HttpURLConnection> connections = ConcurrentHashMap.newKeySet(); + final String chunkDir; + final List<CdnUrl> cdns; + + ChunkPrefetch(List<FileInfo> files, Map<String, ChunkInfo> chunks, Map<String, Long> cached, + File cacheDir, String chunkDir, List<CdnUrl> cdns, AtomicBoolean cancel) { + schedule = new PrefetchSchedule(files, chunks, cached); + this.cacheDir = cacheDir; + this.chunkDir = chunkDir; + this.cdns = cdns; + this.cancel = cancel; + } + + File await(String guid) throws Exception { + for (String fetch : schedule.fill()) { + jobs.put(fetch, workers.submit(() -> downloadChunkStreaming(schedule.chunks.get(fetch), + chunkDir, cdns, new File(cacheDir, fetch), cancel, stopped, connections))); + } + Future<Boolean> job = jobs.get(guid); + if (job != null) while (true) { + if (cancel.get()) throw new IOException("cancelled"); + try { + if (!job.get(100, TimeUnit.MILLISECONDS)) { + if (cancel.get()) throw new IOException("cancelled"); + throw new InstallException("Failed to download Epic chunk " + guid); + } + break; + } catch (TimeoutException ignored) { } + } + return new File(cacheDir, guid); + } + + void consume() throws InstallException { + for (String guid : schedule.consume()) { + // Only the writer advances the schedule, after closing its chunk reader. + jobs.remove(guid); + File chunk = new File(cacheDir, guid); + if (chunk.exists() && !chunk.delete() && chunk.exists()) { + throw new InstallException("Could not release Epic cache chunk " + guid); } - @Override public void onLog(String line) { cb.onLog(line); } - @Override public void onComplete(boolean success, String error, long bytes) {} - }); - r.started = res.started; r.success = res.success; r.cancelled = res.cancelled; r.error = res.error == null ? "" : res.error; - } catch (Throwable t) { - r.started = false; - r.error = t.getClass().getSimpleName() + ": " + t.getMessage(); + } + } + + @Override public void close() { + stopped.set(true); + for (HttpURLConnection connection : connections) connection.disconnect(); + workers.shutdownNow(); + boolean interrupted = false; + while (true) { + try { + if (workers.awaitTermination(100, TimeUnit.MILLISECONDS)) break; + for (HttpURLConnection connection : connections) connection.disconnect(); + } catch (InterruptedException e) { interrupted = true; } + } + if (interrupted) Thread.currentThread().interrupt(); + } + } + + static boolean isValidCachedChunk(ChunkInfo chunk, File file) { + if (!file.isFile()) return false; + if (chunk.windowSize > 0 && file.length() != chunk.windowSize) return false; + byte[] expected = chunk.sha1; + if (expected == null || expected.length != 20) return true; + boolean allZero = true; + for (byte b : expected) if (b != 0) { allZero = false; break; } + if (allZero) return true; + try (FileInputStream in = new FileInputStream(file)) { + MessageDigest sha = MessageDigest.getInstance("SHA-1"); + byte[] buffer = new byte[64 * 1024]; + int count; + while ((count = in.read(buffer)) != -1) sha.update(buffer, 0, count); + return MessageDigest.isEqual(sha.digest(), expected); + } catch (Exception e) { + return false; + } + } + + private static MessageDigest expectedDigest(byte[] expected) throws InstallException { + if (expected == null || expected.length != 20) return null; + for (byte b : expected) if (b != 0) { + try { return MessageDigest.getInstance("SHA-1"); } + catch (java.security.NoSuchAlgorithmException e) { throw new InstallException("SHA-1 verification unavailable"); } + } + return null; + } + + static Map<String, Integer> chunkReferences(List<FileInfo> files) { + Map<String, Integer> refs = new LinkedHashMap<>(); + for (FileInfo file : files) for (ChunkPart part : file.parts) { + String guid = part.guidStr(); + refs.put(guid, refs.containsKey(guid) ? refs.get(guid) + 1 : 1); + } + return refs; + } + + /** + * Simulates reserved parallel prefetch → append part → last-reference eviction order. The returned + * additional-space peak is measured against the current free-space reading: existing verified + * chunks are already reflected in that reading and count as reclaimed only when actually freed. + * Old output bytes are reclaimed after the complete new file is staged and verified, never + * before its parts are written. The same schedule is used for scratch-cache game-volume checks. + */ + static long estimatePeakAdditionalBytes(List<FileInfo> files, Map<String, ChunkInfo> chunksByGuid, + Map<String, Long> cachedChunkBytes, boolean includeFileWrites) { + return estimatePeakAdditionalBytes(files, chunksByGuid, cachedChunkBytes, includeFileWrites, new LinkedHashMap<>()); + } + + static long estimatePeakAdditionalBytes(List<FileInfo> files, Map<String, ChunkInfo> chunksByGuid, + Map<String, Long> cachedChunkBytes, boolean includeFileWrites, + Map<String, Long> oldFileBytes) { + PrefetchSchedule schedule = new PrefetchSchedule(files, chunksByGuid, cachedChunkBytes); + long initialCache = schedule.bytes(); + long fileDelta = 0, peak = 0; + for (FileInfo file : files) { + for (ChunkPart part : file.parts) { + schedule.fill(); + peak = Math.max(peak, fileDelta + schedule.bytes() - initialCache); + if (includeFileWrites) { + fileDelta += part.size & 0xFFFFFFFFL; + peak = Math.max(peak, fileDelta + schedule.bytes() - initialCache); + } + schedule.consume(); + } + // The verified staging file coexists with the old output until its atomic swap. + if (includeFileWrites) fileDelta -= oldFileBytes.containsKey(file.filename) ? oldFileBytes.get(file.filename) : 0; + } + return Math.max(0, peak); + } + + private static void copyChunkPart(File chunkFile, ChunkPart part, BufferedOutputStream out, + MessageDigest fileSha, AtomicBoolean cancel, String relPath) throws IOException { + long offset = part.offset & 0xFFFFFFFFL; + long remaining = part.size & 0xFFFFFFFFL; + long length = chunkFile.length(); + if (offset > length || remaining > length - offset) { + throw new IOException("Chunk " + part.guidStr() + " is too short for " + relPath); + } + try (FileInputStream in = new FileInputStream(chunkFile)) { + in.getChannel().position(offset); + byte[] buffer = new byte[64 * 1024]; + while (remaining > 0) { + if (cancel.get()) throw new IOException("cancelled"); + int count = in.read(buffer, 0, (int) Math.min(buffer.length, remaining)); + if (count < 0) throw new IOException("Unexpected end of chunk " + part.guidStr()); + out.write(buffer, 0, count); + if (fileSha != null) fileSha.update(buffer, 0, count); + remaining -= count; + } } - return r; } - // ---- CDN list and manifest download --------------------------------------------------------- public static List<CdnUrl> parseCdnUrls(String json) { List<CdnUrl> result = new ArrayList<>(); @@ -419,7 +1147,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 +1166,6 @@ public final class EpicDownloadManager { return null; } - // ---- manifest parsing ----------------------------------------------------------------------- public static Manifest parseManifest(byte[] bytes) { try { @@ -468,24 +1194,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 +1327,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 +1356,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,15 +1374,19 @@ 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) { + private static boolean downloadChunkStreaming(ChunkInfo chunk, String chunkDir, List<CdnUrl> cdnUrls, + File outFile, AtomicBoolean cancel, AtomicBoolean stopped, + Set<HttpURLConnection> connections) { String chunkPath = chunk.getPath(chunkDir); File tmp = new File(outFile.getPath() + ".part"); tmp.delete(); for (CdnUrl cdn : cdnUrls) { + if (cancel.get() || stopped.get()) return false; HttpURLConnection conn = null; try { conn = (HttpURLConnection) new URL(cdn.baseUrl + cdn.cloudDir + "/" + chunkPath).openConnection(); + connections.add(conn); + if (cancel.get() || stopped.get()) return false; conn.setConnectTimeout(30000); conn.setReadTimeout(60000); conn.setRequestProperty("User-Agent", UA); @@ -687,12 +1409,14 @@ public final class EpicDownloadManager { int storedAs = hdr.get() & 0xFF; if (headerSize > 41) skipFully(in, headerSize - 41); byte[] iobuf = new byte[131072]; + long outputBytes = 0; if ((storedAs & 1) != 0) { Inflater inflater = new Inflater(); byte[] obuf = new byte[131072]; int remaining = compressedSize; try { while (remaining > 0 && !inflater.finished()) { + if (cancel.get() || stopped.get()) break; if (inflater.needsInput()) { int n = in.read(iobuf, 0, Math.min(iobuf.length, remaining)); if (n <= 0) break; @@ -700,31 +1424,56 @@ public final class EpicDownloadManager { inflater.setInput(iobuf, 0, n); } int out = inflater.inflate(obuf); - if (out > 0) { fos.write(obuf, 0, out); if (sha != null) sha.update(obuf, 0, out); } + if (out > 0) { + outputBytes = checkedChunkWriteSize(chunk, outputBytes, out); + fos.write(obuf, 0, out); + if (sha != null) sha.update(obuf, 0, out); + } + if (out == 0 && !inflater.needsInput() && !inflater.finished()) { + throw new IOException("invalid compressed Epic chunk"); + } } int out; - while ((out = inflater.inflate(obuf)) > 0) { fos.write(obuf, 0, out); if (sha != null) sha.update(obuf, 0, out); } + while ((out = inflater.inflate(obuf)) > 0) { + if (cancel.get() || stopped.get()) break; + outputBytes = checkedChunkWriteSize(chunk, outputBytes, out); + fos.write(obuf, 0, out); + if (sha != null) sha.update(obuf, 0, out); + } + if (!cancel.get() && !stopped.get() && !inflater.finished()) throw new IOException("truncated compressed Epic chunk"); } finally { inflater.end(); } } else { int remaining = compressedSize; while (remaining > 0) { + if (cancel.get() || stopped.get()) break; int n = in.read(iobuf, 0, Math.min(iobuf.length, remaining)); if (n <= 0) break; + outputBytes = checkedChunkWriteSize(chunk, outputBytes, n); fos.write(iobuf, 0, n); if (sha != null) sha.update(iobuf, 0, n); remaining -= n; } + if (!cancel.get() && !stopped.get() && remaining != 0) throw new IOException("truncated Epic chunk"); } } + if (cancel.get() || stopped.get()) { tmp.delete(); conn.disconnect(); return false; } + if (chunk.windowSize > 0 && tmp.length() != chunk.windowSize) { tmp.delete(); conn.disconnect(); continue; } if (sha != null && !MessageDigest.isEqual(sha.digest(), chunk.sha1)) { tmp.delete(); conn.disconnect(); continue; } if (!tmp.renameTo(outFile)) { tmp.delete(); conn.disconnect(); continue; } conn.disconnect(); return true; } catch (Exception e) { + Log.w(TAG, "chunk " + chunk.guidStr() + " failed: " + e.getMessage()); tmp.delete(); if (conn != null) conn.disconnect(); + } finally { + if (conn != null) { + connections.remove(conn); + conn.disconnect(); + } + tmp.delete(); } } tmp.delete(); @@ -732,6 +1481,17 @@ public final class EpicDownloadManager { return false; } + private static long checkedChunkWriteSize(ChunkInfo chunk, long written, int count) throws IOException { + long next = written + count; + if (next > chunkWindowSize(chunk)) throw new IOException("chunk exceeds expected window size"); + return next; + } + + private static long chunkWindowSize(ChunkInfo chunk) { + // Legacy JSON manifests omit window sizes; their chunks use 1 MiB windows. + return chunk.windowSize > 0 ? chunk.windowSize : 1024L * 1024; + } + private static void readFully(InputStream in, byte[] buf) throws IOException { int off = 0; while (off < buf.length) { int n = in.read(buf, off, buf.length - off); if (n < 0) throw new IOException("stream ended"); off += n; } @@ -743,7 +1503,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 +1510,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 +1548,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 +1563,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,16 +1579,6 @@ public final class EpicDownloadManager { } } - // ---- helpers -------------------------------------------------------------------------------- - - static byte[] readFile(File f) throws IOException { - try (FileInputStream fis = new FileInputStream(f)) { - byte[] data = new byte[(int) f.length()]; - int off = 0; - while (off < data.length) { int r = fis.read(data, off, data.length - off); if (r < 0) break; off += r; } - return data; - } - } static void deleteDir(File dir) { if (!dir.exists()) return; @@ -841,20 +1587,6 @@ public final class EpicDownloadManager { dir.delete(); } - private static void sampleSpeed(long done, AtomicLong lastSpeedMs, AtomicLong lastSpeedBytes, AtomicLong speedBps) { - long nowMs = System.currentTimeMillis(), prevMs = lastSpeedMs.get(); - if (nowMs - prevMs >= 500 && lastSpeedMs.compareAndSet(prevMs, nowMs)) { - long prevB = lastSpeedBytes.getAndSet(done); - long dt = nowMs - prevMs; - if (dt > 0) speedBps.set((done - prevB) * 1000L / dt); - } - } - - private static String speedSuffix(long bps) { - if (bps <= 0) return ""; - return " " + (bps >= 1048576 ? String.format("%.1f MB/s", bps / 1048576.0) : (bps / 1024) + " KB/s"); - } - private static String fmt(long bytes) { if (bytes >= 1073741824L) return String.format("%.1f GB", bytes / 1073741824.0); if (bytes >= 1048576L) return String.format("%.1f MB", bytes / 1048576.0); 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 d65b0c3e6..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 @@ -3,18 +3,13 @@ package com.droiddeck.launcher.stores.epic import android.content.Context import android.content.SharedPreferences import android.util.Log +import com.droiddeck.launcher.R import com.droiddeck.launcher.stores.CatalogItem import com.droiddeck.launcher.stores.Store 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" @@ -52,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) @@ -67,17 +61,17 @@ object EpicLibrary { val cachedList = cached(app) val lastSync = p.getLong(LAST_SYNC_KEY, 0L) if (!force && cachedList.isNotEmpty() && System.currentTimeMillis() - lastSync < THROTTLE_MS) return SyncResult.Throttled - onStatus("Checking sign-in…") + onStatus(app.getString(R.string.stores_sync_sign_in)) val token = EpicCredentialStore.getValidAccessToken(app) ?: return SyncResult.NotLoggedIn - onStatus("Fetching game list…") + onStatus(app.getString(R.string.stores_sync_game_list)) val raw = EpicApiClient.getLibraryItems(token) - if (raw.isNullOrEmpty()) return if (cachedList.isEmpty()) SyncResult.Failed("No games found in your Epic library") else SyncResult.Ok(cachedList) + if (raw.isNullOrEmpty()) return if (cachedList.isEmpty()) SyncResult.Failed(app.getString(R.string.stores_sync_no_games, Store.EPIC.label)) else SyncResult.Ok(cachedList) val total = raw.size var done = 0 for (game in raw) { EpicApiClient.enrichFromCatalog(token, game) done++ - if (done % 5 == 0) onStatus("Loading game details… ($done/$total)") + if (done % 5 == 0) onStatus(app.getString(R.string.stores_sync_details, done, total)) } val main = raw.filter { !it.isDLC } val display = (if (main.isEmpty()) raw else main).distinctBy { it.appName }.sortedWith { a, b -> a.title.compareTo(b.title, ignoreCase = true) } @@ -92,7 +86,7 @@ object EpicLibrary { return SyncResult.Ok(display) } catch (e: Exception) { Log.w(TAG, "sync failed: ${e.message}") - return SyncResult.Failed(e.message ?: "Sync failed") + return SyncResult.Failed(e.message ?: context.getString(R.string.stores_sync_failed)) } finally { syncing.set(false) } @@ -114,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..41b6637c2 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,40 @@ 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. */ + interface InstallListener : Listener { + /** Synchronous approval before the first verified atomic replacement. False aborts. */ + fun beforeSwap(): Boolean + } + + /** One native budgeted fetch/assembly run. A loaded-engine failure is never a Java retry. */ + @JvmStatic + fun installAdaptive(request: String, cancel: AtomicBoolean, listener: InstallListener): Result = + drive("nativeInstallAdaptive", cancel, listener, strict = true) { inner -> + nativeInstallAdaptive(request, inner) + } + + /** Callbacks from a [stream] run; every method is called on a native thread. */ + interface StreamListener { + /** File bytes written so far (staging included), and the files verified and swapped in so far. */ + fun onStreamProgress(bytesWritten: Long, bytesTotal: Long, filesDone: Int, filesTotal: Int, filesBytes: Long) + /** Engine log line (already in logcat under `EpicNative`). */ + fun onLog(line: String) + /** Once, before the first file replaces its destination; false (or a throw) fails the run with nothing replaced. */ + fun onBeforeReplace(): Boolean + /** Terminal; `bytes` = file bytes written. */ + fun onComplete(success: Boolean, error: String, bytes: Long) + } + 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 +48,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,18 +56,73 @@ 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) } - private fun drive(what: String, cancel: AtomicBoolean?, listener: Listener, start: (Listener) -> Long): Result { + /** + * The adaptive install of the files [pendingFileIdx] (indices into the manifest's file list), + * with at most [cacheBudget] bytes of shared chunks cached and at most [overlapBudget] planned + * bytes of replaced files staged at once (`-1` = no limit), staged in [stagingDir] (the + * manager's owned staging directory). `started == false` means nothing was fetched or written. + */ + @JvmStatic + fun stream( + manifest: ByteArray, installDir: String, chunkCacheDir: String, stagingDir: String, cdnPrefixes: Array<String>, pendingFileIdx: IntArray, expectedBytes: Long, + cacheBudget: Long, overlapBudget: Long, caBundlePath: String, maxWorkers: Int, processWorkers: Int, cancel: AtomicBoolean?, listener: StreamListener, + ): Result { + if (!StoresNative.ensureLoaded()) return Result(false, false, false, "engine not built", 0L, 0, 0) + val latch = CountDownLatch(1) + val completion = AtomicReference<Completion?>(null) + val doneRef = AtomicReference(0) + val totalRef = AtomicReference(pendingFileIdx.size) + val inner = object : StreamListener { + override fun onStreamProgress(bytesWritten: Long, bytesTotal: Long, filesDone: Int, filesTotal: Int, filesBytes: Long) { + doneRef.set(filesDone); totalRef.set(filesTotal) + listener.onStreamProgress(bytesWritten, bytesTotal, filesDone, filesTotal, filesBytes) + } + override fun onLog(line: String) = listener.onLog(line) + override fun onBeforeReplace(): Boolean = runCatching { listener.onBeforeReplace() }.getOrDefault(false) + override fun onComplete(success: Boolean, error: String, bytes: Long) { + completion.set(Completion(success, error, bytes)) + latch.countDown() + runCatching { listener.onComplete(success, error, bytes) } + } + } + val handle: Long = try { + nativeStream(manifest, installDir, chunkCacheDir, stagingDir, cdnPrefixes, pendingFileIdx, expectedBytes, cacheBudget, overlapBudget, caBundlePath, maxWorkers, processWorkers, inner) + } catch (t: Throwable) { + Log.w(TAG, "engine unavailable: ${t.javaClass.simpleName}: ${t.message}") + return Result(false, false, false, "nativeStream: ${t.javaClass.simpleName}: ${t.message}", 0L, 0, 0) + } + if (handle == 0L) return Result(false, false, false, completion.get()?.error ?: "not started", 0L, 0, totalRef.get()) + var cancelSent = false + try { + while (!latch.await(250, TimeUnit.MILLISECONDS)) { + if (!cancelSent && cancel != null && cancel.get()) { + nativeCancel(handle) + cancelSent = true + } + } + } finally { + if (completion.get() == null) nativeCancel(handle) + nativeRelease(handle) + } + val c = completion.get() + val cancelled = cancelSent || (c != null && !c.success && c.error == "cancelled") + val success = !cancelled && c != null && c.success + val error = when { cancelled -> "cancelled"; c == null -> "no completion"; else -> c.error } + return Result(true, success, cancelled, error, c?.bytes ?: 0L, doneRef.get(), totalRef.get()) + } + + private fun drive(what: String, cancel: AtomicBoolean?, listener: Listener, strict: Boolean = false, start: (Listener) -> Long): Result { if (!StoresNative.ensureLoaded()) return Result(false, false, false, "engine not built", 0L, 0, 0) val latch = CountDownLatch(1) val completion = AtomicReference<Completion?>(null) val doneRef = AtomicReference(0) val totalRef = AtomicReference(0) - val inner = object : Listener { + val inner = object : InstallListener { + override fun beforeSwap(): Boolean = (listener as? InstallListener)?.beforeSwap() ?: false override fun onPlan(chunksTotal: Int, bytesTotal: Long, chunkDir: String) { totalRef.set(chunksTotal); listener.onPlan(chunksTotal, bytesTotal, chunkDir) } override fun onProgress(bytesDone: Long, bytesTotal: Long, done: Int, total: Int) { doneRef.set(done); totalRef.set(total); listener.onProgress(bytesDone, bytesTotal, done, total) } override fun onLog(line: String) = listener.onLog(line) @@ -77,21 +135,26 @@ object EpicNative { val handle: Long = try { start(inner) } catch (t: Throwable) { - Log.w(TAG, "engine unavailable: ${t.javaClass.simpleName}: ${t.message}") - return Result(false, false, false, "$what: ${t.javaClass.simpleName}", 0L, 0, 0) + Log.w(TAG, "${if (strict) "engine start failed (no fallback)" else "engine unavailable"}: ${t.javaClass.simpleName}: ${t.message}") + return Result(strict, false, false, "$what: ${t.javaClass.simpleName}: ${t.message}", 0L, 0, 0) } - if (handle == 0L) return Result(false, false, false, completion.get()?.error ?: "not started", 0L, 0, totalRef.get()) + if (handle == 0L) return Result(strict, false, false, completion.get()?.error ?: "not started", 0L, 0, totalRef.get()) var cancelSent = false try { while (!latch.await(250, TimeUnit.MILLISECONDS)) { if (cancel != null && cancel.get()) { nativeCancel(handle) cancelSent = true - latch.await(5, TimeUnit.SECONDS) + if (strict) { + while (!latch.await(250, TimeUnit.MILLISECONDS)) { } + } else { + latch.await(5, TimeUnit.SECONDS) + } break } } } finally { + if (completion.get() == null) nativeCancel(handle) nativeRelease(handle) } val c = completion.get() @@ -100,6 +163,9 @@ object EpicNative { return Result(true, success, cancelSent, error, c?.bytes ?: 0L, doneRef.get(), totalRef.get()) } + @JvmStatic + private external fun nativeInstallAdaptive(request: String, listener: Listener): Long + @JvmStatic private external fun nativeStart( manifest: ByteArray, installDir: String, chunkCacheDir: String, cdnPrefixes: Array<String>, pendingFileIdx: IntArray, expectedChunks: Int, expectedBytes: Long, @@ -109,6 +175,12 @@ object EpicNative { @JvmStatic private external fun nativeAssemble(manifest: ByteArray, installDir: String, chunkCacheDir: String, fileIdx: IntArray, listener: Listener): Long + @JvmStatic + private external fun nativeStream( + manifest: ByteArray, installDir: String, chunkCacheDir: String, stagingDir: String, cdnPrefixes: Array<String>, pendingFileIdx: IntArray, expectedBytes: Long, + cacheBudget: Long, overlapBudget: Long, caBundlePath: String, maxWorkers: Int, processWorkers: Int, listener: StreamListener, + ): Long + @JvmStatic private external fun nativeCancel(handle: Long) 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 c2bbfa635..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,13 +31,11 @@ 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 { if (cached.isNotEmpty()) StoresState.library[store] = com.droiddeck.launcher.stores.mergeItems(StoresState.library[store], cached) - if (!quiet) StoresState.status[store] = "Fetching your library…" + if (!quiet) StoresState.status[store] = app.getString(com.droiddeck.launcher.R.string.stores_sync_library) } val result = GogLibrary.sync(app, force) { line -> if (!quiet) StoresState.post { StoresState.status[store] = line } } 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) @@ -106,12 +101,11 @@ object GogBackend : StoreBackend { } }, cancelled) ?: return null if (cancelled.get()) return null - handle.stage(DownloadStage.INSTALL, "Registering with Steam…") + handle.stage(DownloadStage.INSTALL, app.getString(com.droiddeck.launcher.R.string.stores_registering)) val sidecar = StoreGameSidecar( 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..b61af1e96 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); } @@ -189,15 +152,15 @@ public final class GogDownloadManager { installPath.mkdirs(); long plannedBytes = 0; for (DepotFile df : files) plannedBytes += df.totalSize; - if (plannedBytes > 0) { - long usable = installPath.getUsableSpace(); - if (usable > 0 && plannedBytes > usable) { + long remainingBytes = remainingInstallBytes(files, installPath); + if (remainingBytes > 0) { + long usable = com.droiddeck.launcher.stores.StoreInstallRoot.availableBytes(installPath); + if (com.droiddeck.launcher.stores.StoreInstallRoot.insufficientSpace(remainingBytes, usable)) { out.hardStop = true; - out.error = "Not enough free space: need " + formatBytes(plannedBytes) + ", only " + formatBytes(usable) + " free"; + out.error = "Not enough free space: need " + formatBytes(remainingBytes) + ", only " + formatBytes(usable) + " free"; 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 +227,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 +244,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 +270,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 +277,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 +379,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 +483,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 +493,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 +504,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 +530,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 +557,6 @@ public final class GogDownloadManager { } } - // ---- gen2 helpers (shared with the Java pool) ----------------------------------------------- private static void parseDepotManifest(String json, List<DepotFile> out) { try { @@ -675,17 +620,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 +651,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 +705,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 +776,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 +881,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 +890,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 +909,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); @@ -1018,7 +947,16 @@ public final class GogDownloadManager { for (File f : files) if (f.isDirectory()) collectExeRecursive(f, out); } - private static final class DepotFile { + static long remainingInstallBytes(List<DepotFile> files, File installPath) { + long remaining = 0; + for (DepotFile file : files) { + File output = new File(installPath, file.relativePath); + if (!fileVerified(output, file.totalSize, file.md5)) remaining += file.totalSize; + } + return remaining; + } + + static final class DepotFile { final String relativePath; final List<ChunkRef> chunks = new ArrayList<>(); String md5; @@ -1038,12 +976,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 +999,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 +1055,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 cba199ce7..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 @@ -3,6 +3,7 @@ package com.droiddeck.launcher.stores.gog import android.content.Context import android.content.SharedPreferences import android.util.Log +import com.droiddeck.launcher.R import com.droiddeck.launcher.stores.CatalogItem import com.droiddeck.launcher.stores.Store import com.droiddeck.launcher.stores.StoreNet @@ -13,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 @@ -37,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 { @@ -53,26 +45,24 @@ 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 { val app = context.applicationContext val p = GogPrefs.get(app) val token = GogAuth.validToken(app) ?: return SyncResult.NotLoggedIn - onStatus("Fetching game list…") + onStatus(app.getString(R.string.stores_sync_game_list)) val gamesJson = StoreNet.get("https://embed.gog.com/user/data/games", bearer = token, userAgent = GogAuth.GALAXY_UA) - ?: return SyncResult.Failed("Couldn't reach GOG") + ?: return SyncResult.Failed(app.getString(R.string.stores_sync_gog_unreachable)) val ids = ArrayList<String>() val parsed = runCatching { 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 - if (!parsed) return SyncResult.Failed("Error parsing library") + if (!parsed) return SyncResult.Failed(app.getString(R.string.stores_sync_gog_unreadable)) val ownedSet = ids.toHashSet() val cachedList = cached(app) @@ -92,7 +82,7 @@ object GogLibrary { saveCache(p, list) return SyncResult.Ok(list.sortedBy { it.title.lowercase() }, 0) } - onStatus("Syncing ${idsToFetch.size} game${if (idsToFetch.size == 1) "" else "s"}…") + onStatus(app.resources.getQuantityString(R.plurals.stores_sync_games, idsToFetch.size, idsToFetch.size)) val pool = Executors.newFixedThreadPool(5) val futures = idsToFetch.map { id -> pool.submit(Callable<GogGame?> { fetchGame(p, id, token, skipped) }) } pool.shutdown() @@ -100,7 +90,7 @@ object GogLibrary { for ((idx, f) in futures.withIndex()) { val g = runCatching { f.get() }.getOrNull() if (g != null) { merged[g.gameId] = g; fetched++ } - if ((idx + 1) % 5 == 0) onStatus("Syncing… ${idx + 1}/${futures.size}") + if ((idx + 1) % 5 == 0) onStatus(app.getString(R.string.stores_sync_progress, idx + 1, futures.size)) } val finalList = merged.values.toList() saveCache(p, finalList) @@ -111,15 +101,13 @@ object GogLibrary { return SyncResult.Ok(finalList.sortedBy { it.title.lowercase() }, fetched) } catch (e: Exception) { Log.w(TAG, "sync failed: ${e.message}") - return SyncResult.Failed(e.message ?: "Sync failed") + return SyncResult.Failed(e.message ?: context.getString(R.string.stores_sync_failed)) } finally { syncing.set(false) } } - /** 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 @@ -130,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 = "" @@ -144,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) } } @@ -217,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 bc126b606..c3a16b944 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(), ) @@ -159,30 +151,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, @@ -191,11 +174,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, @@ -206,6 +186,7 @@ class FrontEndActions( val onAgentCommands: () -> Unit = {}, val onShareLogs: () -> Unit = {}, val onClearLogs: () -> Unit = {}, + val onResetSettings: () -> Unit = {}, val onOffline: () -> Unit, val onPageBack: () -> Unit = {}, val onTheme: (String) -> Unit = {}, @@ -219,7 +200,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 = {}, @@ -242,15 +222,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> = @@ -305,26 +282,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 { @@ -339,16 +305,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 @@ -359,7 +322,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 @@ -373,14 +335,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 { } @@ -389,11 +345,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 { } @@ -442,20 +393,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"), @@ -463,32 +409,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 @@ -511,19 +449,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 @@ -534,7 +465,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") { @@ -547,12 +477,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() } } @@ -562,9 +490,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 @@ -594,9 +519,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 -> @@ -661,11 +584,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 ebf114ab4..13cf53ed5 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() } @@ -405,6 +394,8 @@ internal fun SetupPanel( ActionRow(stringResource(R.string.setup_latest_logs), stringResource(R.string.drawer_logs_hint), stringResource(R.string.drawer_share_logs), a.onShareLogs, progress = com.droiddeck.launcher.session.SessionLogShare.progress) ActionRow(stringResource(R.string.setup_saved_logs), stringResource(R.string.setup_saved_logs_hint, com.droiddeck.launcher.session.SessionPaths.KEEP_SESSIONS), stringResource(R.string.setup_clear_logs), a.onClearLogs) + ActionRow(stringResource(R.string.setup_reset_settings), stringResource(R.string.setup_reset_settings_hint), + stringResource(R.string.setup_reset_settings_button), a.onResetSettings) } } } @@ -413,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 @@ -458,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) @@ -493,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) { @@ -526,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 @@ -548,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) } @@ -578,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 { @@ -614,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 d5a7be1d6..c6296300e 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndWidgets.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndWidgets.kt @@ -66,7 +66,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) @@ -97,7 +96,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( @@ -105,13 +103,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 @@ -154,7 +150,6 @@ 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, fill: Boolean = false, @@ -162,7 +157,6 @@ internal fun PrimaryButton( ) { 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++ @@ -174,24 +168,19 @@ 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 = if (fill) Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally) else 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) } @@ -229,11 +218,6 @@ private fun FittedLabel(text: String, style: TextStyle, modifier: Modifier = Mod } } -/** - * [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 @@ -275,7 +259,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 e07231ef4..da60fe9f1 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/LiftSheet.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/LiftSheet.kt @@ -115,14 +115,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" @@ -133,30 +126,21 @@ 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 } internal val LocalSheetChipWidth = compositionLocalOf { Dp.Unspecified } @@ -168,7 +152,6 @@ private val ChipGap = 6.dp internal fun sheetChipWidth(row: Dp, count: Int): Dp = if (count > 0 && row / count >= 96.dp) (row - ChipGap * (count - 1)) / count else 0.dp -/** 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 @@ -186,13 +169,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( @@ -201,26 +177,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, @@ -233,7 +196,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) } @@ -241,11 +203,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) } @@ -264,18 +223,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 @@ -284,7 +240,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 -> @@ -298,7 +253,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() } } @@ -311,10 +265,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)) } @@ -323,7 +275,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 @@ -335,7 +286,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)) } @@ -343,8 +293,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) { @@ -353,7 +301,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 @@ -361,20 +308,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) @@ -382,7 +325,6 @@ internal fun LiftHero( val lipWanted = !open && (!shown || lipBack) && (playFocused || playHovered || touchOnly || lipFocused) && drop?.let { it.open || it.shown } != true 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 { @@ -391,10 +333,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 { @@ -403,7 +343,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 @@ -416,10 +355,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() } @@ -443,8 +378,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 { @@ -468,9 +401,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(drop) { val tracker = androidx.compose.ui.input.pointer.util.VelocityTracker() var decided = false @@ -489,7 +419,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. when { drop != null && (drop.open || drop.shown) -> if (drop.open && amount < 0f) drop.beginDrag() open || shown || amount < 0f -> if (if (open) amount > 0f else amount < 0f) beginDrag() @@ -515,24 +444,18 @@ 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) val pill = restRect?.translate(0f, raise) - // 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() - inset } val chipWidth = with(density) { sheetChipWidth((w - 2 * inset - chipsX - 64.dp.toPx()).coerceAtLeast(0f).toDp(), tabs.size) } - // The wall behind sinks back about the pane's centre. val foot = h val wallSink = { maxOf(sink.value, drop?.sink?.value ?: 0f) } Box(Modifier.fillMaxSize().clipToBounds().graphicsLayer { val k = 1f - 0.05f * wallSink(); 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)) @@ -540,7 +463,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 @@ -565,7 +487,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) @@ -577,35 +498,27 @@ internal fun LiftHero( } } 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 @@ -635,8 +548,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()) @@ -660,7 +571,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 - wallSink() }) { words() } Spacer(Modifier.height(18.dp)) @@ -670,7 +580,6 @@ internal fun LiftHero( .onGloballyPositioned { rest = it.boundsInRoot() } .graphicsLayer { translationY = raise * ride.value; alpha = 1f - (drop?.sink?.value ?: 0f) } .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 } @@ -693,7 +602,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), @@ -703,9 +611,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 @@ -715,7 +621,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) * (1f - (drop?.sink?.value ?: 0f)) } - // 44dp to touch, with a 32dp visible handle. .heightIn(min = 44.dp).testTag("sheet-open") .semantics { contentDescription = settingsLabel } .then( @@ -733,10 +638,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 internal fun Handle(height: Dp, above: Boolean = false) { val colors = MaterialTheme.colorScheme @@ -744,7 +645,6 @@ internal fun Handle(height: Dp, above: Boolean = false) { val shape = if (above) RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp) else 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( @@ -772,7 +672,6 @@ internal fun Handle(height: Dp, above: Boolean = false) { } } -/** 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, flip: Boolean = false): Modifier = this.drawWithContent { drawContent() @@ -789,12 +688,6 @@ private fun Modifier.drawBorderSidesAndFoot(color: Color, corner: Dp, flip: Bool if (flip) scale(1f, -1f) { drawPath(p, color, style = Stroke(sw)) } else 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( @@ -808,10 +701,8 @@ internal fun SheetChips( 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 }, @@ -820,11 +711,9 @@ internal fun SheetChips( val equal = !width.isSpecified && maxWidth / labels.size.coerceAtLeast(1) >= 96.dp val least = if (width.isSpecified) maxOf(width, 64.dp) else 64.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(ChipGap), - // 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(), @@ -836,7 +725,6 @@ internal fun SheetChips( 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) } } @@ -849,8 +737,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, @@ -866,10 +752,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 3d97ca656..ac9a0524a 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/MainScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/MainScreen.kt @@ -2,17 +2,29 @@ package com.droiddeck.launcher.ui import com.droiddeck.launcher.R import androidx.compose.ui.res.stringResource +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Checkbox import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import com.droiddeck.launcher.HomeApp import com.droiddeck.launcher.input.SecondScreenDisplay @@ -53,3 +65,31 @@ fun ConfirmDialog(title: String, text: String, confirm: String, onConfirm: () -> dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.common_cancel)) } }, ) } + +@Composable +fun ResetSettingsDialog(onConfirm: (gameProfiles: Boolean) -> Unit, onDismiss: () -> Unit) { + var gameProfiles by remember { mutableStateOf(false) } + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.setup_reset_settings_title)) }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text(stringResource(R.string.setup_reset_settings_body)) + Row( + Modifier.fillMaxWidth().toggleable(gameProfiles, role = Role.Checkbox) { gameProfiles = it }, + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox(checked = gameProfiles, onCheckedChange = null) + Spacer(Modifier.width(8.dp)) + Text(stringResource(R.string.setup_reset_settings_profiles), style = MaterialTheme.typography.bodyMedium) + } + } + }, + confirmButton = { + TextButton(onClick = { onDismiss(); onConfirm(gameProfiles) }) { + Text(stringResource(R.string.setup_reset_settings_button), color = MaterialTheme.colorScheme.error) + } + }, + dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.common_cancel)) } }, + ) +} 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 2ec97d7a7..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,57 +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, + val zramCompression: Boolean? = null, + 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, @@ -119,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 = {}, @@ -127,6 +108,7 @@ class ModeSettingsActions( val onTouch: (String) -> Unit, val onSuspendPolicy: (String) -> Unit, val onSteamDownloadsInBackground: (Boolean) -> Unit = {}, + val onZramCompression: (Boolean) -> Unit = {}, val onPipAutoEnter: (Boolean) -> Unit = {}, val onOsc: (String) -> Unit, val onBackActionsInverted: (Boolean) -> Unit = {}, @@ -154,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) @@ -174,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 @@ -303,6 +274,12 @@ fun ModeSheetRows(s: ModeSettings, a: ModeSettingsActions, tab: String, extras: stringResource(R.string.mode_background_downloads_hint), s.steamDownloadsInBackground, onChange = a.onSteamDownloadsInBackground, ) + if (s.zramCompression != null) ToggleRow( + host, "zram", stringResource(R.string.mode_zram), + stringResource(if (s.zramSupported == false) R.string.mode_zram_unsupported else R.string.mode_zram_hint), + s.zramCompression && s.zramSupported != false, enabled = s.zramSupported == true, + onChange = a.onZramCompression, + ) } if (s.pipSupported) SettingsGroup(stringResource(R.string.pip_title)) { ToggleRow(host, "pip-auto", stringResource(R.string.pip_auto), null, @@ -320,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 { @@ -467,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, @@ -497,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 @@ -527,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 c043076c8..bc4b04807 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"), GLASSES("glasses-game"), 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 80e95093b..863a10ff5 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt @@ -182,16 +182,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>, @@ -203,7 +200,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, @@ -211,17 +207,12 @@ 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, val glassesPresent: Boolean = false, val glassesOutput: Boolean = false, - /** 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, @@ -230,18 +221,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 = {}, @@ -259,7 +245,6 @@ class DrawerActions( val onCardboard: (Cardboard) -> Unit = {}, val onCardboardRecenter: () -> Unit = {}, val onGlassesRecenter: () -> Unit = {}, - /** install, start or stop SteamVR. */ val onSteamVr: (String) -> Unit = {}, val onTextureAnisotropy: (Int?) -> Unit = {}, val onTextureLodBias: (String?) -> Unit = {}, @@ -271,17 +256,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, @@ -290,9 +271,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, @@ -302,12 +281,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 = { _, _, _ -> }, ) @@ -352,8 +328,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) { @@ -364,7 +338,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 @@ -382,8 +355,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( @@ -505,7 +476,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 -> @@ -746,7 +716,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, @@ -771,7 +740,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 @@ -795,13 +763,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 @@ -815,9 +776,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") @@ -858,10 +816,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, @@ -926,7 +882,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 @@ -978,7 +933,6 @@ private fun SteamVrRow(a: DrawerActions) { } } -/** Secondary-display modes are mutually exclusive and apply immediately. */ @Composable private fun SecondScreenGroup(host: MenuHost, a: DrawerActions, track: (String) -> Modifier) { val colors = MaterialTheme.colorScheme @@ -1047,7 +1001,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 @@ -1058,7 +1011,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 @@ -1209,11 +1161,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 @@ -1275,7 +1222,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 @@ -1330,7 +1276,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), @@ -1345,7 +1290,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, @@ -1423,10 +1367,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?, @@ -1460,7 +1400,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 @@ -1469,18 +1408,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, @@ -1496,7 +1428,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) } @@ -1550,7 +1481,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), @@ -1567,7 +1497,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()) } @@ -1585,7 +1514,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 @@ -1624,7 +1552,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 5b20b2f96..73806d42c 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt @@ -68,23 +68,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) } @@ -93,7 +84,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( @@ -126,8 +116,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) @@ -140,19 +128,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 @@ -169,11 +154,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>, @@ -185,37 +165,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) { @@ -230,7 +193,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() @@ -279,7 +241,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 @@ -293,7 +254,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) @@ -302,7 +262,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)) } } @@ -312,7 +271,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 @@ -322,7 +280,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..125e4f108 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) { @@ -52,8 +51,9 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro val id = key.substringAfter(':') 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 rawDownload = StoresState.download("${store.id}:$id") + val download = rawDownload?.takeIf { it.isActive } + val failedDownload = rawDownload?.takeIf { it.state == DownloadState.FAILED } 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 +61,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,24 +72,30 @@ 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 + failedDownload != null && failedDownload.location.isNotBlank() -> stringResource(R.string.stores_eyebrow_library) + " · " + failedDownload.location item?.owned == true -> stringResource(R.string.stores_eyebrow_library) else -> stringResource(R.string.stores_eyebrow_store) } val size = (item?.sizeBytes ?: 0L).takeIf { it > 0 }?.let { " · " + formatBytes(it) } ?: "" Text((state + size).uppercase(), fontSize = 12.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 1.2.sp, color = pal.signal) Text(title, fontSize = if (narrow) 24.sp else 30.sp, lineHeight = if (narrow) 27.sp else 33.sp, fontWeight = FontWeight.Black, color = colors.onBackground, maxLines = 2, overflow = TextOverflow.Ellipsis) + if (!failedDownload?.error.isNullOrBlank()) { + Text( + failedDownload!!.error!!, + fontSize = 13.sp, + lineHeight = 17.sp, + fontWeight = FontWeight.Medium, + color = colors.error, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + } 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) {} @@ -106,6 +109,16 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro else PrimaryButton(stringResource(R.string.stores_dl_resume), main = true) { com.droiddeck.launcher.stores.download.DownloadQueue.resume(ctx, download.key) } ConfirmButton(stringResource(R.string.stores_dl_cancel), stringResource(R.string.stores_dl_cancel_confirm)) { com.droiddeck.launcher.stores.download.DownloadQueue.cancel(ctx, download.key) } } + failedDownload != null -> { + ActionChip(stringResource(R.string.stores_dl_failed), ok = false) + PrimaryButton(stringResource(R.string.stores_dl_resume), main = true) { + if (item != null) StoresState.requestInstall(ctx, item) + else com.droiddeck.launcher.stores.download.DownloadQueue.retry(ctx, failedDownload.key) + } + ConfirmButton(stringResource(R.string.stores_dl_clear), stringResource(R.string.stores_dl_cancel_confirm), compact = true) { + com.droiddeck.launcher.stores.download.DownloadQueue.clear(ctx, failedDownload.key) + } + } item?.owned == true && StoresState.isUnfinished(item) -> { InstallButton(stringResource(R.string.stores_resume_install)) { StoresState.requestInstall(ctx, item) } ConfirmButton(stringResource(R.string.stores_dl_clear), stringResource(R.string.stores_dl_cancel_confirm), compact = true) { StoresState.clearUnfinished(ctx, item) } @@ -123,7 +136,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 +143,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 +159,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 +171,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 +187,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..d9f2d1417 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() @@ -339,12 +305,11 @@ private fun Modifier.timeRim(left: () -> Float, color: Color): Modifier = drawWi @Composable private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) { val ctx = LocalContext.current + val colors = MaterialTheme.colorScheme val pal = LocalPalette.current 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,8 +327,11 @@ 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.FAILED -> { + if (!d.error.isNullOrBlank()) stringResource(R.string.stores_dl_failed_reason, d.error) + else 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) DownloadState.RUNNING -> { @@ -375,14 +343,14 @@ private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) } } } - Text(meta, fontSize = 12.sp, color = dim, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false)) + val metaColor = if (d.state == DownloadState.FAILED) colors.error else dim + Text(meta, fontSize = 12.sp, color = metaColor, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false)) if (d.state == DownloadState.RUNNING && (d.stage == DownloadStage.DOWNLOAD || d.stage == DownloadStage.INSTALL) && d.speedBps > 0) { Text(formatSpeed(d.speedBps), fontSize = 12.sp, color = dim, maxLines = 1) if (d.etaSeconds >= 0) Text(eta(d.etaSeconds), fontSize = 12.sp, color = dim, maxLines = 1) } } } - // 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 -> { @@ -393,8 +361,10 @@ private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) RoundAction(Icons.Filled.PlayArrow, stringResource(R.string.stores_dl_resume), primary = true) { DownloadQueue.resume(ctx, d.key) } 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.FAILED -> { + RoundAction(Icons.Filled.Refresh, stringResource(R.string.stores_dl_resume), primary = true) { DownloadQueue.retry(ctx, d.key) } + CancelAction { DownloadQueue.clear(ctx, d.key) } + } 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 +375,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 +385,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 +413,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 @@ -460,6 +420,7 @@ private fun StageBar(d: DownloadEntry) { val x = i * (segW + gap) drawRoundRect(colors.surfaceVariant, Offset(x, cy - h / 2f), Size(segW, h), CornerRadius(h / 2f)) val base = when { + d.state == DownloadState.FAILED -> colors.error done[i] -> pal.good actives[i] && paused -> pausedColor.copy(alpha = breath) actives[i] && stages[i] == DownloadStage.VERIFY -> verifyColor @@ -472,7 +433,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 +442,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 +456,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 +479,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 +490,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 +499,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..a8392465d 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 @@ -72,10 +67,21 @@ internal fun InstallWhereDialog(item: CatalogItem, onDismiss: () -> Unit) { Rise(1) { Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) { targets.forEachIndexed { i, t -> + val needed = com.droiddeck.launcher.stores.StoreInstallRoot.estimatedBytesNeeded(item) + val freeBytes = t.freeBytes + val isLow = com.droiddeck.launcher.stores.StoreInstallRoot.insufficientSpace(needed, freeBytes) + val freeText = if (freeBytes < 0L) { + stringResource(R.string.stores_target_free_unknown) + } else if (isLow) { + stringResource(R.string.stores_storage_low, formatBytes(freeBytes), formatBytes(needed)) + } else { + stringResource(R.string.stores_target_free, formatBytes(freeBytes)) + } TargetRow( label = if (t.removable) t.label else stringResource(R.string.stores_target_internal), - free = stringResource(R.string.stores_target_free, formatBytes(t.freeBytes)), + free = freeText, removable = t.removable, focus = focus[i], + warning = isLow, ) { SessionPrefs.setStoresInstallTarget(ctx, t.root.absolutePath) StoresState.install(ctx, item, t.root) @@ -88,7 +94,7 @@ internal fun InstallWhereDialog(item: CatalogItem, onDismiss: () -> Unit) { } @Composable -private fun TargetRow(label: String, free: String, removable: Boolean, focus: FocusRequester, onPick: () -> Unit) { +private fun TargetRow(label: String, free: String, removable: Boolean, focus: FocusRequester, warning: Boolean = false, onPick: () -> Unit) { val colors = MaterialTheme.colorScheme val pal = LocalPalette.current val src = remember { MutableInteractionSource() } @@ -104,6 +110,6 @@ private fun TargetRow(label: String, free: String, removable: Boolean, focus: Fo ) { Icon(if (removable) Icons.Outlined.SdCard else Icons.Outlined.Smartphone, null, tint = if (hot) pal.signal else colors.onBackground, modifier = Modifier.size(22.dp)) Text(label, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f)) - Text(free, fontSize = 12.sp, color = colors.onSurfaceVariant, maxLines = 1) + Text(free, fontSize = 12.sp, color = if (warning) colors.error else colors.onSurfaceVariant, maxLines = 1) } } 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..c0471d560 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 @@ -511,15 +438,24 @@ private fun InstallWhere(item: CatalogItem) { StepTitle(title) StepList { targets.forEachIndexed { i, t -> + val needed = com.droiddeck.launcher.stores.StoreInstallRoot.estimatedBytesNeeded(item) + val freeBytes = t.freeBytes + val isLow = com.droiddeck.launcher.stores.StoreInstallRoot.insufficientSpace(needed, freeBytes) + val trailingText = if (freeBytes < 0L) { + stringResource(R.string.stores_target_free_unknown) + } else if (isLow) { + stringResource(R.string.stores_storage_low, formatBytes(freeBytes), formatBytes(needed)) + } else { + stringResource(R.string.stores_target_free, formatBytes(freeBytes)) + } StepChoice( label = if (t.removable) t.label else internalLabel, icon = if (t.removable) androidx.compose.material.icons.Icons.Outlined.SdCard else androidx.compose.material.icons.Icons.Outlined.Smartphone, - trailing = stringResource(R.string.stores_target_free, formatBytes(t.freeBytes)), + trailing = trailingText, enabled = open, 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 +466,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 +474,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 +483,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 +501,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 +526,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 +551,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 +570,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 +600,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 +620,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 +632,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 +641,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 +674,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 +688,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 +696,6 @@ internal fun StoreLogo(store: Store, size: Dp) { } } -// ---- storefront -------------------------------------------------------------------------------- @Composable private fun Storefront( @@ -807,19 +707,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 +729,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 +749,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 +792,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 +823,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 +837,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 +853,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 +861,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 +876,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 +890,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 +912,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 +941,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 +956,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 +966,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 +975,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 +988,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 +1013,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 +1027,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 +1048,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 +1056,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 +1064,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 +1079,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 +1093,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 +1105,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 +1121,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 +1130,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 +1146,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 ce05db939..5f880ed28 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, boolean mono, float displayHz); - /** 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-ar/strings.xml b/app/src/main/res/values-ar/strings.xml new file mode 100644 index 000000000..f02b3a580 --- /dev/null +++ b/app/src/main/res/values-ar/strings.xml @@ -0,0 +1,2335 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>يُنزَّل Steam عند التشغيل الأول. قد يستغرق ذلك بضع دقائق.</item> + <item>افتراضيًا، يؤدي الخروج من التطبيق إلى إيقاف Steam مؤقتًا، ويُستأنف من حيث توقف عند عودتك.</item> + <item>تظهر وحدة التحكم لديك كوحدة تحكم Xbox 360. وإلا، فاستخدم عناصر التحكم على الشاشة.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>يُعَدّ سطح المكتب عند التشغيل الأول. قد يستغرق ذلك بضع دقائق.</item> + <item>افتراضيًا، يؤدي الخروج من التطبيق إلى إيقاف الجلسة مؤقتًا، وتُستأنف من حيث توقفت عند عودتك.</item> + <item>يعمل اللمس كلوحة لمس: اسحب لتحريك المؤشر، وانقر لتنفيذ نقرة.</item> + <item>تظهر وحدة التحكم لديك كوحدة تحكم Xbox 360. وإلا، فاستخدم عناصر التحكم على الشاشة.</item> + </string-array> + <string name="session_channel">الجلسة</string> + <string name="session_channel_description">يظهر أثناء فتح جلسة Steam أو جلسة سطح المكتب</string> + <string name="session_notification">Steam قيد التشغيل</string> + <string name="session_notification_downloads">يجري تنزيل في Steam</string> + <string name="session_notification_paused">الجلسة متوقفة مؤقتًا - انقر على «استئناف» عندما تكون مستعدًا</string> + <string name="resume_session">استئناف</string> + <string name="suspend_now">تعليق الآن</string> + <string name="stop_session">إيقاف الجلسة</string> + <string name="frame_gen_title">توليد الإطارات</string> + <string name="frame_gen_hint">اختر وضع توليد الإطارات</string> + <string name="frame_gen_off">إيقاف</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG تكيّفي %1$d</string> + <string name="lsfg_needs_steam">يتطلب Lossless Scaling على Steam</string> + <string name="lsfg_needs_install">ثبّت Lossless Scaling في Steam أو استورد Lossless.dll</string> + <string name="lsfg_import">استيراد Lossless.dll…</string> + <string name="lsfg_using_steam">يتم استخدام النسخة الموجودة في Steam</string> + <string name="lsfg_using_import">يتم استخدام نسخة مستوردة</string> + <string name="lsfg_pick_title">اختر Lossless.dll</string> + <string name="lsfg_imported">تم استيراد Lossless.dll</string> + <string name="lsfg_import_unchanged">ملف Lossless.dll هذا قيد الاستخدام بالفعل</string> + <string name="lsfg_import_not_owned">حساب Steam هذا لا يملك Lossless Scaling</string> + <string name="lsfg_import_invalid">هذا الملف ليس Lossless.dll الخاص بـ Lossless Scaling</string> + <string name="lsfg_import_failed">تعذّر تجهيز LSFG من ملف Lossless.dll هذا</string> + <string name="lsfg_problem_missing">يتطلب LSFG برنامج Lossless Scaling من Steam أو ملف Lossless.dll مستوردًا</string> + <string name="lsfg_problem_failed">تعذّر على LSFG تجهيز أدوات التظليل من Lossless.dll</string> + <string name="hud_fg_unsupported">%1$s: غير مدعوم</string> + <string name="hud_fg_failed">%1$s: فشل المحرك</string> + <string name="hud_fg_needs_lossless">%1$s: يتطلب Lossless Scaling</string> + <string name="hud_fg_starting">%1$s %2$s · جارٍ البدء</string> + <string name="second_library_import_hint">يضيف مكتبة Steam ويفحصها بحثًا عن ألعاب Windows. تستخدم الألعاب المملوكة إدخالاتها في Steam، ويتحول الباقي إلى اختصارات لألعاب من خارج Steam.</string> + <string name="game_env_title">بيئة اللعبة</string> + <string name="game_env_hint">متغيرات البيئة لألعاب Proton.</string> + <string name="game_env_edit">تعديل</string> + <string name="game_env_shared">جميع ألعاب Proton</string> + <string name="game_env_scope">ملف التعريف</string> + <string name="game_env_app_id">معرّف تطبيق Steam</string> + <string name="game_env_open">فتح</string> + <string name="game_env_profile">اللعبة %1$s</string> + <string name="game_env_add">إضافة متغير</string> + <string name="game_env_name">اسم المتغير</string> + <string name="game_env_value">القيمة</string> + <string name="game_env_save">حفظ</string> + <string name="game_env_remove">إزالة</string> + <string name="game_env_restore">استعادة الوراثة</string> + <string name="game_env_reset">مسح تجاوزات البيئة</string> + <string name="game_env_unset">إلغاء التعيين لملف التعريف هذا</string> + <string name="game_env_empty">لم يتم تعيين أي متغيرات.</string> + <string name="game_env_inherited">موروث</string> + <string name="game_env_custom">متغير مخصّص</string> + <string name="game_env_error">تعذّر التحميل أو الحفظ. تحقق من مساحة التخزين المتاحة وحاول مرة أخرى.</string> + <string name="game_env_invalid">استخدم اسمًا صالحًا وقيمة لا تتجاوز 8192 حرفًا.</string> + <string name="game_env_unused">لا يقرأ وقت تشغيل Proton القياسي هذا المتغير.</string> + <string name="game_env_loading">جارٍ التحميل…</string> + <string name="game_env_feature">تجاوز مستوى ميزات D3D12. تُبلغ القيمة 12_2 عن إمكانات DirectX 12 Ultimate، وقد تفشل الميزات غير المدعومة. اتركه دون تعيين للاكتشاف التلقائي.</string> + <string name="game_env_shader">تجاوز نموذج أدوات التظليل في D3D12. تتطلب القيمة 6_9 إصدارًا حديثًا بما يكفي من VKD3D وميزات GPU مطابقة. اتركه دون تعيين للاكتشاف التلقائي.</string> + <string name="game_env_cache">أبقِ التخزين المؤقت لأدوات التظليل في Mesa مفعّلًا لتقليل تكرار التجميع. القيمة false تفعّل التخزين المؤقت.</string> + <string name="game_env_cache_size">الحد الأقصى لحجم ذاكرة التخزين المؤقت لأدوات التظليل في Mesa، مثل 1G. تستهلك ذاكرة التخزين المؤقت الأكبر مساحة تخزين أكثر.</string> + <string name="game_env_vkd3d_config">خيارات VKD3D لـ D3D12. يعطّل nodxr تتبع الأشعة؛ استخدمه فقط للعبة تحتاج إليه.</string> + <string name="game_env_proton_log">اضبطه على 1 للحصول على سجل تشخيص Proton. قد يقلل التسجيل من الأداء ويستهلك مساحة التخزين.</string> + <string name="game_env_wined3d">اضبطه على 1 لاستخدام WineD3D المعتمد على OpenGL بدلًا من DXVK لاختبار توافق D3D9–11.</string> + <string name="game_env_no_ntsync">اضبطه على 1 لإبعاد هذه اللعبة عن ntsync (droiddeck-ntsync أو برنامج تشغيل في النواة)؛ فتستخدم droiddeck-esync حيث تتوفر حزمة، وإلا droiddeck-fsync.</string> + <string name="game_env_no_esync">اضبطه على 1 لإيقاف droiddeck-esync لهذه اللعبة؛ فتستخدم droiddeck-fsync بدلًا منه، أو droiddeck-ntsync إذا كان fsync متوقفًا أيضًا.</string> + <string name="game_env_no_fsync">اضبطه على 1 لإبعاد هذه اللعبة عن droiddeck-fsync؛ فتستخدم droiddeck-esync حيث تتوفر حزمة، وإلا droiddeck-ntsync.</string> + <string name="game_env_fsync_first">اضبطه على 1 لتشغيل هذه اللعبة على droiddeck-fsync، أو على 0 لتقديم droiddeck-esync حيث تتوفر حزمة تناسب إصدار Proton الخاص بها.</string> + <string name="game_env_xalia">اضبطه على 0 لتعطيل مساعد التنقل بوحدة التحكم في Proton لهذه اللعبة.</string> + <string name="game_env_hud">تراكب DXVK لـ D3D9–11. القيمة fps تعرض معدل الإطارات، وتركه دون تعيين يخفيه.</string> + <string name="game_env_dxvk_config">إعدادات DXVK مفصولة بفواصل منقوطة. تستخدم الإصدارات الحديثة dxvk.maxFrameRate لتحديد الإطارات؛ يُفضَّل استخدام محدِّد الإطارات الخاص بالجلسة للعب العادي.</string> + <string name="game_env_vkd3d_limit">حد معدل الإطارات في D3D12. القيمة 0 تلغي الحد.</string> + <string name="game_env_glthread">تعدد مؤشرات الترابط في OpenGL ضمن Mesa. قد تساعد القيمة true ألعاب OpenGL المقيدة بأداء CPU؛ جرّبها لكل لعبة.</string> + <string name="sync_backend_title">مزامنة Wine</string> + <string name="sync_backend_hint">طريقة إبقاء الألعاب لمؤشرات الترابط متزامنة. يُطبَّق في الجلسة التالية.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">إعداد FEX المسبق</string> + <string name="fex_next_launch">يُطبَّق عند التشغيل التالي للعبة.</string> + <string name="game_settings_title">الألعاب</string> + <string name="fex_default_label">إعدادات FEX الافتراضية</string> + <string name="fex_default_detail">يستخدم إعدادات FEX الافتراضية.</string> + <string name="fex_stability_label">الثبات</string> + <string name="fex_stability_detail">الإعدادات الأكثر أمانًا والأبطأ.</string> + <string name="fex_compatibility_label">التوافق</string> + <string name="fex_compatibility_detail">TSO كامل مع multiblock.</string> + <string name="fex_intermediate_label">متوسط</string> + <string name="fex_intermediate_detail">TSO متوازن مع دقة x87 مخفّضة.</string> + <string name="fex_performance_label">الأداء</string> + <string name="fex_performance_detail">TSO معطّل بالكامل؛ مع multiblock.</string> + <string name="fex_performance_tso_label">الأداء + TSO</string> + <string name="fex_performance_tso_detail">إعدادات «الأداء» مع تفعيل TSO الرئيسي.</string> + <string name="fex_extreme_label">فائق</string> + <string name="fex_extreme_detail">يعطّل فحوص التعليمات البرمجية ذاتية التعديل. قد تتعطل الألعاب التي تستخدم JIT وأنظمة DRM.</string> + <string name="fex_extreme_tso_label">فائق + TSO</string> + <string name="fex_extreme_tso_detail">إعدادات «فائق» مع تفعيل TSO الرئيسي.</string> + <string name="fex_extreme_gn_label">فائق-gn</string> + <string name="fex_extreme_gn_detail">«الأداء» مع تلميحات TSC والبيانات الوصفية. جرّبه قبل «فائق».</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">«الأداء» مع فحوص كاملة للتعليمات البرمجية وإخفاء بت hypervisor.</string> + <string name="game_env_custom_value">قيمة مخصّصة</string> + <string name="game_env_select_value">اختر قيمة</string> + <string name="user_apps_section">المحاكيات والتطبيقات</string> + <string name="user_apps_add">إضافة</string> + <string name="user_apps_add_detail">تطبيقك أو لعبتك الخاصة</string> + <string name="user_apps_kind_script">نص برمجي</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d%%</string> + <string name="user_apps_starting">جارٍ البدء…</string> + <string name="user_apps_added">تمت إضافة %1$s</string> + <string name="user_apps_removed">تمت إزالة %1$s</string> + <string name="user_apps_back">سطح المكتب</string> + <string name="user_apps_gone">لم يعد هذا التطبيق موجودًا. اختره مجددًا من سطح المكتب.</string> + <string name="user_apps_open">فتح %1$s</string> + <string name="user_apps_remove">إزالة</string> + <string name="user_apps_remove_confirm">اضغط مجددًا للإزالة</string> + <string name="user_apps_stop_session">أوقف الجلسة لتتمكن من الإزالة</string> + <string name="user_apps_script_linked">يشغّل %1$s من مجلده، المرتبط بالجلسة في مكانه. لا تؤثر إزالة التطبيق على ذلك المجلد.</string> + <string name="user_apps_script_copied">يشغّل %1$s من نسخة من مجلده داخل DroidDeck. تؤدي إزالة التطبيق إلى حذف النسخة، لا الأصل.</string> + <string name="user_apps_runtime_required">ثبّت بيئة تشغيل Linux أولًا</string> + <string name="user_apps_file_gone">لم يعد الملف موجودًا</string> + <string name="user_apps_no_space">لا توجد مساحة كافية: يتطلب نسخ المجلد %1$s</string> + <string name="user_apps_copying">جارٍ نسخ %1$s</string> + <string name="user_apps_failed">تعذّرت إضافة التطبيق</string> + <string name="user_apps_github_checking">جارٍ البحث عن إصدار من %1$s</string> + <string name="user_apps_github_unreachable">تعذّرت قراءة إصدارات %1$s</string> + <string name="user_apps_github_no_appimage">لا يحتوي أي إصدار من %1$s على AppImage لـ ARM64 أو x86_64</string> + <string name="user_apps_downloading">جارٍ تنزيل %1$s</string> + <string name="user_apps_download_failed">فشل التنزيل</string> + <string name="user_apps_update_failed">تعذّر تحديث التطبيق</string> + <string name="user_apps_icon_failed">تعذّرت قراءة هذا الرمز</string> + <string name="user_apps_saved">تم حفظ %1$s</string> + <string name="user_apps_edit">تعديل</string> + <string name="user_apps_check_updates">البحث عن تحديثات</string> + <string name="user_apps_checking_updates">جارٍ البحث عن تحديثات…</string> + <string name="user_apps_update_to">التحديث إلى %1$s</string> + <string name="user_apps_up_to_date">محدَّث · %1$s</string> + <string name="user_apps_updated">تم تحديث %1$s</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">إضافة تطبيق</string> + <string name="add_app_tab_script">نص برمجي</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">ملف ‎.sh. يعمل من مجلده الخاص، بملء الشاشة ضمن gamescope.</string> + <string name="add_app_appimage_hint">ملف AppImage لـ ARM64 (aarch64)، أو لـ x86_64 يعمل عبر FEX. يُفَك ضمن بيئة تشغيل Linux مرة واحدة.</string> + <string name="add_app_github_hint">يثبّت ملف AppImage لـ ARM64 من أحدث إصدار في المستودع، أو ملف x86_64 عبر FEX.</string> + <string name="app_fex_title">استخدام FEX</string> + <string name="app_fex_auto">تلقائي</string> + <string name="app_fex_on">تشغيل</string> + <string name="app_fex_off">إيقاف</string> + <string name="app_fex_hint">يشغّل FEX برامج Linux المخصصة لـ x86 على جهاز ARM64 هذا. يستخدمه الخيار «تلقائي» عندما يكون البرنامج مبنيًا لـ x86.</string> + <string name="app_fex_x86">%1$s عبر FEX</string> + <string name="app_fex_not_ready">هذا البرنامج مبني لأجهزة كمبيوتر %1$s ويعمل عبر FEX، الذي لم يتم إعداده بعد. افتح Steam مرة واحدة أثناء تسجيل الدخول: سيجعله DroidDeck يُنزّل FEX وSteam Linux Runtime اللازمة، ثم يُفتح هذا البرنامج.</string> + <string name="session_end_program">هذا البرنامج</string> + <string name="session_end_fex_missing">%1$s مبني لأجهزة كمبيوتر x86 ويعمل عبر FEX، الذي لم يتم تثبيته بعد. شغّل Steam مرة واحدة أثناء تسجيل الدخول ودعه يُنزّل FEX وSteam Linux Runtime، ثم شغّل هذا البرنامج مجددًا.</string> + <string name="add_app_flatpak_hint">معرّف تطبيق على Flathub أو صفحته على Flathub. يُعَدّ Flatpak أولًا إن لم يكن مُعدًّا بعد.</string> + <string name="add_app_file">الملف</string> + <string name="add_app_no_file">لم يتم اختيار ملف</string> + <string name="add_app_choose_file">اختيار ملف</string> + <string name="add_app_change_file">تغيير</string> + <string name="add_app_pick_script">اختر نصًا برمجيًا</string> + <string name="add_app_pick_appimage">اختر ملف AppImage</string> + <string name="add_app_pick_icon">اختر رمزًا</string> + <string name="add_app_repository">المستودع</string> + <string name="add_app_repository_placeholder">owner/repo أو رابط github.com</string> + <string name="add_app_repository_invalid">ليس مستودع GitHub</string> + <string name="add_app_flatpak_id">معرّف التطبيق</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">ليس معرّف تطبيق على Flathub</string> + <string name="add_app_flatpak_bundle_hint">أو ملف ‎.flatpak لتطبيق غير متوفر على Flathub.</string> + <string name="add_app_pick_flatpak">اختر ملف ‎.flatpak</string> + <string name="add_app_source">المصدر</string> + <string name="add_app_appearance">على سطح المكتب</string> + <string name="add_app_preview_name">تطبيق جديد</string> + <string name="add_app_name">الاسم</string> + <string name="add_app_name_placeholder">من التطبيق</string> + <string name="add_app_icon">الرمز</string> + <string name="add_app_icon_default">الرمز الافتراضي</string> + <string name="add_app_choose_icon">اختيار رمز</string> + <string name="add_app_clear_icon">مسح</string> + <string name="add_app_icon_suggestions">من المستودع</string> + <string name="add_app_icon_searching">جارٍ البحث عن رموز…</string> + <string name="add_app_icon_reset">الافتراضي</string> + <string name="edit_app_title">تعديل %1$s</string> + <string name="edit_app_save">حفظ</string> + <string name="add_app_checking_folder">جارٍ فحص المجلد…</string> + <string name="add_app_folder_linked">مجلده مرتبط بالجلسة: %1$s</string> + <string name="add_app_folder_copied">يحتوي مجلده على برامج Linux لا يمكن تشغيلها من مساحة التخزين المشتركة، لذا يُنسخ إلى DroidDeck (%1$s).</string> + <string name="add_app_confirm">إضافة</string> + <string name="add_app_cancel">إلغاء</string> + <string name="add_app_runtime_required">ثبّت بيئة تشغيل Linux من «الإعداد» أولًا.</string> + <string name="add_app_busy">انتظر حتى ينتهي %1$s.</string> + <string name="steam_compat_label">متوافق</string> + <string name="proton_compatible_note">الإصدارات التي تحمل العلامة «%1$s» في قائمة «التوافق» في Steam تتضمن إصلاحات البادئة من DroidDeck وتشغّل الألعاب هنا. الإصدار الافتراضي هو Proton Experimental ARM64 (%1$s)، أو Proton 11.0 ARM64 (%1$s) بعد تثبيت هذا الإصدار الأحدث. لا يمكن لإدخالات Proton الخاصة بـ Valve التي لا تحمل العلامة تشغيل أي لعبة على هذا الجهاز.</string> + <string name="proton_installed_compatible">مثبّت %1$s · %2$s</string> + <string name="common_cancel">إلغاء</string> + <string name="common_auto">تلقائي</string> + <string name="common_never">أبدًا</string> + <string name="common_always">دائمًا</string> + <string name="common_uninstall">إلغاء التثبيت</string> + <string name="common_check">تحقق</string> + <string name="common_update">تحديث</string> + <string name="common_checking">جارٍ التحقق…</string> + <string name="common_working">جارٍ التنفيذ…</string> + <string name="common_forget">نسيان</string> + <string name="common_add_ellipsis">إضافة…</string> + <string name="common_use">استخدام</string> + <string name="common_applies_next_session">يُطبَّق في الجلسة التالية.</string> + <string name="mode_steam_title">جلسة Steam</string> + <string name="mode_desktop_title">جلسة سطح المكتب</string> + <string name="mode_display">الشاشة</string> + <string name="mode_resolution">دقة الشاشة</string> + <string name="mode_res_720">حتى 720p</string> + <string name="mode_res_900">حتى 900p</string> + <string name="mode_res_1080">حتى 1080p</string> + <string name="mode_res_native">الأصلية</string> + <string name="mode_res_default">%1$s (افتراضي)</string> + <string name="mode_res_custom_value">مخصّصة · %1$d×%2$d</string> + <string name="mode_res_custom">مخصّصة…</string> + <string name="mode_res_note">قد تجعل دقة 720p القوائم أسرع استجابة.</string> + <string name="mode_ratio">نسبة أبعاد الشاشة</string> + <string name="mode_ratio_custom">تحددها الدقة المخصّصة.</string> + <string name="mode_ratio_auto">يستخدم الخيار «تلقائي» نسبة 16:9 على الأقل.</string> + <string name="mode_fps">حد الإطارات</string> + <string name="mode_fps_note">يحدّ الجلسة بأكملها. تنتقل الشاشة إلى معدل تحديث يكون من مضاعفات الحد.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">إخراج HDR10</string> + <string name="mode_hdr_unavailable">غير متاح: %1$s.</string> + <string name="mode_hdr_restart">أعد تشغيل التطبيق لتطبيق التغيير.</string> + <string name="mode_drivers">برامج التشغيل</string> + <string name="mode_gpu_drivers">برامج تشغيل GPU</string> + <string name="mode_gpu_drivers_hint">مشتركة بين Steam وسطح المكتب. يتم ضبطها في صفحة «المكوّنات».</string> + <string name="mode_touch_controls">اللمس وعناصر التحكم</string> + <string name="mode_touch">اللمس</string> + <string name="mode_touch_touchpad">لوحة اللمس</string> + <string name="mode_touch_direct">مباشر</string> + <string name="mode_touch_note">تلقائي: لوحة لمس على سطح المكتب، ولمس مباشر في Steam.</string> + <string name="mode_osc">عناصر التحكم على الشاشة</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">يعرض الخيار «تلقائي» عناصر التحكم عند عدم توصيل وحدة تحكم.</string> + <string name="osc_steam_touch">لمس Steam (تجريبي)</string> + <string name="osc_style_next_session">تبدأ عناصر التحكم باللمس من Steam مع جلسة Steam التالية</string> + <string name="osc_steam_layout">تخطيط لمس Steam</string> + <string name="osc_steam_layout_hint">انقل عناصر التحكم باللمس لهذه اللعبة أو غيّر حجمها أو أخفها؛ ويتم الحفظ في Steam</string> + <string name="mode_controller">وحدة التحكم</string> + <string name="mode_controller_hint">كيف يرى Steam وحدة التحكم لديك. يُطبَّق في الجلسة التالية.</string> + <string name="mode_controller_deck">وحدة تحكم Steam Deck</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">وحدة تحكم Xbox 360</string> + <string name="mode_controller_note">Deck: زر «الوصول السريع» والجيروسكوب. Steam Controller: الشيء نفسه، مع استشعار القبضة على الأزرار الخلفية. DualShock 4 وDualSense: الجيروسكوب، ولوحتا التتبع كلوحة اللمس الخاصة بها. Xbox 360: افتح «الوصول السريع» باستخدام Guide+A.</string> + <string name="mode_back">زر الرجوع</string> + <string name="back_menu_then_qam">القائمة ← الوصول السريع</string> + <string name="back_qam_then_menu">الوصول السريع ← القائمة</string> + <string name="mode_session">الجلسة</string> + <string name="mode_suspend">السلوك في الخلفية</string> + <string name="mode_suspend_hint">عند إخفاء التطبيق أو انطفاء الشاشة.</string> + <string name="mode_suspend_manual">يدوي</string> + <string name="mode_suspend_native">أصلي</string> + <string name="mode_suspend_steam_note">«تلقائي» يوقف مؤقتًا ويستأنف عند الظهور. «أصلي» يجهّز Steam للسكون أولًا. «يدوي» ينتظر الضغط على «استئناف». «أبدًا» يُبقيه قيد التشغيل.</string> + <string name="mode_background_downloads">مواصلة التنزيلات</string> + <string name="mode_background_downloads_hint">تأخير التعليق في الخلفية أثناء قيام Steam بالتنزيل.</string> + <string name="mode_suspend_note">«تلقائي» يوقف مؤقتًا في الخلفية ويستأنف عند الظهور. «يدوي» ينتظر الضغط على «استئناف». «أبدًا» يُبقيه قيد التشغيل.</string> + <string name="mode_startup">بدء التشغيل</string> + <string name="mode_steam_startup">تشغيل Steam مع DroidDeck</string> + <string name="mode_steam_startup_hint">يفتح جلسة Steam عند تشغيل DroidDeck.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">جارٍ البحث عن إصدارات…</string> + <string name="mode_decky_ready">جاهز لتثبيت %1$s.</string> + <string name="mode_decky_none">لم يتم العثور على إصدار متوافق. حاول مرة أخرى لاحقًا.</string> + <string name="mode_decky_installed">مثبّت · %1$s</string> + <string name="mode_decky_update">يتوفر تحديث · %1$s</string> + <string name="mode_decky_current">محدَّث · %1$s</string> + <string name="mode_decky_loader">المُحمِّل</string> + <string name="mode_decky_install_latest">تثبيت أحدث إصدار</string> + <string name="mode_decky_on">يبدأ مع Steam. أثناء تفعيله، قد تتمكن التطبيقات الأخرى على هذا الجهاز من التحكم في Steam.</string> + <string name="mode_decky_off">متوقف. يبقى منفذ تصحيح الأخطاء عن بُعد في Steam مغلقًا.</string> + <string name="mode_decky_remove_title">هل تريد إلغاء تثبيت Decky Loader؟</string> + <string name="mode_decky_remove_text">يزيل المُحمِّل. يتم الاحتفاظ بالمكونات الإضافية والإعدادات.</string> + <string name="mode_client">العميل</string> + <string name="mode_mangoapp">تراكب الأداء</string> + <string name="mode_mangoapp_hint">أوقفه إذا تعطلت الألعاب أو أصبحت شاشتها سوداء في وضع Deck. يُطبَّق في الجلسة التالية.</string> + <string name="mode_steam_repair">إصلاح عميل Steam</string> + <string name="mode_steam_repair_hint">يُنزّل عميل Steam مجددًا في المرة التالية التي يبدأ فيها Steam. استخدمه إذا بقي Steam على شاشة التحميل. يتم الاحتفاظ بالألعاب وتسجيل الدخول والإعدادات.</string> + <string name="mode_steam_repair_queued">سيتم تنزيل عميل Steam مجددًا في المرة التالية التي يبدأ فيها Steam.</string> + <string name="mode_steam_repair_button">إصلاح</string> + <string name="mode_added_art">الصور الفنية</string> + <string name="mode_sgdb_key">مفتاح API لـ SteamGridDB</string> + <string name="mode_sgdb_key_yours">مفتاحك</string> + <string name="mode_sgdb_edit">تعديل…</string> + <string name="mode_added_art_hint">يملأ الصور الناقصة من Steam، ثم من SteamGridDB. تكون الأولوية لملفات cover.jpg أو hero.jpg أو logo.png أو icon.png الخاصة بك في مجلد اللعبة.</string> + <string name="summary_remove_game">إزالة %1$s</string> + <string name="mode_added_remove_confirm">إزالة؟</string> + <string name="games_add">إضافة لعبة</string> + <string name="games_add_title">إضافة لعبة:</string> + <string name="games_pick_exe_title">اختر ملف exe:</string> + <string name="games_add_unreachable">هذا الموقع غير متاح في الجلسة</string> + <string name="mode_added_exe_check">فحص ملف exe هذا</string> + <string name="wincomp_title">مكوّنات Windows</string> + <string name="wincomp_pad_hint">يتنقل LB / RB بين الأقسام.</string> + <string name="wincomp_card_none">لا شيء</string> + <string name="wincomp_applies">ملفات DLL لنظام Windows لا يتضمنها Proton، من قائمة المكوّنات. فعّل ما تحتاجه هذه اللعبة منها: توضع في مجلد Windows الخاص باللعبة، وليس في ملفاتها أبدًا، وتبقى مهما كان إصدار Proton الذي تعمل عليه.</string> + <string name="wincomp_offline">تعذّر الوصول إلى قائمة المكوّنات. لا يزال بالإمكان تبديل المكوّنات التي تم تنزيلها بالفعل.</string> + <string name="wincomp_recommended">موصى بها لهذه اللعبة</string> + <string name="wincomp_checking">جارٍ التحقق مما تحتاجه هذه اللعبة…</string> + <string name="wincomp_when_online">متاح عند الاتصال بالإنترنت</string> + <plurals name="wincomp_turning_on"> + <item quantity="zero">جارٍ تفعيل %1$d مكوّن…</item> + <item quantity="one">جارٍ تفعيل مكوّن واحد…</item> + <item quantity="two">جارٍ تفعيل مكوّنين…</item> + <item quantity="few">جارٍ تفعيل %1$d مكوّنات…</item> + <item quantity="many">جارٍ تفعيل %1$d مكوّنًا…</item> + <item quantity="other">جارٍ تفعيل %1$d مكوّن…</item> + </plurals> + <string name="wincomp_auto_section">مفعّلة لهذه اللعبة</string> + <string name="wincomp_auto_steam">يثبّت Steam %1$s مع %2$s (التطبيق %3$s)</string> + <string name="wincomp_auto_gog">من قائمة GOG لهذه اللعبة</string> + <string name="wincomp_auto_epic">من متطلبات Epic المسبقة لهذه اللعبة</string> + <string name="wincomp_auto_amazon">من خطوات التثبيت في Amazon لهذه اللعبة</string> + <string name="wincomp_steam_by_name">يثبّت Steam %1$s مع التطبيق %2$s، بالمطابقة حسب الاسم</string> + <string name="wincomp_suggestions">مُدرجة أيضًا</string> + <string name="wincomp_suggestion_reason">مُدرج من %1$s؛ ليس مكوّنًا هنا</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">تأتي اللعبة مع %1$s</string> + <string name="wincomp_found_shipped">تحتوي اللعبة على %1$s (غالبًا يعمل كما هو)</string> + <string name="wincomp_already_here">موجود بالفعل في هذه اللعبة</string> + <string name="wincomp_no_recommendation">لا شيء في مجلد اللعبة يشير إلى مكوّن.</string> + <string name="wincomp_active">مثبّتة لهذه اللعبة (%1$d)</string> + <string name="wincomp_all">جميع المكوّنات (%1$d)</string> + <string name="wincomp_installers">تتطلب مثبّت Windows (%1$d)</string> + <string name="wincomp_installers_note">هذه برامج إعداد مستقلة لا تحتوي على حزمة يمكن تفكيكها، ولا يوجد بعد تسجيل لما تثبّته. معظمها بيئات تشغيل قديمة يحل محلها بالفعل Mono المضمّن في Proton، وتُضاف التسجيلات عند إنشائها.</string> + <string name="wincomp_show">إظهار</string> + <string name="wincomp_hide">إخفاء</string> + <string name="wincomp_needs_installer">يشغّل برنامج الإعداد الخاص به (لا يوجد تسجيل بعد)</string> + <string name="wincomp_unsupported">غير مدعوم هنا بعد</string> + <string name="wincomp_loading">جارٍ تحميل المكوّنات…</string> + <string name="wincomp_failed">تعذّر التثبيت: %1$s</string> + <string name="wincomp_downloaded">تم التنزيل</string> + <string name="wincomp_downloading">جارٍ تنزيل %1$s</string> + <string name="wincomp_installing">جارٍ التثبيت: %1$s</string> + <string name="wincomp_reverting">جارٍ التعطيل…</string> + <string name="wincomp_download_size">تنزيل بحجم %1$s</string> + <string name="wincomp_next_launch">يسري مفعوله في المرة التالية التي تبدأ فيها اللعبة.</string> + <string name="mode_stretch169">تمديد ألعاب 16:9</string> + <string name="mode_stretch169_hint">على الشاشات الأقرب إلى الشكل المربع من 16:9، يمدّ الألعاب لملء الشاشة بدلًا من عرض أشرطة. يُطبَّق في الجلسة التالية.</string> + <string name="mode_audio">الصوت</string> + <string name="mode_directaudio">DirectAudio للألعاب</string> + <string name="mode_directaudio_hint">يتجاوز PulseAudio لتقليل زمن الاستجابة.</string> + <string name="mode_mic">الميكروفون</string> + <string name="mode_mic_hint">للدردشة الصوتية.</string> + <string name="mode_network">الشبكة</string> + <string name="mode_wifi_discovery">اكتشاف Wi-Fi</string> + <string name="mode_wifi_discovery_hint">عرض أسماء شبكات Wi-Fi والشبكات القريبة في Steam.</string> + <string name="mode_wifi_location_off">خدمات الموقع متوقفة.</string> + <string name="mode_wifi_permission_denied">لم يتم منح إذن الموقع.</string> + <string name="mode_wifi_open_settings">فتح الإعدادات</string> + <string name="mode_wifi_continue">متابعة</string> + <string name="mode_wifi_explain_title">هل تريد تفعيل اكتشاف Wi-Fi؟</string> + <string name="mode_wifi_explain_text">يتطلب Android إذن الموقع لقراءة أسماء شبكات Wi-Fi واكتشاف الشبكات القريبة. يستخدم DroidDeck هذا الإذن لعرض تلك الشبكات في Steam. نحن لا نتتبع موقعك. يعمل الوصول إلى الإنترنت بدون هذا الإذن.</string> + <string name="mode_storage">تخزين الألعاب</string> + <string name="mode_storage_auto">تلقائي - بطاقة SD عند توفرها، بالإضافة إلى مجلدات Games لديك</string> + <string name="mode_storage_auto_none">تلقائي - مجلدات Games لديك، بالإضافة إلى بطاقة SD عند توفرها (غير موجودة حاليًا)</string> + <string name="mode_storage_internal">مساحة التخزين الداخلية فقط</string> + <string name="mode_storage_folder">المجلد: %1$s</string> + <string name="mode_second_library">مكتبة ثانية</string> + <string name="mode_storage_note">قد تتقطع الألعاب التي تحمّل ملفاتها باستمرار من بطاقة SD أو من مساحة التخزين المشتركة.</string> + <string name="mode_storage_diagnostics">تشخيص التخزين</string> + <string name="mode_storage_diagnostics_hint">يسجّل نتائج تخصيص المساحة وعينات من توقيتات التخزين في «مشاركة السجلات». قد يبطئ عمليات التثبيت أثناء تفعيله. يُطبَّق في الجلسة التالية.</string> + <string name="mode_choose_folder">اختيار مجلد…</string> + <string name="mode_custom_res_title">دقة مخصّصة</string> + <string name="mode_custom_res_text">تحل محل الحد والنسبة. يُعرض الحجم غير المطابق للشاشة مع أشرطة.</string> + <string name="mode_width">العرض</string> + <string name="mode_height">الارتفاع</string> + <string name="mode_custom_res_range">بين ‎320×240‎ و‎3840×2160‎.</string> + <plurals name="mode_added_count"> + <item quantity="zero">%1$d لعبة</item> + <item quantity="one">لعبة واحدة</item> + <item quantity="two">لعبتان</item> + <item quantity="few">%1$d ألعاب</item> + <item quantity="many">%1$d لعبة</item> + <item quantity="other">%1$d لعبة</item> + </plurals> + <string name="common_ok">حسنًا</string> + <string name="common_delete">حذف</string> + <string name="common_restore">استعادة</string> + <string name="common_download">تنزيل</string> + <string name="common_delete_named">حذف %1$s</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">برنامج تشغيل بيئة التشغيل</string> + <string name="comp_runtime_driver_hint">يستخدمه Steam والألعاب وسطح المكتب. يُطبَّق في الجلسة التالية.</string> + <string name="comp_import_turnip">استيراد ملف zip لـ Turnip…</string> + <string name="comp_display_driver">برنامج تشغيل العرض</string> + <string name="comp_display_driver_hint">يرسم الإطارات على الشاشة في كلا الوضعين. أعد تشغيل التطبيق لتطبيق التغيير.</string> + <string name="comp_import_adrenotools">استيراد ملف zip لـ AdrenoTools…</string> + <string name="comp_never">لم يتم بعد</string> + <string name="comp_nightlies_checked">آخر فحص للإصدارات الليلية: %1$s</string> + <string name="comp_title">المكوّنات</string> + <string name="comp_about">حول المكوّنات</string> + <string name="comp_check_drivers">البحث عن برامج تشغيل جديدة</string> + <string name="comp_check_nightlies">البحث عن حزم جديدة في الإصدارات الليلية</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">يشغّل Proton المختار هنا كل لعبة لم تختر إصدارها الخاص في Steam، ويظهر هنا أيضًا أي تغيير يتم في Steam. يستبدل Steam إصدار Proton من Valve عند تحديثه، ويُحتفظ بالملفات الأصلية لكل إصدار.</string> + <string name="comp_scope">التهيئة</string> + <string name="comp_profile_configure">تهيئة ملف التعريف</string> + <string name="comp_profile">ملف التعريف</string> + <string name="comp_scope_general">ملف التعريف الافتراضي</string> + <string name="comp_scope_general_detail">تستخدمه تلقائيًا الألعاب التي ليست لها تجاوزات.</string> + <string name="comp_profile_game_detail">إعدادات محفوظة تُستخدم تلقائيًا عند تشغيل %1$s.</string> + <string name="comp_game_hint">اختر فقط الإعدادات التي تحتاجها هذه اللعبة. يواصل كل ما عداها استخدام ملف التعريف الافتراضي.</string> + <string name="comp_game_fex">إعداد FEX المسبق</string> + <string name="comp_game_fex_inherited">يتم استخدام الإعداد المسبق لملف التعريف الافتراضي.</string> + <string name="comp_game_fex_override">تم حفظ إعداد مسبق مخصّص لهذه اللعبة.</string> + <string name="comp_game_fex_custom">متغيرات FEX المخصّصة</string> + <string name="comp_game_use_general">استخدام الافتراضي</string> + <string name="comp_game_compatibility">توافق اللعبة</string> + <string name="comp_game_environment_hint">خيارات البيئة لـ Proton وDXVK وVKD3D والمزامنة لهذه اللعبة.</string> + <string name="comp_game_proton_inherited">يتم استخدام Proton من ملف التعريف الافتراضي.</string> + <string name="comp_game_proton_override">تم حفظ Proton مخصّص لهذه اللعبة عبر تعيين التوافق في Steam.</string> + <string name="comp_no_proton_short">بدون Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">يستخدم حزمة ملف التعريف الافتراضي من %1$s ما لم تحدد هذه اللعبة حزمة أخرى.</string> + <string name="comp_game_component_general">يستخدم حزمة المكوّنات من ملف التعريف الافتراضي.</string> + <string name="comp_game_component_override">يستخدم %1$s لهذه اللعبة فقط.</string> + <string name="comp_game_runtime_options">خيارات بيئة التشغيل</string> + <string name="comp_game_environment_other_hint">خيارات بيئة أخرى لكل لعبة لـ Proton وDXVK وVKD3D-Proton والمزامنة.</string> + <string name="comp_game_saved_next">تم حفظ ملف تعريف %1$s للتشغيل التالي للعبة.</string> + <string name="game_no_steam_title">بدء اللعبة بدون Steam</string> + <string name="game_no_steam_detail">لعبة تُغلق فورًا: شغّلها بدون إعداد Steam الخاص بـ Proton. تكون ميزات Steam، مثل الطبقة العلوية، معطّلة لها.</string> + <string name="game_no_steam_on">تشغيل</string> + <string name="comp_game_proton_live">تم حفظ اختيار Proton لـ %1$s وهو متاح في Steam الآن.</string> + <string name="comp_game_proton_next">تم حفظ اختيار Proton لـ %1$s. أعد تشغيل Steam قبل تشغيل اللعبة.</string> + <string name="comp_game_proton_not_runnable">لا يمكن لـ %1$s استخدام Proton المحدد على هذا الجهاز.</string> + <string name="comp_game_load_failed">تعذّر تحميل ملف تعريف %1$s: %2$s</string> + <string name="comp_game_save_failed">تعذّر حفظ ملف تعريف اللعبة: %1$s</string> + <string name="comp_game_reset">إعادة تعيين ملف تعريف اللعبة</string> + <string name="comp_game_reset_title">هل تريد إعادة تعيين ملف تعريف %1$s؟</string> + <string name="comp_game_reset_body">هل تريد إزالة تجاوزات Proton وFEX وحزمة المكوّنات والبيئة وتصفية الأنسجة ومكوّنات Windows لهذه اللعبة؟ ستستخدم ملف التعريف الافتراضي.</string> + <string name="comp_game_reset_done">يستخدم %1$s الآن ملف التعريف الافتراضي.</string> + <string name="comp_game_reset_failed">تعذّرت إعادة تعيين ملف تعريف اللعبة بالكامل: %1$s</string> + <string name="comp_default_live">يشغّل %1$s الآن كل لعبة لم تختر إصدار Proton الخاص بها في Steam.</string> + <string name="comp_default_next">سيشغّل %1$s كل لعبة لم تختر إصدار Proton الخاص بها، بدءًا من المرة التالية التي يبدأ فيها Steam.</string> + <string name="comp_default_not_runnable">لا يمكن لـ %1$s تشغيل الألعاب هنا، لذا لا يمكن أن يكون الافتراضي.</string> + <string name="comp_default_failed">تعذّر حفظ Proton الافتراضي: %1$s</string> + <string name="comp_swapped">%1$s · تم تبديل %2$d</string> + <string name="comp_all_original">%1$s · الكل أصلي</string> + <string name="comp_game_running_suffix">%1$s · لعبة قيد التشغيل</string> + <string name="comp_esync_pack">%1$s · حزمة droiddeck-esync</string> + <string name="comp_esync_wanted">%1$s · لا توجد حزمة droiddeck-esync بعد</string> + <string name="comp_esync_builtin">%1$s · esync مضمّن، لا حاجة إلى حزمة</string> + <string name="comp_gpu_drivers">برامج تشغيل GPU</string> + <string name="comp_reapplied">%1$s · أُعيد تطبيقه عند التشغيل %2$s</string> + <string name="comp_next_queued">التالي: %1$s · بعد إغلاق اللعبة</string> + <string name="comp_in_use">%1$s قيد الاستخدام: %2$s</string> + <string name="comp_waits">ينتظر حتى تُغلق اللعبة</string> + <string name="comp_applies_next_game">يُطبَّق في المرة التالية التي تبدأ فيها لعبة</string> + <string name="comp_import_zip">استيراد ملف ‎.zip</string> + <string name="comp_reading">جارٍ قراءة إصدارات Proton…</string> + <string name="comp_no_proton">لم يتم تثبيت أي إصدار من Proton بعد. شغّل Steam مرة واحدة لتنزيل Proton لـ ARM64، أو أضف GE-Proton أو CachyOS أو DroidDeck-Proton من «الإعداد».</string> + <string name="comp_original">الأصلي · %1$s</string> + <string name="comp_this_build">%1$s · هذا الإصدار</string> + <string name="comp_earlier_build">%1$s · إصدار سابق</string> + <string name="comp_installed_count">مثبّتة · %1$d</string> + <string name="comp_nothing_stored">لا يوجد شيء مخزّن لـ %1$s بعد. تُحفظ ملفات Proton هذا الخاصة به كنسخة أصلية.</string> + <string name="comp_waits_long">هناك لعبة تعمل على Proton هذا، لذا سينتظر هذا حتى تُغلق.</string> + <string name="comp_applies_long">يُطبَّق في المرة التالية التي تبدأ فيها لعبة.</string> + <string name="comp_swap_title">هل تريد تبديل %1$s؟</string> + <string name="comp_swap_body">هل تريد وضع %1$s في %2$s؟ تبقى ملفاته الأصلية محفوظة. %3$s</string> + <string name="comp_restore_title">هل تريد استعادة %1$s؟</string> + <string name="comp_restore_body">هل تريد إعادة النسخة الأصلية من %1$s إلى %2$s؟ %3$s</string> + <string name="comp_delete_package_body">يزيل الحزمة من التطبيق. يمكنك تنزيلها مجددًا.</string> + <string name="comp_delete_original_title">هل تريد حذف هذه النسخة الأصلية؟</string> + <string name="comp_delete_original_body">هذه النسخة الأصلية من %1$s تعود إلى إصدار سابق من %2$s. يتم دائمًا الاحتفاظ بالنسخة الأصلية للإصدار الحالي.</string> + <string name="comp_nightlies_count">في الإصدارات الليلية · %1$d</string> + <string name="comp_refresh_to_list">حدّث لعرض الحزم في الإصدارات الليلية.</string> + <string name="comp_nothing_new">لا جديد منذ آخر فحص.</string> + <string name="comp_import_wcp">استيراد ملف ‎.wcp</string> + <string name="comp_restore_original">استعادة النسخة الأصلية لـ Proton هذا</string> + <string name="comp_restore_original_body">هل تريد إعادة %2$s الأصلي الخاص بـ %1$s؟</string> + <string name="comp_swap">تبديل</string> + <string name="comp_about_text">FEX وDXVK وVKD3D-Proton لكل إصدار من Proton يشغّل به Steam الألعاب.\n\nاختر صفًا مثبّتًا لتبديله. يُطبَّق في المرة التالية التي تبدأ فيها لعبة، أو بعد إغلاق اللعبة قيد التشغيل.\n\nيُعاد التحقق من اختياراتك قبل كل تشغيل وتُستعاد إذا غيّرها أي شيء. يُحتفظ بالملفات الأصلية لكل إصدار من Proton، لذا لا تضيع أبدًا مع تحديثات Steam.</string> + <string name="comp_tag_original">أصلي</string> + <string name="comp_tag_in_use">قيد الاستخدام</string> + <string name="comp_tag_new">جديد</string> + <string name="comp_tag_stored">مخزّن</string> + <string name="perf_title">الأداء</string> + <string name="perf_lede">أنوية CPU وإصلاحات الجلسة واسم المضيف. يُطبَّق في الجلسة التالية.</string> + <string name="perf_client_cores_group">أنوية عميل Steam</string> + <string name="perf_override">تجاوز اختيار Steam للأنوية</string> + <string name="perf_override_hint">يثبّت Steam ومساعد واجهته وgamescope على الأنوية أدناه.</string> + <string name="perf_client_cores">أنوية العميل</string> + <string name="perf_client_cores_on">الأنوية المخصّصة لـ Steam.</string> + <string name="perf_client_cores_off">فعّل التجاوز لتتمكن من الاختيار.</string> + <string name="perf_game_cores_group">أنوية الألعاب</string> + <string name="perf_game_cores">أنوية الألعاب</string> + <string name="perf_game_cores_hint">الأنوية المخصّصة للألعاب. عند تحديدها جميعًا، يوزّع Android المهام بحرية.</string> + <string name="perf_client_ui">واجهة العميل</string> + <string name="perf_glthread">GL متعدد مؤشرات الترابط</string> + <string name="perf_glthread_hint">قد يجعل قوائم Steam أسرع استجابة.</string> + <string name="perf_zink">Zink: الواصفات المؤجلة</string> + <string name="perf_zink_hint">موصى به لبرامج التشغيل التي لا تدعم مخازن الواصفات.</string> + <string name="perf_noglerror">تخطي فحوص أخطاء GL</string> + <string name="perf_noglerror_hint">يوقف التحقق من GL عند كل استدعاء.</string> + <string name="perf_gsrealtime">gamescope: طابور GPU بأولوية الوقت الفعلي</string> + <string name="perf_gsrealtime_hint">يعطي الأولوية لعمل GPU الخاص بالمُركِّب. قد يحسّن انتظام الإطارات على حساب أداء الألعاب قليلًا.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">تثبيت GPU على أعلى تردد له</string> + <string name="perf_gpuclock_hint">لـ Adreno فقط. تقطّع أقل، مع استهلاك أكبر للبطارية والحرارة.</string> + <string name="perf_fixes">إصلاحات الجلسة</string> + <string name="perf_sysmem">Turnip: التصيير عبر sysmem</string> + <string name="perf_sysmem_hint">أسرع لـ Steam ولمعظم الألعاب. مطلوب على Adreno 710/720/722.</string> + <string name="perf_xalia">تجاوز مساعد xalia في Steam</string> + <string name="perf_fastsync">droiddeck-ntsync (تجريبي)</string> + <string name="perf_fastsync_hint">بديل في مساحة المستخدم لبرنامج تشغيل ntsync في Linux، للاختبار. أثناء تفعيله، يستخدمه Proton بدلًا من droiddeck-esync وdroiddeck-fsync. يعمل مع Proton Experimental ARM64 وGE-Proton وproton-cachyos. يُطبَّق في الجلسة التالية.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync أولًا</string> + <string name="perf_fsync_hint">يشغّل خاصية fsync المدمجة في Proton على ملفات Proton الأصلية، مع توفير futex_waitv في مساحة المستخدم، فلا يحتاج إلى حزمة ويستمر في العمل لحظة تحديث Steam لـ Proton. أثناء تفعيله، تستخدمه الألعاب قبل droiddeck-esync، باستثناء القليل الذي يبقيه Proton معطّلًا لـ fsync. يُطبَّق في الجلسة التالية.</string> + <string name="perf_esync_hint">تنتظر مؤشرات ترابط Windows في اللعبة بعضها بعضًا عبر eventfd والذاكرة المشتركة، بدلًا من رحلات ذهاب وعودة عبر wineserver. يستخدم حزمة droiddeck-esync مُعدّة لإصدار Proton ذاك بالضبط؛ وتُجلَب الحزم تلقائيًا. وإلى أن تتوفر حزمة لإصدار Proton جديد، يحل droiddeck-fsync محله. يُطبَّق في الجلسة التالية.</string> + <string name="perf_xalia_hint">يعطّل مساعد التنقّل بوحدة التحكم في Proton، الذي يستهلك CPU في كل لعبة.</string> + <string name="perf_seccomp">تشغيل proot بلا seccomp</string> + <string name="perf_seccomp_hint">قد يُصلح أخطاء استدعاءات النظام (syscall) الناقصة على بعض الأنوية. قد يقلّل الأداء.</string> + <string name="perf_fastpath">مسار proot السريع</string> + <string name="perf_fastpath_needs_seccomp">يحتاج إلى مرشّح seccomp في proot.</string> + <string name="perf_fastpath_hint">عمليات بحث أسرع عن الملفات وتقطّع أقل. أوقفه إذا تعذّر على برنامج العثور على ملفاته.</string> + <string name="perf_identity">هوية الجلسة</string> + <string name="perf_hostname">اسم المضيف</string> + <string name="perf_hostname_hint">الاسم الذي يراه Steam لهذا الجهاز. اتركه خاليًا للاستعادة إلى %1$s.</string> + <string name="perf_hostname_invalid">أحرف وأرقام وشرطات داخلية فقط، حتى 63 حرفًا.</string> + <string name="perf_phantom_warning">Android مهيَّأ لإنهاء هذه الجلسة</string> + <string name="driver_installed">مثبّت</string> + <string name="driver_available">متاح للتنزيل</string> + <string name="driver_nothing_new">لا جديد منذ آخر تحقّق.</string> + <string name="driver_restore_builtin">استعادة برامج التشغيل المدمجة</string> + <string name="driver_delete_builtin">مدمج في التطبيق: هذا يزيل ملفاته غير المضغوطة ويخفيه. «استعادة برامج التشغيل المدمجة» يعيده.</string> + <string name="driver_delete_bundle">نصف حزمة Android + Linux: تُزال كلا النصفين. يرجع برنامج التشغيل قيد الاستخدام إلى الافتراضي.</string> + <string name="driver_delete_plain">تُزال ملفاته من التطبيق.</string> + <string name="driver_delete_in_use">تُزال ملفاته. إنه قيد الاستخدام، فيتولى الافتراضي مكانه.</string> + <string name="drawer_page_display">العرض</string> + <string name="drawer_page_effects">المؤثرات</string> + <string name="drawer_page_controls">عناصر التحكم</string> + <string name="drawer_page_components">المكوّنات</string> + <string name="drawer_page_controller">وحدة التحكم</string> + <string name="drawer_page_settings">الإعدادات</string> + <string name="drawer_page_desc">صفحة %1$s</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">سطح المكتب</string> + <string name="drawer_steam_menu">قائمة Steam</string> + <string name="drawer_quick_access">الوصول السريع</string> + <string name="drawer_open_qam">فتح قائمة الوصول السريع</string> + <string name="drawer_prev_page">الصفحة السابقة</string> + <string name="drawer_next_page">الصفحة التالية</string> + <string name="drawer_hud">واجهة الأداء</string> + <string name="drawer_scaling">وضع التحجيم</string> + <string name="drawer_scaling_sharpness">حدة التحجيم</string> + <string name="drawer_effects">مؤثرات الشاشة</string> + <string name="drawer_effects_hint">فوري. يستهلك وقت GPU قليلًا أثناء تفعيل أي مؤثر.</string> + <string name="drawer_look">الإعدادات المسبقة</string> + <string name="drawer_look_custom">مخصّص</string> + <string name="drawer_effects_advanced">متقدّم</string> + <string name="widgets_expanded">موسّع</string> + <string name="widgets_collapsed">مصغّر</string> + <string name="drawer_cas">الحدة (CAS)</string> + <string name="drawer_cas_level">مستوى الحدة</string> + <string name="drawer_fake_hdr">HDR مزيّف</string> + <string name="drawer_deband">إزالة التدرّج اللوني</string> + <string name="drawer_deband_strength">قوة إزالة التدرّج اللوني</string> + <string name="drawer_brightness">السطوع</string> + <string name="drawer_contrast">التباين</string> + <string name="drawer_gamma">غاما</string> + <string name="drawer_saturation">التشبّع</string> + <string name="drawer_fxaa">FXAA (تنعيم الحواف)</string> + <string name="drawer_toon">الحدود الكرتونية</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">تصفية الملمس</string> + <string name="drawer_texture_hint">ألعاب DirectX 9–11، من تشغيلها التالي.</string> + <string name="drawer_anisotropy">التصفية اللامتجانسة</string> + <string name="drawer_texture_sharpness">حدة الملمس</string> + <string name="drawer_texture_sharpness_note">انحياز mip سالب: ملمس أكثر حدة مع بعض التلألؤ. يطابق الخيار «تلقائي» مقدار تكبير الجلسة إلى الشاشة.</string> + <string name="drawer_touch_auto">تلقائي (%1$s)</string> + <string name="drawer_keyboard">لوحة المفاتيح</string> + <string name="drawer_pc_keyboard">لوحة مفاتيح الكمبيوتر</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">الشاشة الثانية</string> + <string name="drawer_second_screen_mode">الوضع</string> + <string name="drawer_second_screen_shows">يعرض</string> + <string name="drawer_android_apps">تطبيقات Android</string> + <string name="drawer_launch_app">تشغيل تطبيق</string> + <string name="common_hide">إخفاء</string> + <string name="common_show">إظهار</string> + <string name="drawer_no_apps">لا تطبيقات قابلة للتشغيل</string> + <string name="drawer_session_behavior">سلوك الجلسة</string> + <string name="drawer_suspend_hint">يُطبَّق الآن وعلى الجلسات القادمة في هذا الوضع.</string> + <string name="drawer_next_session">الجلسة التالية</string> + <string name="drawer_support">الدعم</string> + <string name="drawer_logs">سجلات الجلسة</string> + <string name="drawer_logs_hint">أرفقها بتقرير خلل</string> + <string name="drawer_share_logs">مشاركة السجلات</string> + <string name="drawer_background">الخلفية</string> + <string name="drawer_stop">إيقاف</string> + <string name="drawer_stop_title">إيقاف الجلسة؟</string> + <string name="drawer_running_now">قيد التشغيل الآن</string> + <string name="drawer_no_game">لا توجد لعبة قيد التشغيل على Proton</string> + <string name="drawer_selected_profile">ملف تعريف اللعبة</string> + <string name="drawer_selected_profile_none">تفاصيل ملف التعريف غير متاحة بعد.</string> + <string name="drawer_profile_refresh_failed_initial">تعذّر تحميل ملفات تعريف الألعاب. تتم إعادة المحاولة تلقائيًا.</string> + <string name="drawer_profile_refresh_failed_stale">تعذّر تحديث ملفات تعريف الألعاب. تُعرَض آخر بيانات متاحة؛ وتتم إعادة المحاولة تلقائيًا.</string> + <string name="drawer_profile_inspect_hint">عرض وتعديل الإعدادات المحفوظة المستخدَمة في التشغيل التالي.</string> + <string name="drawer_profile_default">استخدام ملف التعريف الافتراضي</string> + <string name="drawer_profile_custom">ملف تعريف مخصّص</string> + <string name="drawer_component_inherited">استخدام %1$s من ملف التعريف الافتراضي.</string> + <string name="drawer_game_components_note">تُحفَظ التغييرات في ملف تعريف %1$s وتُطبَّق عند تشغيل اللعبة التالي.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="zero">لا تجاوزات للعبة</item> + <item quantity="one">تجاوز واحد للعبة</item> + <item quantity="two">تجاوزان للعبة</item> + <item quantity="few">%1$d تجاوزات للعبة</item> + <item quantity="many">%1$d تجاوزًا للعبة</item> + <item quantity="other">%1$d تجاوز للعبة</item> + </plurals> + <string name="drawer_no_proton">لم يتم تثبيت أي Proton بعد.</string> + <string name="drawer_game_running">لعبة قيد التشغيل</string> + <string name="drawer_reapplied">أُعيد تطبيقه عند التشغيل %1$s</string> + <string name="drawer_original">الأصلي</string> + <string name="drawer_components_note">الاستيراد والحذف من صفحة «المكوّنات».</string> + <string name="setup_check_done">تم</string> + <string name="setup_check_warn">يحتاج إلى انتباه</string> + <string name="setup_check_busy">قيد التنفيذ</string> + <string name="setup_limit_adb_failed">تحقّق من عنوان IP والمنفذ في تصحيح الأخطاء اللاسلكي، أو أعد الإقران إذا أزال Android هذا الجهاز.</string> + <string name="setup_install">تثبيت</string> + <string name="setup_manage">إدارة</string> + <string name="setup_tab_overview">نظرة عامة</string> + <string name="setup_tab_controller">وحدة التحكم</string> + <string name="setup_tab_session">الجلسة</string> + <string name="setup_tab_launcher">المشغّل</string> + <string name="setup_title">الإعداد</string> + <string name="setup_all_set">● الكل جاهز</string> + <string name="setup_n_ready">%1$d من %2$d جاهز</string> + <string name="setup_system_check">فحص النظام</string> + <string name="setup_gpu_supported">الجهاز مدعوم</string> + <string name="setup_gpu_untested">GPU غير مُختبَر</string> + <string name="setup_gpu_unsupported">GPU غير مدعوم</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">يحتاج Steam إلى برنامج تشغيل Adreno؛ قد يعرض %1$s شاشة سوداء</string> + <string name="setup_runtime">بيئة تشغيل Linux</string> + <string name="setup_runtime_progress">%1$s · %2$d%%</string> + <string name="setup_runtime_missing">غير مثبّت · نحو 3 GB، يُثبَّت عند أول تشغيل</string> + <string name="setup_installed">مثبّت</string> + <string name="setup_runtime_update">%1$s · يتوفر تحديث</string> + <string name="setup_runtime_current">%1$s · محدَّث</string> + <string name="setup_limit">حد العمليات الفرعية</string> + <string name="setup_limit_on">مفعَّل · قد يُغلق Android تطبيق Steam. إيقافه يستغرق دقيقة.</string> + <string name="setup_limit_unset">غير مُحدَّد · قد يُغلق Android تطبيق Steam. إيقافه يستغرق دقيقة.</string> + <string name="setup_limit_unknown">تعذّر التحقّق منه · إيقافه يستغرق دقيقة.</string> + <string name="setup_fix_it">إصلاح</string> + <string name="setup_details">التفاصيل</string> + <string name="setup_dev_options">خيارات المطوّرين</string> + <string name="setup_use_wireless">استخدام تصحيح الأخطاء اللاسلكي</string> + <string name="setup_check_again">التحقّق مرة أخرى</string> + <string name="setup_limit_turn_on">تفعيل الحد</string> + <string name="setup_account">حساب Steam</string> + <string name="setup_signed_in_offline">تم تسجيل الدخول باسم %1$s · وضع عدم الاتصال</string> + <string name="setup_signed_in">تم تسجيل الدخول باسم %1$s</string> + <string name="setup_sign_in">اضغط على «تشغيل» وسجّل الدخول إلى Steam</string> + <string name="setup_tools">الأدوات</string> + <string name="setup_controller">وحدة التحكم</string> + <string name="setup_controller_unavailable">إعدادات وحدة التحكم غير متاحة.</string> + <string name="setup_session">الجلسة</string> + <string name="setup_logs">سجلات الجلسة</string> + <string name="setup_logs_hint">تُحفَظ بعد كل جلسة</string> + <string name="setup_agent_commands">أدوات تصحيح الأخطاء</string> + <string name="setup_latest_logs">أحدث سجلات الجلسة</string> + <string name="setup_saved_logs">سجلات الجلسة المحفوظة</string> + <string name="setup_saved_logs_hint">يحتفظ بأحدث %1$d</string> + <string name="setup_clear_logs">مسح السجلات</string> + <string name="setup_offline">وضع عدم الاتصال</string> + <string name="setup_sign_in_first">سجّل الدخول إلى Steam أولًا</string> + <string name="setup_launcher">المشغّل</string> + <string name="setup_language">اللغة</string> + <string name="setup_language_hint">يستخدمها Steam أيضًا، من المرة التالية التي يبدأ فيها.</string> + <string name="setup_language_system">الافتراضي للنظام (%1$s)</string> + <string name="setup_language_system_short">الافتراضي للنظام</string> + <string name="setup_app_scale">تحجيم التطبيق</string> + <string name="setup_app_scale_hint">نصوص DroidDeck وعناصر التحكم فيه</string> + <string name="setup_app_scale_default">%1$d%% (افتراضي)</string> + <string name="setup_theme">المظهر</string> + <string name="setup_theme_hint">مظهر المشغّل</string> + <string name="setup_home">استخدام كشاشة رئيسية</string> + <string name="setup_home_on">يمكن أن يكون DroidDeck تطبيق الشاشة الرئيسية</string> + <string name="setup_home_off">لا يُعرَض DroidDeck أبدًا كتطبيق للشاشة الرئيسية</string> + <string name="setup_orientation">الاتجاه</string> + <string name="setup_orientation_auto">أفقي تلقائي</string> + <string name="setup_orientation_landscape">أفقي</string> + <string name="setup_orientation_reverse">أفقي معكوس</string> + <string name="setup_fullscreen">ملء الشاشة</string> + <string name="setup_fullscreen_on">يخفي شريطي الحالة والتنقّل في Android</string> + <string name="setup_fullscreen_off">يعرض شريطي الحالة والتنقّل في Android</string> + <string name="setup_default_home">تطبيق الشاشة الرئيسية الافتراضي</string> + <string name="setup_choose_home">اختيار تطبيق للشاشة الرئيسية</string> + <string name="setup_choose">اختيار</string> + <string name="setup_linux_apps">تطبيقات Linux (تجريبي)</string> + <string name="setup_tool_files">الملفات</string> + <string name="setup_tool_files_hint">تصفّح الملفات وإدارتها</string> + <string name="setup_tool_protons">إصدارات Proton</string> + <string name="setup_tool_protons_hint">تثبيت إصدارات Proton لـ ARM64</string> + <string name="setup_tool_performance">الأداء</string> + <string name="setup_tool_performance_hint">تخصيص أنوية CPU</string> + <string name="setup_tool_roms">مجلد ROMs</string> + <string name="setup_tool_roms_hint">حيث تُخزَّن ألعاب المحاكيات</string> + <string name="setup_card_components">المكوّنات</string> + <string name="setup_card_components_hint">FEX، DXVK، VKD3D</string> + <string name="setup_card_controls">عناصر التحكم</string> + <string name="setup_card_controls_hint">تعيين الأزرار</string> + <string name="store_tab_discover">استكشاف</string> + <string name="store_tab_search">البحث</string> + <string name="store_tab_installed">المثبّتة</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">المتجر</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak جاهز</string> + <string name="store_needs_runtime">ثبّت بيئة تشغيل Linux من «الإعداد» أولًا.</string> + <string name="store_setup_title">إعداد Flatpak</string> + <string name="store_setup_text">يضيف Flatpak ويصله بـ Flathub (نحو 20 MB). يُنزِّل أول تطبيق بيئة تشغيله أيضًا.</string> + <string name="store_setting_up">جارٍ الإعداد…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">التحديثات</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d%%</string> + <string name="store_starting">جارٍ البدء…</string> + <string name="store_categories">الفئات</string> + <string name="store_unreachable">تعذّر الوصول إلى Flathub. تحقّق من اتصالك وحاول مرة أخرى.</string> + <string name="store_try_again">إعادة المحاولة</string> + <string name="store_search_label">البحث في Flathub</string> + <string name="store_search_placeholder">Firefox، ألعاب، محاكيات…</string> + <string name="store_search">بحث</string> + <string name="store_all">الكل</string> + <string name="store_searching">جارٍ البحث…</string> + <string name="store_search_hint">ابحث بالاسم أو اختر فئة. تُعرَض تطبيقات ARM64 فقط.</string> + <string name="store_no_results">لا توجد تطبيقات ARM64 على Flathub تطابق ذلك.</string> + <string name="store_results">النتائج</string> + <string name="store_check_updates">التحقّق من التحديثات</string> + <string name="store_update_all">تحديث الكل (%1$d)</string> + <string name="store_all_current">● كل شيء محدَّث</string> + <string name="store_no_apps">لا تطبيقات حتى الآن. ابحث عنها ضمن «استكشاف» أو «البحث».</string> + <string name="store_setup_first_note">أعدّ Flatpak لتثبيت التطبيقات.</string> + <string name="store_update_available_prefix">تحديث متاح · %1$s</string> + <string name="store_open">فتح</string> + <string name="store_details">التفاصيل</string> + <string name="store_remove">إزالة</string> + <string name="store_press_again">اضغط مرة أخرى للإزالة</string> + <string name="store_installing">جارٍ التثبيت…</string> + <string name="store_install">تثبيت</string> + <string name="store_setup_first">أعدّ Flatpak أولًا</string> + <string name="store_download_size">تنزيل %1$s</string> + <string name="store_installed_chip">● مثبَّت</string> + <string name="store_details_failed">تعذّر تحميل صفحة هذا التطبيق على Flathub.</string> + <string name="store_loading">جارٍ التحميل من Flathub…</string> + <string name="store_no_arm">لا يوجد إصدار ARM64 على Flathub، لذا لا يمكن تثبيته هنا.</string> + <string name="store_screenshots">لقطات الشاشة</string> + <string name="store_about">حول</string> + <string name="store_app_id">معرّف التطبيق</string> + <string name="store_license">الترخيص</string> + <string name="store_runtime">بيئة التشغيل</string> + <string name="store_installed_size">حجم التثبيت</string> + <string name="store_installed_size_value">%1$s (مع بيئة التشغيل المشتركة)</string> + <string name="store_website">الموقع الإلكتروني</string> + <string name="store_verified">موثّق</string> + <string name="upd_title">التحديثات</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">تم التحقّق %1$s</string> + <string name="upd_not_checked">لم يتم التحقّق بعد</string> + <string name="upd_perm_title">اسمح لـ DroidDeck بتحديث نفسه</string> + <string name="upd_perm_text">في الشاشة التالية، فعِّل «السماح من هذا المصدر» ثم عُد. يستمر التحديث تلقائيًا.</string> + <string name="upd_open_settings">فتح الإعدادات</string> + <string name="upd_not_now">ليس الآن</string> + <string name="upd_signed_status">توقيع مختلف</string> + <string name="upd_signed_headline">لا يمكن التحديث مباشرة</string> + <string name="upd_signed_detail">هذه النسخة غير موقَّعة بمفتاح إصدار DroidDeck، فلا يمكن تثبيت التحديثات فوقها. أزل تثبيتها للتبديل.</string> + <string name="upd_available">تحديث متاح</string> + <string name="upd_blocked_status">لا يمكن التثبيت</string> + <string name="upd_switch">جاهز للتبديل</string> + <string name="upd_ahead_status">متقدّم على المستقر</string> + <string name="upd_ahead_headline">أنت متقدّم على الإصدار المستقر</string> + <string name="upd_ahead_detail">هذا الإصدار أحدث من آخر إصدار مستقر. ستنتقل إلى الإصدار المستقر مع إصداره التالي.</string> + <string name="upd_gone_status">انتهى الاختبار</string> + <string name="upd_gone_headline">انتهى هذا الاختبار</string> + <string name="upd_gone_detail">تم دمج إصلاحه أو التخلي عنه. تابع إصدار المعاينة للحصول على أحدث الإصلاحات.</string> + <string name="upd_current_status">محدَّث</string> + <string name="upd_current_headline">لديك أحدث إصدار %1$s</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">تحديث%1$s</string> + <string name="upd_install_button">تثبيت%1$s</string> + <string name="upd_install_anyway">تثبيت الإصدار المستقر %1$s رغم ذلك</string> + <string name="upd_follow_preview">متابعة إصدار المعاينة</string> + <string name="upd_stop_session">أوقف الجلسة قيد التشغيل للتحديث.</string> + <string name="upd_less">أقل</string> + <string name="upd_more">أكثر</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">إصدار معاينة جديد</string> + <string name="upd_new_test">إصدار اختبار PR #%1$d</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">قناة التحديث</string> + <string name="upd_stable">المستقر</string> + <string name="upd_stable_hint">إصدارات مُختبَرة، تناسب معظم المستخدمين</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">المعاينة</string> + <string name="upd_preview_hint">أحدث الإصلاحات، قبل الإصدار المستقر</string> + <string name="upd_preview_current">محدَّث</string> + <string name="upd_tests">إصدارات الاختبار</string> + <string name="upd_tests_hint">جرّب إصلاحًا قبل إصداره رسميًا</string> + <string name="upd_tests_none">لا شيء حاليًا</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">لم يُبنَ لهذه النسخة من DroidDeck</string> + <string name="upd_updated">تم التحديث %1$s</string> + <string name="upd_test_name">اختبار PR #%1$d</string> + <string name="upd_while_ago">منذ فترة</string> + <string name="upd_just_now">منذ لحظات</string> + <plurals name="upd_tests_count"> + <item quantity="zero">لا شيء للتجربة</item> + <item quantity="one">واحد للتجربة</item> + <item quantity="two">اثنان للتجربة</item> + <item quantity="few">%1$d للتجربة</item> + <item quantity="many">%1$d للتجربة</item> + <item quantity="other">%1$d للتجربة</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="zero">التغييرات منذ إصدارك · غير متأخر</item> + <item quantity="one">التغييرات منذ إصدارك · متأخر بإصدار واحد</item> + <item quantity="two">التغييرات منذ إصدارك · متأخر بإصدارين</item> + <item quantity="few">التغييرات منذ إصدارك · متأخر بـ%1$d إصدارات</item> + <item quantity="many">التغييرات منذ إصدارك · متأخر بـ%1$d إصدارًا</item> + <item quantity="other">التغييرات منذ إصدارك · متأخر بـ%1$d إصدار</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="zero">آخر التغييرات · لا إصدارات جديدة</item> + <item quantity="one">آخر التغييرات · إصدار واحد</item> + <item quantity="two">آخر التغييرات · إصداران</item> + <item quantity="few">آخر التغييرات · %1$d إصدارات</item> + <item quantity="many">آخر التغييرات · %1$d إصدارًا</item> + <item quantity="other">آخر التغييرات · %1$d إصدار</item> + </plurals> + <string name="gate_title">إعداد واحد في Android يجب تغييره</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">يوقف Android التطبيقات التي تبدأ عمليات خلفية كثيرة، وSteam يبدأ عشرات منها. وإلى أن يتغيّر هذا، تُغلَق الألعاب دون أي خطأ.</string> + <string name="gate_fix_for_me">إصلاحه لي</string> + <string name="gate_toggle_steps">فعِّل «تعطيل تقييد العمليات الفرعية»، ثم عُد. اترك خيارات المطوّرين مفعّلة، وإلا سيُعاد تعطيل المفتاح.</string> + <string name="gate_open_dev">فتح خيارات المطوّرين</string> + <string name="gate_no_toggle">لا يحتوي إصدار Android هذا على مفتاح لذلك، لذا يغيّره DroidDeck عبر تصحيح الأخطاء اللاسلكي. يستغرق نحو دقيقة، ولا يحتاج إلى جهاز كمبيوتر.</string> + <string name="gate_needs_wifi">يحتاج تصحيح الأخطاء اللاسلكي إلى Wi-Fi. تعمل أي شبكة؛ فهو لا يحتاج إلى اتصال بالإنترنت.</string> + <string name="gate_needs_notifications">يظهر رمز الإقران في إشعار. فعِّل الإشعارات لـ DroidDeck، أو استخدم «طرق أخرى».</string> + <string name="gate_allow_notifications">السماح بالإشعارات</string> + <string name="gate_step1">افتح الإعدادات ← خيارات المطوّرين ← تصحيح الأخطاء اللاسلكي وفعِّله</string> + <string name="gate_step2">اضغط على «إقران جهاز برمز إقران»</string> + <string name="gate_step3">أدخل الرمز في إشعار DroidDeck</string> + <string name="gate_waiting">في انتظار نافذة الإقران المنبثقة…</string> + <string name="gate_code_needed">تم العثور على النافذة المنبثقة. أدخل رمزها في الإشعار.</string> + <string name="gate_status">الحالة</string> + <string name="gate_polling">يتم التحقّق كل ثانيتين. تُغلَق هذه الصفحة عند إيقافه.</string> + <string name="gate_other_ways">طرق أخرى</string> + <string name="gate_other_toggle">تصحيح الأخطاء اللاسلكي، عنوان يدوي، ADB من كمبيوتر</string> + <string name="gate_other_no_toggle">عنوان يدوي، ADB من كمبيوتر، خيارات المطوّرين</string> + <string name="gate_use_wireless_instead">استخدام تصحيح الأخطاء اللاسلكي بدلًا من ذلك</string> + <string name="gate_some_roms">تضيف بعض إصدارات ROM هذا المفتاح رغم ذلك. ابحث عن «العمليات الفرعية» في خيارات المطوّرين.</string> + <string name="gate_manual_hint">إذا لم ينجح الإشعار، أدخل عنوان النافذة المنبثقة ورمزها بنفسك.</string> + <string name="gate_enter_manually">إدخال العنوان يدويًا</string> + <string name="gate_from_computer">من جهاز كمبيوتر يحتوي على ADB:</string> + <string name="gate_copy_command">نسخ الأمر</string> + <string name="gate_choose_display">اختر الشاشة التي ستُفتَح فيها إعدادات Android.</string> + <string name="gate_main_screen">الشاشة الرئيسية</string> + <string name="wadb_finding_port">تم الإقران. جارٍ البحث عن منفذ تصحيح الأخطاء اللاسلكي…</string> + <string name="wadb_enter_port">تم الإقران. أدخل المنفذ من الإعدادات.</string> + <string name="wadb_applying">تم العثور على الجهاز. جارٍ تطبيق الإعداد…</string> + <string name="wadb_apply_failed">تعذّر التطبيق تلقائيًا. تحقّق من العنوان أدناه وأعد المحاولة. %1$s</string> + <string name="wadb_title_wireless">تصحيح الأخطاء اللاسلكي</string> + <string name="wadb_title_done">تم تحديث حد العمليات الفرعية</string> + <string name="wadb_title">تغيير حد العمليات الفرعية</string> + <string name="wadb_lede_compact0">إذا كان هذا الخيار غير متاح، فأقرن عبر تصحيح الأخطاء اللاسلكي.</string> + <string name="wadb_lede_compact1">تم الإقران. تحقّق من عنوان IP والمنفذ في تصحيح الأخطاء اللاسلكي.</string> + <string name="wadb_lede_done">تم تحديث حد العمليات الفرعية.</string> + <string name="wadb_lede0">استخدم الخطوة 2 إذا لم يكن في خيارات المطوّرين إعداد للعمليات الفرعية. افتح خيارات المطوّرين ← تصحيح الأخطاء اللاسلكي ← إقران جهاز برمز إقران.</string> + <string name="wadb_lede1">تم الإقران. استخدم العنوان أدناه، أو حدِّثه من تصحيح الأخطاء اللاسلكي ← عنوان IP والمنفذ.</string> + <string name="wadb_lede_done_long">تم تحديث حد العمليات الفرعية عبر تصحيح الأخطاء اللاسلكي.</string> + <string name="wadb_confirmed_on">أكّد Android أن الحد مفعّل.</string> + <string name="wadb_confirmed_off">أكّد Android أن الحد معطّل.</string> + <string name="wadb_pair_group">2 · إقران تصحيح الأخطاء اللاسلكي</string> + <string name="wadb_pair_address">عنوان IP ومنفذ النافذة المنبثقة للإقران</string> + <string name="wadb_pin">رمز الإقران (PIN)</string> + <string name="wadb_pin_placeholder">6 أرقام</string> + <string name="wadb_pair">إقران</string> + <string name="wadb_apply_group">2 · التطبيق عبر تصحيح الأخطاء اللاسلكي</string> + <string name="wadb_connect_address">عنوان IP ومنفذ الاتصال</string> + <string name="wadb_pair_again">إقران مرة أخرى</string> + <string name="wadb_turn_off">إيقاف الحد</string> + <string name="wadb_other_ways">طرق أخرى لتغييره</string> + <string name="wadb_dev_try_first">1 · خيارات المطوّرين (جرّب هذا أولًا)</string> + <string name="wadb_restrict_on">فعِّل «تقييد العمليات الفرعية».</string> + <string name="wadb_restrict_off">عطِّل «تقييد العمليات الفرعية».</string> + <string name="wadb_computer_last">3 · ADB من كمبيوتر (كخيار أخير)</string> + <string name="wadb_only_if">فقط إذا كان تصحيح الأخطاء اللاسلكي غير متاح.</string> + <string name="wadb_computer_group">3 · ADB من كمبيوتر (كخيار أخير)</string> + <string name="ctrl_osc_hint">متى تظهر عناصر التحكم باللمس في الجلسة</string> + <string name="ctrl_color">اللون</string> + <string name="ctrl_color_hint">درجة لون الأزرار على الشاشة</string> + <string name="ctrl_custom">مخصّص</string> + <string name="ctrl_opacity">العتامة</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">حجم الأزرار</string> + <string name="ctrl_size_hint" formatted="false">100% هو الحجم القياسي</string> + <string name="ctrl_stick_click">ضغط العصا</string> + <string name="ctrl_stick_click_hint">انقر مرتين مع الاستمرار على عصا للحصول على L3 أو R3</string> + <string name="ctrl_adaptive">عصي تكيّفية</string> + <string name="ctrl_rumble">اهتزاز اللعبة</string> + <string name="ctrl_steam_button">زر Steam</string> + <string name="ctrl_qam_button">زر QAM</string> + <string name="ctrl_keyboard_button">زر لوحة المفاتيح</string> + <string name="ctrl_select_steam">اضغط على Select لفتح قائمة Steam</string> + <string name="ctrl_select_steam_hint">بدلًا من View/Select.</string> + <string name="setup_animations">الرسوم المتحركة</string> + <string name="ctrl_adaptive_hint">تظهر العصي حيث تلمس وتُخفى عند الرفع</string> + <string name="ctrl_layout">التخطيط</string> + <string name="ctrl_layout_custom">تم حفظ مواضع مخصّصة</string> + <string name="ctrl_layout_default">مهيّأ لهذه الشاشة</string> + <string name="ctrl_edit">تعديل</string> + <string name="ctrl_reset">إعادة ضبط</string> + <string name="ctrl_mapping">تعيين الأزرار</string> + <string name="ctrl_mapping_default">يرسل كل زر إدخاله الخاص</string> + <string name="ctrl_mapping_count">تمت إعادة تعيين %1$d من %2$d أزرار</string> + <string name="ctrl_configure">ضبط</string> + <string name="ctrl_reset_all">إعادة ضبط وحدة التحكم</string> + <string name="ctrl_reset_all_hint">يستعيد كل إعدادات وحدة التحكم والتخطيط</string> + <string name="ctrl_mapping_lede">ما يرسله كل زر على الشاشة إلى اللعبة. يؤدي إخفاؤه إلى إزالة الزر.</string> + <string name="ctrl_onscreen_buttons">أزرار الشاشة</string> + <string name="ctrl_defaults">الافتراضية</string> + <string name="ctrl_reset_mapping">إعادة ضبط التعيين</string> + <string name="content_android_apps">تطبيقات Android</string> + <string name="content_apps_count">%1$d تطبيق</string> + <string name="content_no_android_apps">لم يتم العثور على تطبيقات Android قابلة للتشغيل.</string> + <string name="content_desktop_installed">● سطح المكتب مثبّت</string> + <string name="content_desktop_first_open">يُثبَّت عند أول فتح</string> + <string name="content_available">متاح للتثبيت</string> + <string name="content_not_installed">غير مثبّت.</string> + <string name="content_not_installed_suffix">%1$s · غير مثبّت</string> + <string name="content_open_named">فتح %1$s</string> + <string name="content_games">الألعاب</string> + <string name="content_choose_roms">اختر مجلد ROMs.</string> + <string name="content_retroarch_roms">تصفّح إلى /root/ROMs في RetroArch.</string> + <string name="content_add_roms">أضف ألعاب %1$s إلى ROMs/%2$s.</string> + <string name="content_install_named">تثبيت %1$s</string> + <string name="content_stop_to_install">أوقف الجلسة للتثبيت</string> + <string name="content_runtime_required">بيئة التشغيل مطلوبة</string> + <string name="content_loading_details">جارٍ تحميل تفاصيل التثبيت…</string> + <string name="content_details_unavailable">تفاصيل التثبيت غير متاحة. حاول مرة أخرى لاحقًا.</string> + <string name="content_needs_runtime">ثبّت بيئة تشغيل Linux من «الإعداد» أولًا.</string> + <string name="content_stop_first">أوقف الجلسة قيد التشغيل لتثبيت تطبيقات سطح المكتب.</string> + <string name="content_install_into">يثبّت %1$s في سطح مكتب Linux.</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">يؤدي إخفاؤه إلى إزالته من سطح المكتب. تبقى ملفاته في بيئة تشغيل Linux.</string> + <string name="content_rom_gone">لم تعد هذه اللعبة موجودة في مجلد ROMs.</string> + <string name="content_desktop_system">سطح المكتب · %1$s</string> + <string name="content_launch_in">تشغيل في %1$s</string> + <string name="content_also_in">أيضًا في %1$s</string> + <string name="content_page_gone">لم تعد هذه الصفحة موجودة. اختر قسمًا من القائمة على اليسار.</string> + <string name="content_open_desktop">فتح سطح المكتب</string> + <string name="content_install_open_desktop">تثبيت وفتح سطح المكتب</string> + <string name="desktop_remove_title">إلغاء تثبيت سطح المكتب</string> + <string name="desktop_remove_text">إزالة سطح المكتب والتطبيقات المرفقة به. الاحتفاظ بالألعاب والتطبيقات المُضافة والإعدادات والمكتبات المشتركة.</string> + <string name="desktop_removing">جارٍ إزالة سطح المكتب</string> + <string name="desktop_removed">تمت إزالة سطح المكتب</string> + <string name="desktop_cleaning_legacy">جارٍ إزالة ملفات LXQt القديمة</string> + <string name="desktop_removal_unknown">حدِّث DroidDeck قبل إزالة إصدار سطح المكتب هذا.</string> + <string name="content_linux_desktop">سطح مكتب Linux</string> + <string name="content_linux_desktop_hint">KDE Plasma وFirefox ومحاكياتك</string> + <string name="games_empty">الألعاب التي تثبّتها في Steam تظهر هنا بعد الجلسة.</string> + <string name="games_play_steam">تشغيل Steam</string> + <string name="games_launch_settings">إعدادات التشغيل</string> + <string name="games_launch">تشغيل</string> + <string name="games_files">ملفات اللعبة</string> + <string name="games_prefix">بادئة Proton</string> + <string name="games_saves_looking">جارٍ البحث عن الحفظ…</string> + <string name="games_saves_none">لم يتم العثور على مجلد حفظ بالاسم. يأخذ التصدير مجلد المستخدم بالكامل.</string> + <string name="games_saves_folders">%1$d مجلدات حفظ</string> + <string name="games_saves_summary">%1$s · %2$d ملفات · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">إدارة الحفظ</string> + <string name="games_saves">حفظ اللعبة</string> + <string name="games_import_saves">استيراد الحفظ…</string> + <string name="games_export_gamehub">تصدير بصيغة GameHub المضغوطة…</string> + <string name="games_export_winlator">تصدير بصيغة Winlator المضغوطة…</string> + <string name="games_open_save_folder">فتح مجلد الحفظ</string> + <string name="games_never_played">لم يتم تشغيلها أبدًا</string> + <string name="games_runtime_busy_percent">بيئة التشغيل مشغولة · %1$d%%</string> + <string name="games_runtime_busy">بيئة التشغيل مشغولة</string> + <string name="games_runtime_first_play">تُثبَّت بيئة التشغيل عند أول تشغيل</string> + <string name="games_runtime_update">يتوفر تحديث لبيئة التشغيل</string> + <string name="games_runtime_ready">● بيئة التشغيل جاهزة</string> + <string name="games_last_played">آخر تشغيل %1$s</string> + <string name="games_internal">التخزين الداخلي</string> + <string name="games_added">لعبة مُضافة</string> + <string name="games_custom">لعبة مخصّصة</string> + <string name="games_store_storage">تخزين الألعاب</string> + <string name="gpu_auto_hint">أفضل زوج لهذا GPU، يُحافَظ على تحديثه</string> + <string name="gpu_manual_hint">اختر زوجًا بنفسك</string> + <string name="gpu_checking">جارٍ البحث عن برامج التشغيل…</string> + <string name="gpu_none_listed">لا توجد برامج تشغيل مُدرَجة حتى الآن. حدِّث للبحث عنها.</string> + <string name="gpu_hide_other">إخفاء برامج التشغيل لوحدات GPU الأخرى</string> + <string name="gpu_show_other">إظهار برامج التشغيل لوحدات GPU الأخرى</string> + <string name="gpu_advanced">متقدّم</string> + <string name="gpu_set_with_display">يُضبَط مع برنامج تشغيل العرض معًا: يحمل %1$s كلاهما.</string> + <string name="gpu_set_with_runtime">يُضبَط مع برنامج تشغيل بيئة التشغيل معًا: يحمل %1$s كلاهما.</string> + <string name="gpu_runtime_hint">يُستخدَم في الألعاب وسطح المكتب.</string> + <string name="gpu_runtime_default">الافتراضي لبيئة التشغيل</string> + <string name="gpu_display_hint">يرسم الإطارات على الشاشة. يُطبَّق بعد إعادة تشغيل DroidDeck.</string> + <string name="gpu_auto_by_gpu">تلقائي - يُحدَّد بحسب GPU</string> + <string name="gpu_manage">تنزيل أو استيراد أو حذف…</string> + <string name="gpu_in_use_caps">قيد الاستخدام</string> + <string name="gpu_builtin">برامج التشغيل المدمجة في DroidDeck</string> + <string name="gpu_matched_pair">يتم ضبط برنامجَي تشغيل بيئة التشغيل والعرض معًا كزوج متطابق.</string> + <string name="gpu_recommended_for">المُستحسَن لهذا GPU: ‏%1$s.</string> + <string name="gpu_refresh_hint">حدِّث للبحث عن برامج تشغيل لهذا GPU.</string> + <string name="gpu_recommended_caps">مُستحسَن</string> + <string name="gpu_unavailable">غير متاح</string> + <string name="load_runtime">بيئة تشغيل Linux</string> + <string name="load_runtime_detail">جارٍ تجهيز بيئة تشغيل Linux</string> + <string name="load_session">الجلسة</string> + <string name="load_session_detail">بدء تشغيل العرض والصوت</string> + <string name="load_steam">عميل Steam</string> + <string name="load_steam_detail">التحقّق من Steam وأدوات التوافق الخاصة به</string> + <string name="load_opening_steam">جارٍ فتح Steam</string> + <string name="load_opening_steam_detail">في انتظار أول إطار من Steam</string> + <string name="load_desktop">سطح المكتب</string> + <string name="load_desktop_detail">جارٍ تجهيز سطح المكتب</string> + <string name="load_opening">جارٍ الفتح</string> + <string name="load_opening_detail">في انتظار الإطار الأول</string> + <string name="load_starting_steam">جارٍ تشغيل Steam</string> + <string name="load_ended">انتهت الجلسة</string> + <string name="load_step">الخطوة %1$d من %2$d</string> + <string name="load_steam_paused">تم إيقاف Steam مؤقتًا</string> + <string name="deck_tab_grips">المقابض</string> + <string name="deck_tab_left">اللوحة اليسرى</string> + <string name="deck_tab_right">اللوحة اليمنى</string> + <string name="deck_tab_both">كلتاهما</string> + <string name="deck_tab_gamepad">يد التحكّم</string> + <string name="deck_surface_stick">عصا التحكّم</string> + <string name="deck_surface_trackpad">لوحة اللمس</string> + <string name="deck_surface_switch">التبديل بين عصا التحكّم ولوحة اللمس</string> + <string name="deck_more_controls">المزيد من عناصر تحكّم Deck</string> + <string name="deck_panels">اللوحات</string> + <string name="deck_close">إغلاق عناصر التحكّم على الشاشة الثانية</string> + <string name="deck_open_steam_menu">فتح قائمة Steam</string> + <string name="deck_left_upper">يسار · علوي</string> + <string name="deck_left_lower">يسار · سفلي</string> + <string name="deck_right_upper">يمين · علوي</string> + <string name="deck_right_lower">يمين · سفلي</string> + <string name="deck_click">نقر</string> + <string name="deck_right_trackpad">لوحة اللمس اليمنى</string> + <string name="deck_left_trackpad">لوحة اللمس اليسرى</string> + <string name="second_trackpad">لوحة اللمس</string> + <string name="second_show_keyboard">إظهار لوحة المفاتيح</string> + <string name="second_hide_keyboard">إخفاء لوحة المفاتيح</string> + <string name="second_terminal">الوحدة الطرفية لـ Linux</string> + <string name="second_terminal_not_ready">جلسة Linux ليست جاهزة للوحدة الطرفية بعد.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">الألعاب</string> + <string name="rail_desktop">سطح المكتب</string> + <string name="rail_store">المتجر</string> + <string name="rail_components">المكوّنات</string> + <string name="rail_apps">التطبيقات</string> + <string name="rail_setup">الإعداد</string> + <string name="rail_updates">التحديثات</string> + <string name="rail_percent">%1$d%%</string> + <string name="rail_resume_named">استئناف %1$s</string> + <string name="roms_text">يظهر باسم /root/ROMs في كل جلسة. مساحة التخزين الداخلية متاحة في /root/Storage.</string> + <string name="roms_none">لم يتم اختيار مجلد</string> + <string name="roms_applies">يُطبَّق في الجلسة التالية. بطاقات SD مدعومة.</string> + <string name="roms_choose">اختيار مجلد</string> + <string name="roms_change">تغيير المجلد</string> + <string name="proton_lede">ثبِّت إصدار Proton لمعالجات ARM64، ثم اختره لكل لعبة من Steam → الخصائص → التوافق.</string> + <string name="proton_stop_first">أوقِف الجلسة الجارية لتثبيت أداة توافق أو إزالتها.</string> + <string name="proton_available">الإصدارات المتاحة</string> + <string name="proton_installed">مثبَّت: %1$s</string> + <string name="proton_queued">معلَّق من طلب سابق</string> + <string name="proton_not_installed">غير مثبَّت</string> + <string name="proton_update_available">يتوفّر تحديث → %1$s</string> + <string name="proton_remove_older">إزالة الإصدارات القديمة من %1$s</string> + <string name="proton_remove_older_body">سيتم حذف %1$s. يجب تحويل الألعاب المضبوطة على هذه الإصدارات إلى إصدار Proton آخر.</string> + <string name="proton_remove_body">يجب تحويل الألعاب المضبوطة على هذا الإصدار إلى إصدار Proton آخر.</string> + <string name="proton_install_now">التثبيت الآن</string> + <string name="proton_cancel_queued">إلغاء الطلب المعلَّق</string> + <string name="pckb_android">لوحة مفاتيح Android</string> + <string name="pckb_hide">إخفاء ✕</string> + <string name="main_launch_named">تشغيل %1$s</string> + <string name="main_choose_screen">اختر شاشة. الشاشة الثانوية: %1$s.</string> + <string name="main_secondary_gone">لم تعد الشاشة الثانوية متاحة.</string> + <string name="main_primary">الشاشة الأساسية</string> + <string name="main_secondary">الشاشة الثانوية</string> + <string name="widgets_prev_tab">علامة التبويب السابقة</string> + <string name="widgets_next_tab">علامة التبويب التالية</string> + <string name="widgets_on">مفعَّل</string> + <string name="widgets_all_n">الكل %1$d</string> + <string name="widgets_none">لا شيء</string> + <string name="widgets_n_of_m">%1$d من %2$d</string> + <string name="widgets_off">غير مفعَّل</string> + <string name="screen_bottom">الشاشة السفلية</string> + <string name="game_env_applies">يُطبَّق عند تشغيل اللعبة في المرة التالية. ملف تعريف اللعبة له الأولوية على الملف المشترك.</string> + <string name="game_env_other">لعبة أخرى حسب معرّف التطبيق…</string> + <string name="game_env_done">تم</string> + <string name="game_env_default">الافتراضي</string> + <string name="game_env_off">غير مفعَّل</string> + <string name="game_env_change">تغيير…</string> + <string name="game_env_change_title">تغيير المتغيّر</string> + <string name="game_env_restore_default">العودة إلى الافتراضي</string> + <string name="game_env_eyebrow">ألعاب Proton</string> + <string name="pip_title">نافذة ضمن النافذة</string> + <string name="pip_auto">الدخول تلقائيًا إلى وضع نافذة ضمن النافذة</string> + <string name="unfolded_controls">عناصر التحكّم على الشاشة غير المطوية</string> + <string name="unfolded_controls_hint">عند فتح الجهاز، تظهر اللعبة فوق لوحتَي اللمس والمقابض الخلفية بأسلوب Steam Deck. وعند طيّه، تُعرض اللعبة بملء الشاشة.</string> + <string name="pip_suspended">معلَّقة</string> + <string name="pip_not_ready">تتوفّر ميزة نافذة ضمن النافذة عندما تصبح الجلسة جاهزة.</string> + <string name="pip_permission">اسمح بميزة نافذة ضمن النافذة لتطبيق DroidDeck في إعدادات Android.</string> + <string name="pip_unavailable">تعذّر على Android فتح نافذة ضمن النافذة.</string> + <string name="game_shortcut">اختصار</string> + <string name="game_shortcut_add">إضافة إلى الشاشة الرئيسية</string> + <string name="game_shortcut_choose">اختيار لعبة</string> + <string name="game_shortcut_scanning">جارٍ البحث عن الألعاب المثبَّتة…</string> + <string name="game_link_copy">نسخ رابط التشغيل</string> + <string name="game_link_copied">تم نسخ رابط التشغيل</string> + <string name="game_shortcut_unsupported">لا يمكن لتطبيق الشاشة الرئيسية هذا إضافة اختصارات. استخدِم «نسخ رابط التشغيل» بدلاً من ذلك.</string> + <string name="game_link_invalid">رابط تشغيل اللعبة غير صالح.</string> + <string name="game_link_missing">لم تعد هذه اللعبة في مكتبة DroidDeck.</string> + <string name="game_link_session_busy">أوقِف الجلسة الحالية قبل تشغيل لعبة Steam.</string> + <string name="game_link_failed">تعذّر إرسال اللعبة إلى Steam. حاوِل تشغيلها مرة أخرى.</string> + <string name="setup_retry_removal">إعادة محاولة الإزالة</string> + <string name="runtime_removing">جارٍ الإزالة</string> + <string name="runtime_removal_incomplete">لم تكتمل الإزالة</string> + <string name="game_file_export">تصدير ملف اللعبة</string> + <string name="game_file_sync">مزامنة كل ملفات الألعاب مع مجلد</string> + <string name="game_file_stop_sync">إيقاف مزامنة ملفات الألعاب</string> + <string name="game_file_folder">اختر مجلدًا لملفات الألعاب الخاصة بالواجهات الأمامية</string> + <string name="game_file_exported">تم حفظ ملفات الألعاب في %1$s</string> + <string name="game_frontend_files">ملفات الواجهة الأمامية</string> + + <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> + <string name="main_saves_importing">الاستيراد إلى %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="zero">لم يتم استيراد أي ملف من %2$s إلى %3$s</item> + <item quantity="one">تم استيراد ملف واحد من %2$s إلى %3$s</item> + <item quantity="two">تم استيراد ملفين من %2$s إلى %3$s</item> + <item quantity="few">تم استيراد %1$d ملفات من %2$s إلى %3$s</item> + <item quantity="many">تم استيراد %1$d ملفًا من %2$s إلى %3$s</item> + <item quantity="other">تم استيراد %1$d ملف من %2$s إلى %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="zero">لم يتم استيراد أي ملف من %2$s إلى %3$s. تم نسخ ملفات الحفظ القديمة احتياطيًا إلى Download/DroidDeck/Saves/backups</item> + <item quantity="one">تم استيراد ملف واحد من %2$s إلى %3$s. تم نسخ ملفات الحفظ القديمة احتياطيًا إلى Download/DroidDeck/Saves/backups</item> + <item quantity="two">تم استيراد ملفين من %2$s إلى %3$s. تم نسخ ملفات الحفظ القديمة احتياطيًا إلى Download/DroidDeck/Saves/backups</item> + <item quantity="few">تم استيراد %1$d ملفات من %2$s إلى %3$s. تم نسخ ملفات الحفظ القديمة احتياطيًا إلى Download/DroidDeck/Saves/backups</item> + <item quantity="many">تم استيراد %1$d ملفًا من %2$s إلى %3$s. تم نسخ ملفات الحفظ القديمة احتياطيًا إلى Download/DroidDeck/Saves/backups</item> + <item quantity="other">تم استيراد %1$d ملف من %2$s إلى %3$s. تم نسخ ملفات الحفظ القديمة احتياطيًا إلى Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">اختر ملف zip لملفات حفظ %1$s</string> + <string name="main_saves_exporting">تصدير %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="zero">لم يتم تصدير أي ملف على هيئة %2$s: %3$s</item> + <item quantity="one">تم تصدير ملف واحد على هيئة %2$s: %3$s</item> + <item quantity="two">تم تصدير ملفين على هيئة %2$s: %3$s</item> + <item quantity="few">تم تصدير %1$d ملفات على هيئة %2$s: %3$s</item> + <item quantity="many">تم تصدير %1$d ملفًا على هيئة %2$s: %3$s</item> + <item quantity="other">تم تصدير %1$d ملف على هيئة %2$s: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">اختر مكان حفظ %1$s (%2$s)</string> + <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> + <plurals name="main_logs_cleared"> + <item quantity="zero">لم يتم محو أي مجلد لسجلات الجلسات.</item> + <item quantity="one">تم محو مجلد واحد لسجلات الجلسات.</item> + <item quantity="two">تم محو مجلدين لسجلات الجلسات.</item> + <item quantity="few">تم محو %1$d مجلدات لسجلات الجلسات.</item> + <item quantity="many">تم محو %1$d مجلدًا لسجلات الجلسات.</item> + <item quantity="other">تم محو %1$d مجلد لسجلات الجلسات.</item> + </plurals> + <string name="main_wadb_pair_failed">فشل الإقران عبر تصحيح الأخطاء اللاسلكي</string> + <string name="main_wadb_command_failed">فشل أمر تصحيح الأخطاء اللاسلكي</string> + <string name="main_wadb_unavailable">تصحيح الأخطاء اللاسلكي غير متاح</string> + <string name="main_adb_copied">تم نسخ أمر ADB</string> + <string name="main_pick_roms">اختر مجلد ROMs</string> + <string name="main_non_adreno_title">وحدة GPU ليست من نوع Adreno</string> + <string name="main_non_adreno_text">يدعم Turnip وحدات GPU من نوع Adreno. على %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> + <string name="main_applied">تم التطبيق: %1$s</string> + <string name="android_app_open_failed_primary">تعذّر فتح %1$s على الشاشة الأساسية</string> + <string name="android_app_open_failed_on">تعذّر فتح %1$s على %2$s</string> + <string name="android_app_open_failed_display">تعذّر فتح %1$s على الشاشة %2$d</string> + <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> + <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> + <string name="comp_stored">تم تخزين %1$s</string> + <string name="comp_download_failed">فشل التنزيل: %1$s</string> + <string name="drivers_pick_linux">اختر برنامج تشغيل لبيئة Linux (ملف zip بلاحقة -Linux أو حزمة Android + Linux)</string> + <string name="drivers_pick_android">اختر برنامج تشغيل العرض (ملف zip من AdrenoTools أو حزمة Android + Linux)</string> + <string name="drivers_pick_any">اختر ملف zip لبرنامج تشغيل أو حزمة Android + Linux</string> + <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">اختر ملف ZIP لإضافة Decky</string> + <string name="main_storage_not_usable">غير صالح للاستخدام: %1$s</string> + <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> + + <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> + <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> + + <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> + + <string name="drivers_not_checked">لم يتم التحقّق بعد - انقر على «تحديث» للبحث عن برامج تشغيل جديدة</string> + <string name="drivers_bundle_sets_display">Android + Linux: يضبط برنامج تشغيل العرض معه</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: يضبط برنامج تشغيل بيئة Linux معه</string> + <string name="drivers_runtime_builtin">Turnip المدمج في بيئة التشغيل</string> + <string name="drivers_system_vulkan">Vulkan الخاص بالنظام: لا يوجد إصدار مضمَّن لوحدة GPU هذه</string> + <string name="drivers_bundled">%1$s (مضمَّن)</string> + <string name="drivers_manual">يدوي</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">نُشر أحد الجزأين فقط حاليًا</string> + <string name="drivers_pair_size">%1$.0f MB للاثنين</string> + <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> + <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> + <string name="drivers_switched_to">تم التبديل إلى %1$s %2$s.</string> + <string name="drivers_switched_to_restart">تم التبديل إلى %1$s %2$s. يُطبَّق برنامج تشغيل العرض بعد إعادة تشغيل DroidDeck.</string> + <string name="drivers_using">يتم استخدام %1$s %2$s.</string> + <string name="drivers_using_restart">يتم استخدام %1$s %2$s. يُطبَّق برنامج تشغيل العرض بعد إعادة تشغيل DroidDeck.</string> + <string name="drivers_not_a_driver">ليس ملف zip لبرنامج تشغيل: ليس برنامج تشغيل AdrenoTools ولا Turnip بلاحقة -Linux ولا حزمة Android + Linux</string> + <string name="drivers_import_failed">فشل الاستيراد: %1$s</string> + <string name="drivers_imported_unnamed">برنامج تشغيل مستورَد</string> + <string name="drivers_using_bundle">يتم استخدام %1$s كبرنامج تشغيل لبيئة Linux وللعرض معًا.</string> + <string name="drivers_using_bundle_restart">يتم استخدام %1$s كبرنامج تشغيل لبيئة Linux وللعرض معًا. يُطبَّق برنامج تشغيل العرض بعد إعادة تشغيل DroidDeck.</string> + <string name="drivers_deleted_bundle">تم حذف %1$s (كلا برنامجَي التشغيل)</string> + <string name="drivers_deleted">تم حذف %1$s</string> + <string name="drivers_release_status">الأحدث: %1$s · آخر تحقّق %2$s</string> + <string name="drivers_release_status_failed">الأحدث: %1$s · تعذّر الوصول إلى %2$s · آخر تحقّق %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="zero">منذ %1$d دقيقة</item> + <item quantity="one">منذ دقيقة واحدة</item> + <item quantity="two">منذ دقيقتين</item> + <item quantity="few">منذ %1$d دقائق</item> + <item quantity="many">منذ %1$d دقيقة</item> + <item quantity="other">منذ %1$d دقيقة</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="zero">منذ %1$d ساعة</item> + <item quantity="one">منذ ساعة واحدة</item> + <item quantity="two">منذ ساعتين</item> + <item quantity="few">منذ %1$d ساعات</item> + <item quantity="many">منذ %1$d ساعة</item> + <item quantity="other">منذ %1$d ساعة</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="zero">منذ %1$d يوم</item> + <item quantity="one">منذ يوم واحد</item> + <item quantity="two">منذ يومين</item> + <item quantity="few">منذ %1$d أيام</item> + <item quantity="many">منذ %1$d يومًا</item> + <item quantity="other">منذ %1$d يوم</item> + </plurals> + <string name="drivers_checking_sources">جارٍ التحقّق من Banners-Turnip وWinNative وDroidDeck…</string> + <string name="drivers_check_failed">فشل التحقّق</string> + <string name="drivers_installed_pick_either">تم تثبيت %1$s - اختره من أيٍّ من القائمتين لضبط برنامجَي التشغيل معًا</string> + <string name="drivers_installed_pick">تم تثبيت %1$s - اختره من القائمة</string> + + <string name="session_wait_removal">انتظر حتى تنتهي إزالة بيئة التشغيل</string> + <string name="session_title_game">لعبة</string> + <string name="session_touch_auto_touchpad">لوحة اللمس</string> + <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> + <string name="session_named_paused">تم إيقاف %1$s مؤقتًا</string> + <string name="session_program_paused">تم إيقاف البرنامج مؤقتًا</string> + <string name="session_starting_desktop">جارٍ تشغيل سطح المكتب</string> + <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> + <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> + <string name="session_runtime_unpacking">جارٍ فك ضغط بيئة تشغيل Linux</string> + <string name="session_desktop_downloading">جارٍ تنزيل سطح المكتب</string> + <string name="session_desktop_downloading_mb">جارٍ تنزيل سطح المكتب · %1$d من %2$d MB</string> + <string name="session_desktop_checking">جارٍ فحص سطح المكتب</string> + <string name="session_desktop_unpacking">جارٍ فك ضغط سطح المكتب</string> + <string name="session_proton_downloading">جارٍ تنزيل Proton Experimental (ARM64)</string> + <string name="session_proton_downloading_mb">جارٍ تنزيل Proton Experimental (ARM64) · %1$d من %2$d MB</string> + <string name="session_proton_checking">جارٍ فحص Proton Experimental (ARM64)</string> + <string name="session_proton_unpacking">جارٍ فك ضغط Proton Experimental (ARM64)</string> + <string name="session_runtime_catalog_unreachable">تعذّر الوصول إلى كتالوج بيئة التشغيل. تحقَّق من الاتصال واضغط على «تشغيل» مرة أخرى.</string> + <string name="session_runtime_install_failed">لم يتم تثبيت بيئة تشغيل Linux. تحقَّق من بيئة التشغيل في «الإعداد» واضغط على «تشغيل» مرة أخرى.</string> + <string name="session_desktop_install_failed">لم يتم تثبيت سطح المكتب (%1$s). تحقَّق من الاتصال واضغط على «سطح المكتب» مرة أخرى.</string> + <string name="session_ended_named">توقّف %1$s بشكل غير متوقَّع. شارِك السجلات مع بلاغ عن الخطأ، أو حاوِل مرة أخرى.</string> + <string name="session_ended_program">توقّف البرنامج بشكل غير متوقَّع. شارِك السجلات مع بلاغ عن الخطأ، أو حاوِل مرة أخرى.</string> + <string name="session_ended_desktop">توقّف سطح المكتب بشكل غير متوقَّع. شارِك السجلات مع بلاغ عن الخطأ، أو حاوِل مرة أخرى.</string> + <string name="session_ended_detail">حالة الخروج %1$d · %2$s</string> + <string name="session_ended_no_log">لا يوجد سجل</string> + <plurals name="session_hint_enosys"> + <item quantity="zero">يُظهر السجل %1$d خطأ من نوع «Function not implemented»: تسريع seccomp في proot يتسبّب في فشل أداة مساعدة على هذا الجهاز. جرِّب %2$s → «%3$s» (أو «%4$s») وابدأ من جديد.</item> + <item quantity="one">يُظهر السجل خطأً واحدًا من نوع «Function not implemented»: تسريع seccomp في proot يتسبّب في فشل أداة مساعدة على هذا الجهاز. جرِّب %2$s → «%3$s» (أو «%4$s») وابدأ من جديد.</item> + <item quantity="two">يُظهر السجل خطأين من نوع «Function not implemented»: تسريع seccomp في proot يتسبّب في فشل أداة مساعدة على هذا الجهاز. جرِّب %2$s → «%3$s» (أو «%4$s») وابدأ من جديد.</item> + <item quantity="few">يُظهر السجل %1$d أخطاء من نوع «Function not implemented»: تسريع seccomp في proot يتسبّب في فشل أداة مساعدة على هذا الجهاز. جرِّب %2$s → «%3$s» (أو «%4$s») وابدأ من جديد.</item> + <item quantity="many">يُظهر السجل %1$d خطأً من نوع «Function not implemented»: تسريع seccomp في proot يتسبّب في فشل أداة مساعدة على هذا الجهاز. جرِّب %2$s → «%3$s» (أو «%4$s») وابدأ من جديد.</item> + <item quantity="other">يُظهر السجل %1$d خطأ من نوع «Function not implemented»: تسريع seccomp في proot يتسبّب في فشل أداة مساعدة على هذا الجهاز. جرِّب %2$s → «%3$s» (أو «%4$s») وابدأ من جديد.</item> + </plurals> + + <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> + <string name="store_cat_games">الألعاب</string> + <string name="store_cat_audio_video">الصوت والفيديو</string> + <string name="store_cat_graphics">الرسومات</string> + <string name="store_cat_internet">الإنترنت</string> + <string name="store_cat_office">الأدوات المكتبية</string> + <string name="store_cat_developer">التطوير</string> + <string name="store_cat_education">التعليم</string> + <string name="store_cat_science">العلوم</string> + <string name="store_cat_system">النظام</string> + <string name="store_cat_utilities">الأدوات المساعدة</string> + <string name="store_section_popular">الشائعة</string> + <string name="store_section_trending">الرائجة</string> + <string name="store_section_new">الجديدة</string> + <string name="store_section_updated">المحدَّثة</string> + <string name="store_section_loading">جارٍ التحميل…</string> + <string name="store_flatpak_setup">إعداد Flatpak</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">تعذّر البحث عن تحديثات على Flathub</string> + <string name="content_rom_installed">مثبَّتة</string> + <string name="content_rom_folder">مجلد</string> + <string name="theme_graphite">غرافيت</string> + <string name="theme_graphite_detail">أسطح بلون الغرافيت، مع أزرق كرة الشعار لكل عنصر محدَّد أو مُركَّز عليه أو مضغوط.</string> + <string name="theme_paper">ورق على أسود</string> + <string name="theme_paper_detail">الأبيض هو اللون الأساسي، وأزرق الأيقونة هو لون الإشارة للخطوط والحلقات والنقاط.</string> + <string name="theme_icon_blue">أزرق الأيقونة</string> + <string name="theme_icon_blue_detail">خلفية سوداء ونص أبيض، والأزرق لكل عنصر محدَّد أو مُركَّز عليه أو مضغوط.</string> + <string name="theme_electric">كحلي كهربائي</string> + <string name="theme_electric_detail">أزرق أكثر سطوعًا، على أسطح تميل إلى الكحلي بدلاً من الرمادي.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">مسافة</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <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> + <string name="content_system_many">أنظمة متعددة</string> + <string name="fm_drive_internal">التخزين الداخلي</string> + <string name="fm_drive_linux">نظام Linux</string> + <string name="fm_drive_sd">بطاقة SD</string> + <string name="fm_drive_storage">التخزين</string> + <string name="fm_select">تحديد</string> + <string name="fm_properties">الخصائص</string> + <string name="fm_favorite_add">إضافة إلى المفضّلة</string> + <string name="fm_favorite_remove">إزالة من المفضّلة</string> + <string name="fm_rename">إعادة التسمية</string> + <string name="fm_copy">نسخ</string> + <string name="fm_cut">قص</string> + <string name="fm_actions">الإجراءات</string> + <string name="fm_favorites">المفضّلة</string> + <string name="fm_pin_current">تثبيت المجلد الحالي</string> + <string name="fm_no_favorites">لا توجد عناصر مفضّلة بعد - ثبِّت مجلدًا من قائمته ⋮ للعودة إليه بسرعة.</string> + <string name="fm_unpin_favorite">إزالة من المفضّلة</string> + <string name="fm_size_zero">0 بايت</string> + <string name="fm_unit_bytes">بايت</string> + <string name="fm_prop_location">الموقع</string> + <string name="fm_prop_type">النوع</string> + <string name="fm_prop_size">الحجم</string> + <string name="fm_prop_modified">تاريخ التعديل</string> + <string name="fm_type_folder">مجلد</string> + <string name="fm_type_file">ملف</string> + <string name="fm_type_ext_file">ملف %1$s</string> + <string name="fm_read_only">للقراءة فقط</string> + <string name="fm_read_only_hint">يمنع الألعاب (وWine) من استبدال هذا الملف أو حذفه.</string> + <string name="fm_read_only_failed">تعذّر تغيير «للقراءة فقط»</string> + <string name="fm_hidden">مخفي</string> + <string name="fm_hidden_unsupported">لا تدعم وحدة التخزين هذه علامة الإخفاء.</string> + <string name="fm_hidden_hint">يجعل الملف مخفيًا في Windows (سمة الإخفاء في Wine).</string> + <string name="fm_hidden_failed">تعذّر تغيير «مخفي» على وحدة التخزين هذه</string> + <string name="fm_done">تم</string> + <string name="fm_op_deleting">جارٍ الحذف...</string> + <string name="fm_delete_failed">فشل الحذف</string> + <string name="fm_op_moving">جارٍ النقل...</string> + <string name="fm_op_copying">جارٍ النسخ...</string> + <string name="fm_op_moving_item">جارٍ النقل - %1$s</string> + <string name="fm_op_copying_item">جارٍ النسخ - %1$s</string> + <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> + <string name="fm_paste_failed">اكتمل %1$d، وفشل %2$d</string> + <string name="fm_paste_skipped">اكتمل %1$d، وتم تخطّي %2$d</string> + <plurals name="fm_paste_moved"> + <item quantity="zero">لم يتم نقل أي عنصر</item> + <item quantity="one">تم نقل عنصر واحد</item> + <item quantity="two">تم نقل عنصرين</item> + <item quantity="few">تم نقل %1$d عناصر</item> + <item quantity="many">تم نقل %1$d عنصرًا</item> + <item quantity="other">تم نقل %1$d عنصر</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="zero">لم يتم نسخ أي عنصر</item> + <item quantity="one">تم نسخ عنصر واحد</item> + <item quantity="two">تم نسخ عنصرين</item> + <item quantity="few">تم نسخ %1$d عناصر</item> + <item quantity="many">تم نسخ %1$d عنصرًا</item> + <item quantity="other">تم نسخ %1$d عنصر</item> + </plurals> + <string name="fm_already_exists">«%1$s» موجود بالفعل</string> + <string name="fm_op_renaming">جارٍ إعادة التسمية...</string> + <string name="fm_rename_failed">فشلت إعادة التسمية</string> + <string name="fm_op_creating_folder">جارٍ إنشاء المجلد...</string> + <string name="fm_create_folder_failed">تعذّر إنشاء المجلد</string> + <string name="fm_new_folder">مجلد جديد</string> + <string name="fm_folder_name">اسم المجلد</string> + <string name="fm_create">إنشاء</string> + <string name="fm_new_name">الاسم الجديد</string> + <string name="fm_delete_title">هل تريد الحذف؟</string> + <string name="fm_delete_text">هل تريد حذف «%1$s» نهائيًا؟</string> + <plurals name="fm_delete_items_title"> + <item quantity="zero">هل تريد حذف %1$d عنصر؟</item> + <item quantity="one">هل تريد حذف عنصر واحد؟</item> + <item quantity="two">هل تريد حذف عنصرين؟</item> + <item quantity="few">هل تريد حذف %1$d عناصر؟</item> + <item quantity="many">هل تريد حذف %1$d عنصرًا؟</item> + <item quantity="other">هل تريد حذف %1$d عنصر؟</item> + </plurals> + <string name="fm_cannot_undo">لا يمكن التراجع عن هذا الإجراء.</string> + <plurals name="fm_and_more"> + <item quantity="zero">…و%1$d عنصر آخر</item> + <item quantity="one">…وعنصر آخر</item> + <item quantity="two">…وعنصران آخران</item> + <item quantity="few">…و%1$d عناصر أخرى</item> + <item quantity="many">…و%1$d عنصرًا آخر</item> + <item quantity="other">…و%1$d عنصر آخر</item> + </plurals> + <string name="fm_op_deleting_batch">جارٍ الحذف %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="zero">تعذّر حذف %1$d عنصر</item> + <item quantity="one">تعذّر حذف عنصر واحد</item> + <item quantity="two">تعذّر حذف عنصرين</item> + <item quantity="few">تعذّر حذف %1$d عناصر</item> + <item quantity="many">تعذّر حذف %1$d عنصرًا</item> + <item quantity="other">تعذّر حذف %1$d عنصر</item> + </plurals> + <string name="fm_conflict_merge_hint">«دمج» يضيف الملفات ويستبدلها داخل المجلد الموجود.</string> + <string name="fm_conflict_overwrite_hint">«استبدال» يحلّ محلّ الملف الموجود.</string> + <string name="fm_conflict_apply_all">التطبيق على كل التعارضات</string> + <string name="fm_conflict_merge">دمج</string> + <string name="fm_conflict_overwrite">استبدال</string> + <string name="fm_conflict_keep_both">الاحتفاظ بالاثنين</string> + <string name="fm_conflict_skip">تخطّي</string> + <string name="fm_select_this_folder">اختيار هذا المجلد</string> + <string name="fm_back">رجوع</string> + <string name="fm_drive_not_readable">تم تركيب %1$s لكن تتعذّر قراءته حاليًا</string> + <string name="fm_list_view">عرض القائمة</string> + <string name="fm_grid_view">عرض الشبكة</string> + <string name="fm_search">بحث</string> + <string name="fm_sort">ترتيب</string> + <string name="fm_sort_name">الاسم</string> + <string name="fm_sort_date">تاريخ التعديل</string> + <string name="fm_rows_comfortable">صفوف واسعة</string> + <string name="fm_rows_compact">صفوف مضغوطة</string> + <string name="fm_hide_hidden">إخفاء الملفات المخفية</string> + <string name="fm_show_hidden">إظهار الملفات المخفية</string> + <string name="fm_hide_favorites">إخفاء المفضّلة</string> + <string name="fm_show_favorites">إظهار المفضّلة</string> + <string name="fm_filter_folder">تصفية هذا المجلد</string> + <string name="fm_free_space">%1$s متاحة</string> + <string name="fm_selected_count">تم تحديد %1$d</string> + <string name="fm_select_none">لا شيء</string> + <string name="fm_select_all">الكل</string> + <string name="fm_paste">لصق</string> + <plurals name="fm_items"> + <item quantity="zero">لا عناصر</item> + <item quantity="one">عنصر واحد</item> + <item quantity="two">عنصران</item> + <item quantity="few">%1$d عناصر</item> + <item quantity="many">%1$d عنصرًا</item> + <item quantity="other">%1$d عنصر</item> + </plurals> + <string name="fm_paste_here">لصق %1$s هنا</string> + <string name="fm_paste_move_here">لصق %1$s هنا (نقل)</string> + <string name="fm_rail_title">الملفات</string> + <string name="fm_rail_storage">التخزين</string> + <string name="fm_rail_quick">وصول سريع</string> + <string name="fm_rail_favorites">المفضّلة</string> + <string name="fm_rail_downloads">التنزيلات</string> + <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> + <string name="fm_favorite_added">تمت إضافة «%1$s» إلى المفضّلة</string> + <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> + <string name="ctrl_tint_steam_blue">أزرق Steam</string> + <string name="ctrl_tint_sky">سماوي</string> + <string name="ctrl_tint_white">أبيض</string> + <string name="ctrl_tint_violet">بنفسجي</string> + <string name="ctrl_tint_green">أخضر</string> + <string name="ctrl_tint_amber">كهرماني</string> + <string name="ctrl_tint_red">أحمر</string> + <string name="ctrl_tint_pink">وردي</string> + <string name="ctrl_button_a">الزر A</string> + <string name="ctrl_button_b">الزر B</string> + <string name="ctrl_button_x">الزر X</string> + <string name="ctrl_button_y">الزر Y</string> + <string name="ctrl_button_lb">الزر الكتفي الأيسر</string> + <string name="ctrl_button_rb">الزر الكتفي الأيمن</string> + <string name="ctrl_button_lt">الزناد الأيسر</string> + <string name="ctrl_button_rt">الزناد الأيمن</string> + <string name="ctrl_button_view">زر العرض</string> + <string name="ctrl_button_menu">زر القائمة</string> + <string name="ctrl_target_view">العرض</string> + <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> + <string name="ctrl_target_hidden">مخفي</string> + <string name="ctrl_editor_drag">اسحب للنقل</string> + <string name="ctrl_editor_snap">محاذاة</string> + <string name="ctrl_editor_save">حفظ</string> + <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> + + <string name="rtinst_removing">جارٍ إزالة بيئة تشغيل Linux</string> + <plurals name="rtinst_removing_entries"> + <item quantity="zero">جارٍ إزالة بيئة تشغيل Linux · %1$d عنصر</item> + <item quantity="one">جارٍ إزالة بيئة تشغيل Linux · عنصر واحد</item> + <item quantity="two">جارٍ إزالة بيئة تشغيل Linux · عنصران</item> + <item quantity="few">جارٍ إزالة بيئة تشغيل Linux · %1$d عناصر</item> + <item quantity="many">جارٍ إزالة بيئة تشغيل Linux · %1$d عنصرًا</item> + <item quantity="other">جارٍ إزالة بيئة تشغيل Linux · %1$d عنصر</item> + </plurals> + <string name="rtinst_removed">تمت إزالة بيئة تشغيل Linux</string> + <string name="rtinst_removal_failed">تعذّر إكمال إزالة بيئة تشغيل Linux. أعِد محاولة الإزالة في «الإعداد».</string> + <string name="rtinst_removing_leftovers">جارٍ إزالة بقايا بيئة التشغيل</string> + <string name="rtinst_downloading">جارٍ التنزيل</string> + <string name="rtinst_verifying">جارٍ التحقّق</string> + <string name="rtinst_extracting">جارٍ الاستخراج</string> + <string name="rtinst_channel_description">يظهر أثناء تثبيت بيئة تشغيل Linux أو إزالتها</string> + <string name="rtinst_title_installing">جارٍ تثبيت بيئة تشغيل Linux</string> + <string name="rtinst_title_removing">جارٍ إزالة بيئة تشغيل Linux</string> + + <string name="deskpkg_runtime_missing">بيئة تشغيل Linux غير مثبَّتة</string> + <string name="deskpkg_no_checksum">لا يتضمّن الكتالوج مجموعًا اختباريًا لـ %1$s</string> + <string name="deskpkg_checksum_mismatch">المجموع الاختباري غير مطابق - لم يتم تغيير أي شيء</string> + <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> + + <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> + <string name="flatpakmgr_installing">جارٍ تثبيت %1$s</string> + <string name="flatpakmgr_updating">جارٍ تحديث %1$s</string> + <string name="flatpakmgr_removing">جارٍ إزالة %1$s</string> + <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> + <string name="flatpakmgr_ref_translations">الترجمات</string> + <string name="flatpakmgr_ref_graphics_drivers">برامج تشغيل الرسومات</string> + <string name="flatpakmgr_ref_media_codecs">برامج ترميز الوسائط</string> + <string name="flatpakmgr_ref_runtime">بيئة تشغيل %1$s</string> + <string name="flatpakmgr_ref_runtime_branch">بيئة تشغيل %1$s %2$s</string> + + <string name="appimg_too_small">الملف أصغر من أن يكون AppImage</string> + <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">لا يمكن استيراد سوى ملفات AppImage من النوع 2 (هذا الملف من النوع %1$d)</string> + <string name="appimg_no_space_import">لا توجد مساحة خالية كافية: يتطلّب الاستيراد نحو %1$s</string> + <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> + <string name="appimg_extracting">جارٍ استخراج %1$s</string> + <string name="appimg_extract_failed">تعذّر استخراج ملف AppImage</string> + <string name="appimg_extract_failed_detail">تعذّر استخراج ملف AppImage: %1$s</string> + + <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> + <string name="deckymgr_checksum_download_failed">تعذّر تنزيل المجموع الاختباري للإصدار</string> + <string name="deckymgr_checksum_missing">المجموع الاختباري للإصدار غير متوفّر</string> + <string name="deckymgr_checksum_invalid">المجموع الاختباري للإصدار غير صالح</string> + <string name="deckymgr_checksum_mismatch">عدم تطابق المجموع الاختباري لـ Decky Loader</string> + <string name="deckymgr_not_elf">ملف الإصدار ليس ملفًا تنفيذيًا متوافقًا بنظام Linux 64 بت</string> + <string name="deckymgr_session_started_loader">بدأت جلسة خلال التنزيل، أوقفها قبل تثبيت Decky Loader</string> + <string name="deckymgr_loader_not_executable">تعذّر وضع علامة قابل للتنفيذ على PluginLoader</string> + <string name="deckymgr_cef_failed">تعذّر تفعيل تصحيح الأخطاء عن بُعد لـ Steam CEF</string> + <string name="deckymgr_stage_failed">تعذّر تجهيز PluginLoader</string> + <string name="deckymgr_loader_preserve_failed">تعذّر الحفاظ على PluginLoader الحالي</string> + <string name="deckymgr_loader_activate_failed">تعذّر تفعيل PluginLoader الجديد</string> + <string name="deckymgr_stop_session_plugin">أوقِف الجلسة النشطة قبل تثبيت إضافة</string> + <string name="deckymgr_loader_required">ثبِّت Decky Loader قبل استيراد إضافة</string> + <string name="deckymgr_zip_unreadable">تعذّرت قراءة ملف ZIP المحدَّد</string> + <string name="deckymgr_plugins_dir_failed">تعذّر إنشاء مجلد إضافات Decky</string> + <string name="deckymgr_extracting_zip">جارٍ استخراج ملف ZIP للإضافة</string> + <string name="deckymgr_zip_one_folder">يجب أن يحتوي ملف ZIP للإضافة على مجلد إضافة واحد في المستوى الأعلى</string> + <string name="deckymgr_zip_missing_files">ملف ZIP للإضافة يفتقد plugin.json أو package.json أو dist/index.js</string> + <string name="deckymgr_plugin_name_missing">اسم الإضافة غير متوفّر</string> + <string name="deckymgr_session_started_plugin">بدأت جلسة خلال التثبيت، أوقفها قبل تثبيت إضافة</string> + <string name="deckymgr_plugin_installed">تم تثبيت الإضافة · أعِد تشغيل Steam لتحميلها</string> + <string name="deckymgr_plugin_install_failed">فشل تثبيت ملف ZIP للإضافة</string> + <string name="deckymgr_zip_too_many">يحتوي ملف ZIP للإضافة على عدد كبير جدًا من العناصر</string> + <string name="deckymgr_zip_invalid_path">يحتوي ملف ZIP للإضافة على مسار غير صالح</string> + <string name="deckymgr_zip_duplicate_paths">يحتوي ملف ZIP للإضافة على مسارات مكرَّرة</string> + <string name="deckymgr_zip_unsafe_path">يحتوي ملف ZIP للإضافة على مسار غير آمن</string> + <string name="deckymgr_folder_failed">تعذّر إنشاء مجلد الإضافة</string> + <string name="deckymgr_zip_too_large">يتجاوز ملف ZIP للإضافة بعد الاستخراج الحدّ البالغ 1 GiB</string> + <string name="deckymgr_bundled_not_executable">تعذّر وضع علامة قابل للتنفيذ على الملف الثنائي المرفَق للإضافة</string> + <string name="deckymgr_remote_entry_invalid">عنصر remote_binary غير صالح</string> + <string name="deckymgr_remote_name_invalid">اسم الملف الثنائي البعيد غير صالح</string> + <string name="deckymgr_remote_url_https">يجب أن يستخدم رابط الملف الثنائي البعيد بروتوكول HTTPS</string> + <string name="deckymgr_remote_hash_invalid">تجزئة SHA-256 غير صالحة للملف الثنائي البعيد %1$s</string> + <string name="deckymgr_bin_dir_failed">تعذّر إنشاء مجلد bin للإضافة</string> + <string name="deckymgr_downloading_binary">جارٍ تنزيل الملف الثنائي للإضافة · %1$s</string> + <string name="deckymgr_binary_download_failed">تعذّر تنزيل الملف الثنائي للإضافة %1$s</string> + <string name="deckymgr_binary_checksum_mismatch">عدم تطابق المجموع الاختباري للملف الثنائي للإضافة · %1$s</string> + <string name="deckymgr_binary_not_executable">تعذّر وضع علامة قابل للتنفيذ على الملف الثنائي للإضافة · %1$s</string> + <string name="deckymgr_binary_replace_failed">تعذّر استبدال الملف الثنائي للإضافة · %1$s</string> + <string name="deckymgr_binary_install_failed">تعذّر تثبيت الملف الثنائي للإضافة · %1$s</string> + <string name="deckymgr_folder_taken">مجلد الإضافة يخصّ إضافة أخرى</string> + <string name="deckymgr_preserve_failed">تعذّر الحفاظ على الإضافة الحالية</string> + <string name="deckymgr_activate_failed">تعذّر تفعيل الإضافة المستورَدة</string> + <string name="deckymgr_settings_failed">تعذّر تحديث إعدادات إضافات Decky</string> + <string name="deckymgr_restore_failed">تعذّر استعادة الإضافة الحالية</string> + + <string name="adbfix_save_failed">تعذّر حفظ هوية تصحيح الأخطاء اللاسلكي</string> + <string name="adbfix_command_failed">فشل أمر ADB (%1$d)</string> + <string name="adbfix_not_confirmed">لم يؤكّد Android تغيير حدّ العمليات الفرعية: %1$s</string> + <string name="adbfix_no_pairing">لا يوجد إقران محفوظ لتصحيح الأخطاء اللاسلكي</string> + <string name="adbfix_enter_address">أدخِل عنوان IP الحالي ورقم المنفذ لتصحيح الأخطاء اللاسلكي</string> + + <string name="adbpair_timed_out">انتهت مهلة انتظار النافذة المنبثقة للإقران. حاوِل مرة أخرى من DroidDeck.</string> + <string name="adbpair_discovery_failed_code">تعذّر البحث عن النافذة المنبثقة للإقران (%1$d). تحقَّق من تفعيل Wi-Fi.</string> + <string name="adbpair_discovery_failed">تعذّر البحث عن النافذة المنبثقة للإقران: %1$s</string> + <string name="adbpair_six_digits">أدخِل الأرقام الستة كاملةً للرمز</string> + <string name="adbpair_pairing">جارٍ الإقران…</string> + <string name="adbpair_code_rejected">لم ينجح هذا الرمز. افتح النافذة المنبثقة للإقران مرة أخرى وأدخِل رمزها الجديد.</string> + <string name="adbpair_connecting">تم الإقران. جارٍ الاتصال…</string> + <string name="adbpair_port_not_found">تم الإقران، لكن لم يتم العثور على منفذ تصحيح الأخطاء اللاسلكي. اترك تصحيح الأخطاء اللاسلكي مفعَّلاً وحاوِل مرة أخرى.</string> + <string name="adbpair_applying">جارٍ تطبيق الإعداد…</string> + <string name="adbpair_command_failed">فشل أمر تصحيح الأخطاء اللاسلكي</string> + <string name="adbpair_channel_name">إعداد تصحيح الأخطاء اللاسلكي</string> + <string name="adbpair_channel_description">يطلب رمز إقران تصحيح الأخطاء اللاسلكي أثناء فتح الإعدادات</string> + <string name="adbpair_title_pair">إقران تصحيح الأخطاء اللاسلكي</string> + <string name="adbpair_waiting_text">فعِّل تصحيح الأخطاء اللاسلكي، ثم اضغط على «إقران الجهاز برمز الإقران».</string> + <string name="adbpair_waiting_long">فعِّل تصحيح الأخطاء اللاسلكي، ثم اضغط على «إقران الجهاز برمز الإقران». ابقَ في الإعدادات: يُكتب الرمز في هذا الإشعار.</string> + <string name="adbpair_code_label">رمز الإقران المكوَّن من 6 أرقام</string> + <string name="adbpair_code_hint">اضغط على «إدخال الرمز» وأدخِل الأرقام الستة الظاهرة في الإعدادات.</string> + <string name="adbpair_title_code">أدخِل رمز إقران Wi-Fi</string> + <string name="adbpair_title_retry">أعِد تجربة الرمز</string> + <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> + + <string name="selfinst_http">استجاب التنزيل بـ HTTP %1$d</string> + <string name="selfinst_size_mismatch">حجم التنزيل غير مطابق لملف APK المنشور، وتم تجاهله - حاوِل مرة أخرى</string> + <string name="selfinst_checksum_mismatch">التنزيل غير مطابق لمجموعه الاختباري، وتم تجاهله - حاوِل مرة أخرى</string> + <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> + <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> + <string name="selfinst_no_signing">ملف APK الذي تم تنزيله لا يحمل معلومات توقيع</string> + <string name="selfinst_wrong_signer">ملف APK الذي تم تنزيله غير موقَّع بمفتاح إصدار DroidDeck</string> + <string name="appupd_http">استجاب كتالوج التحديثات بـ HTTP %1$d</string> + + <string name="loadstate_starting">جارٍ بدء الجلسة…</string> + <string name="loadstate_elapsed">%1$d:%2$02d منقضية · لا تزال جارية</string> + <string name="loadstate_steam_update">جارٍ تنزيل تحديث عميل Steam · %1$d%%</string> + <string name="loadstate_steam_client">جارٍ تنزيل عميل Steam · %1$s</string> + + <string name="pextra_busy">يوجد تثبيت لأداة توافق قيد التنفيذ بالفعل</string> + <string name="pextra_stop_session">أوقِف الجلسة النشطة قبل تثبيت أدوات التوافق</string> + <string name="pextra_no_release">تعذّر العثور على إصدار ARM64 لـ %1$s</string> + <string name="pextra_no_space">يلزم توفُّر 4 GB على الأقل بالإضافة إلى حجم التنزيل لتثبيت %1$s</string> + <string name="pextra_downloading">جارٍ تنزيل %1$s %2$s</string> + <string name="pextra_retrying">جارٍ إعادة محاولة التنزيل · المحاولة %1$d من %2$d</string> + <string name="pextra_interrupted">انقطع التنزيل</string> + <string name="pextra_download_failed">فشل التنزيل؛ تم الاحتفاظ بالملف الجزئي لإتاحة استئنافه عند إعادة المحاولة</string> + <string name="pextra_too_big">تجاوز التنزيل حجم الإصدار وتم حذفه</string> + <string name="pextra_incomplete">لم يكتمل التنزيل؛ تم الاحتفاظ بالملف الجزئي لإعادة محاولة قابلة للاستئناف</string> + <string name="pextra_verifying">جارٍ التحقّق من التنزيل</string> + <string name="pextra_checksum_unreadable">تعذّرت قراءة المجموع الاختباري للإصدار؛ حاوِل مرة أخرى</string> + <string name="pextra_no_checksum">لا ينشر %1$s %2$s مجموعًا اختباريًا؛ لم يتم تثبيت أي شيء</string> + <string name="pextra_checksum_mismatch">عدم تطابق المجموع الاختباري؛ تم حذف التنزيل</string> + <string name="pextra_session_started">بدأت جلسة خلال التنزيل، أوقفها قبل تثبيت أدوات التوافق</string> + <string name="pextra_installing">جارٍ تثبيت %1$s</string> + <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> + + <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> + <string name="cmgr_not_present">غير موجود</string> + <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> + <string name="cmgr_restore_queued">سيعود %1$s في %2$s إلى أصله عند إغلاق اللعبة التي تعمل عليه.</string> + <string name="cmgr_restore_done">تمت استعادة %1$s في %2$s من أصله في %3$s.</string> + <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> + <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> + + <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> + + <string name="look_off_desc">لا يُطبَّق أي شيء.</string> + <string name="look_game_clarity">وضوح اللعبة</string> + <string name="look_game_clarity_desc">زيادة حدّة مع لمسة من التباين - الخيار اليومي.</string> + <string name="look_vivid">زاهي</string> + <string name="look_vivid_desc">ألوان مُعزَّزة بقوة، مع تفعيل إزالة التدرّج الشريطي لمنع ظهور خطوط في السماء.</string> + <string name="look_cinematic">سينمائي</string> + <string name="look_cinematic_desc">تباين أعمق، ألوان أبرد، وحواف أكثر نعومة.</string> + <string name="look_competitive">تنافسي</string> + <string name="look_competitive_desc">ظلال مرفوعة وزيادة حدّة قوية - لرؤية الأشخاص أولاً.</string> + <string name="look_adaptive_sharpen">زيادة حدّة متكيّفة</string> + <string name="look_adaptive_sharpen_desc">زيادة حدّة فقط، دون تغيير الألوان.</string> + <string name="look_filmic">فيلمي</string> + <string name="look_filmic_desc">منحنى يشبه الفيلم السينمائي، بألوان خافتة قليلاً.</string> + <string name="look_arcade">أركيد</string> + <string name="look_arcade_desc">صاخب وقوي التأثير.</string> + <string name="look_retro_crt">CRT الكلاسيكي</string> + <string name="look_retro_crt_desc">تأثير أنبوب CRT والإشارة التناظرية معًا.</string> + <string name="look_upscale_sharp">تكبير حاد</string> + <string name="look_upscale_sharp_desc">للتشغيل بدقة أقل من دقة الشاشة.</string> + <string name="look_pixel_clean">بكسل نقي</string> + <string name="look_pixel_clean_desc">بكسلات واضحة للألعاب الثنائية الأبعاد والألعاب القديمة.</string> + <string name="look_anime_edge">حافة الأنيمي</string> + <string name="look_anime_edge_desc">حدود كرتونية مع زيادة الحدّة.</string> + + <string name="gpuinfo_not_adreno">وحدة GPU ليست من نوع Adreno</string> + <string name="gpuinfo_supported">مدعوم</string> + <string name="gpuinfo_experimental">تجريبي: برامج تشغيله إصدارات اختبارية</string> + <string name="gpuinfo_below_tested">أقل من العتاد المُختبَر (Adreno 725 وأحدث): قد لا يعمل</string> + <string name="gpuinfo_unsupported">غير مدعوم: يحتاج DroidDeck إلى وحدة GPU من نوع Adreno (Snapdragon)</string> + + <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> + <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> + <string name="turnip_reject_library_missing">يذكر meta.json اسم %1$s، وهو غير موجود في ملف zip</string> + <string name="turnip_reject_not_aarch64">%1$s ليست مكتبة مشتركة ELF من نوع AArch64 64 بت</string> + <string name="turnip_reject_glibc">%1$s يرتبط بـ glibc - وهو إصدار Turnip بامتداد «-Linux»، الذي يندرج تحت «برنامج تشغيل بيئة Linux»</string> + <string name="lvd_no_library">لا يوجد libvulkan_freedreno*.so في ملف zip هذا. ملف Turnip من نوع Android (AdrenoTools) أو -Wayland ليس برنامج تشغيل بيئة Linux.</string> + <string name="lvd_not_aarch64">%1$s ليست مكتبة مشتركة ELF من نوع AArch64 64 بت.</string> + <string name="lvd_not_glibc">%1$s ليس برنامج تشغيل glibc - فهو يرتبط بمكتبة libc الخاصة بـ Android. تحتاج بيئة تشغيل Linux إلى ملف zip بامتداد «-Linux»؛ ولا يمكن لعميل Steam تحميل إصدار Turnip العادي أو بامتداد «-Wayland» على الإطلاق.</string> + <string name="bundle_no_manifest">ليست حزمة برامج تشغيل: لا يوجد manifest.json يذكر برنامج تشغيل لـ Android وآخر لـ Linux</string> + <string name="bundle_entry_outside">ليست حزمة برامج تشغيل: %1$s يشير إلى خارجها</string> + <string name="bundle_too_large">ليست حزمة برامج تشغيل: تتجاوز بعد الاستخراج %1$d MB</string> + + <string name="gstore_sd_card">بطاقة SD</string> + <string name="gstore_folder">مجلد</string> + + <string name="logshare_chooser">مشاركة سجلات الجلسة</string> + + <string name="hdr_needs_android_10">يحتاج إلى Android 10 أو أحدث (طبقات عرض بمساحة ألوان)</string> + <string name="hdr_none">لا تُعلن هذه الشاشة عن دعم HDR</string> + <string name="hdr_not_hdr10">تدعم هذه الشاشة %1$s، وليس HDR10</string> + + <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> + <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> + <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> + <string name="upd_fail_with_message">لم يتم تثبيت التحديث: %1$s</string> + <string name="upd_fail_generic">لم يتم تثبيت التحديث.</string> + <string name="saves_layout_zip">ملف zip لـ %1$s</string> + <string name="saves_zip">ملف zip</string> + <string name="saves_not_launched">لم يتم تشغيل %1$s بعد</string> + <string name="saves_none_found">لم يتم العثور على ملفات حفظ لـ %1$s</string> + <string name="saves_not_save_zip">لا يحتوي %1$s على مجلد drive_c، فهو ليس ملف حفظ من GameHub أو Winlator</string> + <string name="gstore_cannot_create">تعذّر إنشاء مجلدات في %1$s</string> + <string name="gstore_cannot_write">تعذّرت الكتابة في %1$s</string> + <string name="gstore_cannot_write_detail">تعذّرت الكتابة في %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">المجلد غير متاح</string> + <string name="gfiles_name_taken">يستخدم ملف آخر %1$s بالفعل</string> + <string name="gfiles_art_unavailable">مجلد الصور الفنية غير متاح</string> + <string name="gfiles_unrelated_inventory">يحتوي المجلد على قائمة مزامنة غير ذات صلة</string> + <string name="gfiles_remove_failed">تعذّرت إزالة %1$s</string> + <string name="gfiles_outside_folder">الملف يخرج عن مجلد التصدير</string> + <string name="cmgr_fex_unnamed">FEX (إصدار بدون اسم نسخة)</string> + <string name="cmgr_no_profile">لا يوجد profile.json في %1$s</string> + <string name="cmgr_not_component_package">%1$s ليس حزمة مكوّن لـ Linux (النوع %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s متاحة</string> + <string name="mode_storage_auto_short">تلقائي</string> + <string name="appupd_catalog_rejected">فشل كتالوج التحديثات في اختباراته (%1$s)</string> + <string name="main_adb_clip_label">إعداد العمليات الفرعية لـ DroidDeck</string> + <string name="setup_signed_in_offline_unnamed">تم تسجيل الدخول · وضع عدم الاتصال</string> + <string name="setup_signed_in_unnamed">تم تسجيل الدخول</string> + <string name="driver_tag_bundled">مرفق</string> + <string name="driver_tag_downloaded">منزَّل</string> + <string name="driver_tag_imported">مستورَد</string> + <string name="driver_tag_bundle">Android + Linux</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">شاشة الجلسة</string> + <string name="display_resolution">الدقة</string> + <string name="display_game_fit">ملاءمة اللعبة لنسبة 16:9</string> + <string name="display_game_fit_hint">كيفية ملاءمة ألعاب 16:9 لهذه الشاشة. يُطبَّق في الجلسة التالية.</string> + <string name="display_game_fit_unavailable">اختر «مطابقة الشاشة» أو حجمًا مخصَّصًا أضيق من 16:9.</string> + <string name="display_preserve">الحفاظ على النسب</string> + <string name="display_stretch">تمديد لملء الشاشة</string> + <string name="display_image_scaling">تكبير الصورة</string> + <string name="display_filter">مرشِّح التكبير</string> + <string name="display_filter_hint">مشترك بين Steam وسطح المكتب.</string> + <string name="display_filter_live">يُطبَّق فوريًا. مشترك بين Steam وسطح المكتب.</string> + <string name="display_filter_note">تُطبَّق FSR و GSR و NIS عند تكبير صورة الجلسة. ويعمل خيار «زيادة الحدة فقط» أيضًا دون تكبير.</string> + <string name="display_sharpness">حدّة المرشِّح</string> + <string name="display_frame_rate">معدّل الإطارات</string> + <string name="display_look_filter">%1$s يضبط أسلوب التكبير على %2$s.</string> + <string name="display_match_screen">مطابقة الشاشة (%1$d×%2$d)</string> + <string name="display_follow_screen">اتباع الشاشة (يُغيَّر الحجم عند الطي أو الفتح)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">إظهار تفاصيل الجهاز</string> + <string name="device_info_hide">إخفاء تفاصيل الجهاز</string> + <string name="device_info_loading">جارٍ قراءة بيانات الجهاز…</string> + <string name="device_info_copy">نسخ</string> + <string name="device_info_copy_hint">انسخ هذه التفاصيل في بلاغ عن الخطأ.</string> + <string name="device_info_copied">تم النسخ.</string> + <string name="force_ssbs_title">فرض SSBS</string> + <string name="force_ssbs_hint">يحافظ على إعداد سرعة لوحدة المعالجة (CPU) يستمر Wine في إيقافه. قد يجعل ألعاب Windows أسرع على Snapdragon 8 Elite؛ مع تغيّر طفيف على الشرائح الأقدم. يُطبَّق عند تشغيل اللعبة في المرة التالية.</string> + + <string name="rail_stores">المتاجر</string> + <string name="source_steam">Steam</string> + <string name="source_added">مخصَّص</string> + <string name="setup_stores">المتاجر</string> + <string name="setup_stores_open_on">فتح متجر على</string> + <string name="setup_stores_speed">فئة سرعة التنزيل</string> + <string name="setup_stores_engine">محرك المتاجر</string> + <string name="setup_stores_engine_chip_ready">● المحرك جاهز</string> + <string name="setup_stores_engine_chip_missing">غير مبنيّ</string> + <string name="stores_tier_balanced">متوازن</string> + <string name="stores_tier_fast">سريع</string> + <string name="stores_tier_max">أقصى</string> + <string name="stores_title">المتاجر</string> + <string name="stores_settings">إعدادات المتاجر</string> + <string name="stores_settings_eyebrow">الإعدادات</string> + <string name="stores_chip_signed_out">(لم يتم تسجيل الدخول)</string> + <string name="stores_tab_store">المتجر</string> + <string name="stores_tab_installed">المثبَّتة</string> + <string name="stores_tab_library">المكتبة</string> + <string name="stores_tab_all">الكل</string> + <string name="stores_tab_downloads">التنزيلات</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">تسجيل الدخول</string> + <string name="stores_signout_named">%1$s · تسجيل الخروج</string> + <string name="stores_signout_title">تسجيل الخروج من %1$s؟</string> + <string name="stores_signout_note">تبقى الألعاب المثبَّتة. تُجلَب المكتبة مرة أخرى عند تسجيل الدخول مجددًا.</string> + <string name="stores_signout_stay">البقاء مسجَّل الدخول</string> + <string name="stores_signout_go">تسجيل الخروج</string> + <string name="stores_search_catalog">البحث في كتالوج %1$s…</string> + <string name="stores_search_installed">البحث في الألعاب المثبَّتة…</string> + <string name="stores_search_library">البحث في مكتبتك…</string> + <string name="stores_search_open">البحث</string> + <string name="stores_installed_empty">لا شيء مثبَّت</string> + <string name="stores_library_loading">جارٍ التحميل</string> + <string name="stores_nothing_matches">لا توجد ألعاب</string> + <string name="stores_shelves_loading">جارٍ تحميل المتجر…</string> + <string name="stores_amazon_no_catalog">لا يوجد كتالوج</string> + <string name="stores_shelf_new">الجديد</string> + <string name="stores_shelf_deals">العروض</string> + <string name="stores_shelf_free">ألعاب مجانية</string> + <string name="stores_shelf_trending">الرائج</string> + <string name="stores_shelf_library">مكتبتك</string> + <string name="stores_play">تشغيل</string> + <string name="stores_install">تثبيت</string> + <string name="stores_resume_install">استئناف التثبيت</string> + <string name="stores_dl_interrupted">منقطع</string> + <string name="stores_install_size">تثبيت · %1$s</string> + <string name="stores_get_free">احصل عليها مجانًا</string> + <string name="stores_view_on">عرض على %1$s</string> + <string name="stores_buy_price">شراء · %1$s</string> + <string name="stores_installed_chip">مثبَّتة</string> + <string name="stores_in_library">في المكتبة</string> + <string name="stores_free">مجاني</string> + <string name="stores_resume">استئناف</string> + <string name="stores_refresh">تحديث</string> + <string name="stores_show_mature">إظهار المحتوى للبالغين</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">غير متصل</string> + <string name="epic_card_none">غير مفعَّل</string> + <string name="epic_eos">تسجيل الدخول إلى Epic (EOS)</string> + <string name="epic_offline">التشغيل دون اتصال</string> + <string name="epic_resolve">إصلاح تسجيل الدخول إلى Epic</string> + <string name="store_cloud_saves">ملفات الحفظ السحابية</string> + <string name="cloud_upload">الرفع إلى السحابة</string> + <string name="cloud_download">التنزيل من السحابة</string> + <string name="cloud_none">لا توجد ملفات حفظ سحابية</string> + <string name="cloud_conflict">تعارض سحابي</string> + <string name="cloud_conflict_files">تعارض سحابي · %1$d ملفات</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">الاحتفاظ بالسحابة</string> + <string name="cloud_keep_local">الاحتفاظ بالمحلي</string> + <string name="cloud_checking">جارٍ التحقّق من السحابة…</string> + <string name="cloud_last_sync">آخر مزامنة %1$s</string> + <string name="cloud_never">لم تتم المزامنة بعد</string> + <string name="cloud_done">%1$d ملفات</string> + <string name="stores_eyebrow_installed">مثبَّتة</string> + <string name="stores_eyebrow_library">في مكتبتك</string> + <string name="stores_eyebrow_store">متجر</string> + <string name="achievements_title">الإنجازات</string> + <string name="achievements_count">%1$d من %2$d مفتوحة</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">مفتوحة · %1$d</string> + <string name="achievements_group_locked">مقفلة · %1$d</string> + <string name="achievements_group_hidden">مخفية · %1$d</string> + <string name="achievements_hidden">إنجاز مخفي</string> + <string name="achievements_hidden_detail">يتم عرض التفاصيل بعد فتحه.</string> + <string name="achievements_all">جميع الإنجازات مفتوحة</string> + <string name="achievements_reading">جارٍ التحميل…</string> + <string name="achievements_signed_out">سجّل الدخول إلى %1$s لرؤية إنجازاتك.</string> + <string name="achievements_unreachable">تعذّر الوصول إلى %1$s. تحقّق مرة أخرى عند الاتصال بالإنترنت.</string> + <string name="achievements_none">لا يسرد %1$s أي إنجازات لهذه اللعبة.</string> + <string name="achievements_show_all">إظهار الكل (%1$d)</string> + <string name="achievements_dialog_title">الإنجازات · %1$s</string> + <string name="stores_in_steam">✓ في Steam</string> + <string name="stores_uninstall">إلغاء التثبيت</string> + <string name="stores_uninstalling">جارٍ إلغاء التثبيت…</string> + <string name="stores_uninstall_title">إلغاء تثبيت %1$s؟</string> + <string name="stores_uninstall_body_size">يُفرِغ %1$s من المساحة ويزيل اللعبة من Steam.</string> + <string name="stores_uninstall_body">يزيل ملفاتها واختصارها في Steam.</string> + <string name="stores_launch_not_registered">يكتشف Steam اللعبة عند بدء تشغيله التالي؛ شغِّلها من هناك، أو شغِّل Steam الآن.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">اختيار Steam لكل لعبة</string> + <string name="stores_card_audio">الصوت</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">المكوّنات</string> + <string name="stores_card_components_value">FEX، DXVK، VKD3D</string> + <string name="stores_card_engine">المحرك</string> + <string name="stores_card_engine_value">%1$s · أصلي</string> + <string name="stores_card_engine_java">%1$s · مدمَج</string> + <string name="stores_how_it_runs">كيف تعمل</string> + <string name="stores_kv_fetched">تم الجلب من</string> + <string name="stores_kv_fetched_value">%1$s، بحسابك</string> + <string name="stores_kv_installed_to">مثبَّتة في</string> + <string name="stores_kv_installed_to_value">تخزين الألعاب (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">تُشغَّل باستخدام</string> + <string name="stores_kv_launched_by_value">Steam وProton، بنفس مسار ألعاب Steam</string> + <string name="stores_kv_launched_by_epic">Steam وProton · تسجيل الدخول إلى Epic عند كل تشغيل، وتراكب EOS معطَّل</string> + <string name="stores_kv_launched_by_amazon">Steam وProton · تم ضبط متغيّرات Amazon Games Services</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">تُضاف إلى Steam عند التثبيت (واجهة Deck + Steam Input)</string> + <string name="stores_downloads_empty">لا توجد تنزيلات</string> + <string name="stores_dl_paused">متوقِّف مؤقتًا</string> + <string name="stores_dl_queued">قيد الانتظار</string> + <string name="stores_dl_queued_at">قيد الانتظار · #%1$d</string> + <string name="stores_dl_cancelled">ملغى</string> + <string name="stores_dl_failed">فشل</string> + <string name="stores_dl_failed_reason">فشل · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">إيقاف مؤقت</string> + <string name="stores_dl_resume">استئناف</string> + <string name="stores_dl_cancel">إلغاء</string> + <string name="stores_dl_retry">إعادة المحاولة</string> + <string name="stores_dl_clear">إزالة</string> + <string name="stores_dl_clear_all">إزالة المنتهية</string> + <string name="stores_stage_manifest">الفهرس</string> + <string name="stores_stage_download">التنزيل</string> + <string name="stores_stage_verify">التحقّق</string> + <string name="stores_stage_install">التثبيت</string> + <string name="stores_stage_done">تم</string> + <string name="stores_stage_checking">جارٍ الفحص</string> + <string name="stores_stage_downloading">جارٍ التنزيل</string> + <string name="stores_dl_cancel_confirm">حذف التنزيل؟</string> + <string name="stores_dl_failed_kept">فشل · تم الاحتفاظ بـ %1$s</string> + <string name="stores_stage_verifying">جارٍ التحقّق</string> + <string name="stores_stage_installing">جارٍ التثبيت</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d ملفات</string> + <string name="stores_download_manager">مدير التنزيلات</string> + <string name="stores_parallel">عدد التنزيلات المتزامنة</string> + <string name="stores_install_where">التثبيت في</string> + <string name="stores_install_title">تثبيت %1$s:</string> + <string name="stores_target_internal">التخزين الداخلي</string> + <string name="stores_target_free">%1$s متاحة</string> + <string name="stores_notification_channel">تنزيلات المتجر</string> + <string name="stores_notification_channel_hint">يعرض تقدّم تنزيل الألعاب ويحافظ على استمرار التنزيلات في الخلفية</string> + <string name="stores_notification_one">جارٍ التنزيل</string> + <string name="stores_notification_many">%1$d تنزيلات</string> + <string name="stores_notification_waiting">جارٍ التحضير…</string> + <string name="stores_notification_done_channel">تنزيلات المتجر المكتملة</string> + <string name="stores_notification_installed">تم تثبيت %1$s</string> + <string name="stores_notification_failed">فشل تنزيل %1$s</string> + <string name="stores_notification_delete">حذف</string> + <string name="stores_notification_keep">الاحتفاظ</string> + <string name="stores_login_title">تسجيل الدخول إلى %1$s</string> + <string name="stores_login_provider_title">تسجيل الدخول عبر مزوّدك</string> + <string name="stores_login_reload">إعادة التحميل</string> + <string name="stores_login_close">إغلاق</string> + <string name="stores_login_retry">حاوِل مرة أخرى</string> + <string name="stores_login_finishing">جارٍ إنهاء تسجيل الدخول…</string> + <string name="stores_login_error_network">تعذّر تحميل صفحة تسجيل الدخول: %1$s</string> + <string name="stores_login_error_idp_blocked">يرفض هذا المزوّد تسجيل الدخول من صفحة مضمَّنة. سجِّل الدخول باستخدام كلمة مرور المتجر بدلاً من ذلك.</string> + <string name="stores_login_error_http">استجابت صفحة تسجيل الدخول بخطأ (HTTP %1$d).</string> + <string name="stores_login_error_ssl">الاتصال بصفحة تسجيل الدخول غير آمن.</string> + <string name="stores_login_error_verification">تعذّر التحقّق من تسجيل الدخول. حاوِل مرة أخرى.</string> + <string name="stores_login_error_generic">فشل تسجيل الدخول. حاوِل مرة أخرى.</string> + <string name="common_close">إغلاق</string> + <string name="common_back">رجوع</string> + <string name="common_edit">تعديل</string> + <string name="games_add_all">إضافة كل الألعاب في هذا المجلد</string> + <string name="games_added_none">لا شيء جديد في هذا المجلد</string> + <string name="edit_name">الاسم</string> + <string name="edit_target">الهدف</string> + <string name="edit_start_in">بدء التشغيل في</string> + <string name="edit_launch_options">خيارات التشغيل</string> + <string name="edit_artwork">الصور الفنية</string> + <string name="edit_other_exe">ملف exe آخر…</string> + <string name="edit_remove">إزالة</string> + <string name="art_cover">الغلاف</string> + <string name="art_background">الخلفية</string> + <string name="art_logo">الشعار</string> + <string name="art_icon">الأيقونة</string> + <string name="art_src_folder">مجلد اللعبة</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">ملفك</string> + <string name="art_src_exe">من ملف exe</string> + <string name="art_src_auto">تلقائي</string> + <string name="art_pick_image">اختيار صورة…</string> + <string name="art_pick_title">اختيار صورة: %1$s</string> + <string name="art_reset">إعادة الضبط إلى تلقائي</string> + <plurals name="games_added_n"> + <item quantity="zero">لم تتم إضافة أي لعبة</item> + <item quantity="one">تمت إضافة لعبة واحدة</item> + <item quantity="two">تمت إضافة لعبتين</item> + <item quantity="few">تمت إضافة %1$d ألعاب</item> + <item quantity="many">تمت إضافة %1$d لعبةً</item> + <item quantity="other">تمت إضافة %1$d لعبة</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="zero">لا توجد أي لعبة ضمن الألعاب بالفعل</item> + <item quantity="one">لعبة واحدة ضمن الألعاب بالفعل</item> + <item quantity="two">لعبتان ضمن الألعاب بالفعل</item> + <item quantity="few">%1$d ألعاب ضمن الألعاب بالفعل</item> + <item quantity="many">%1$d لعبةً ضمن الألعاب بالفعل</item> + <item quantity="other">%1$d لعبة ضمن الألعاب بالفعل</item> + </plurals> + <string name="hop_back">B · العودة إلى %1$s</string> + <string name="hop_from_games">اللعبة</string> + <string name="hop_from_play">إعدادات التشغيل</string> + <string name="sheet_graphics_drivers">برامج تشغيل الرسوميات</string> + <string name="comp_this_proton">هذا الإصدار من Proton</string> + <string name="comp_every_proton">كل إصدار من Proton</string> + <string name="comp_for_every_proton">لكل إصدار من Proton</string> + <string name="comp_available_count">متاح · %1$d</string> + <string name="comp_checked">آخر تحقّق %1$s</string> + <string name="comp_check_all">التحقّق من وجود مكوّنات وبرامج تشغيل جديدة</string> + <string name="comp_get">جلب</string> + <string name="comp_proton_bar">%1$s · DXVK وVKD3D-Proton وFEX أدناه خاصة بهذا الإصدار من Proton</string> + <string name="comp_device_tested">%1$s · تم اختباره</string> + <string name="comp_device_untested">%1$s · لم يتم اختباره</string> + <string name="comp_device_unsupported">%1$s · غير مدعوم</string> + <string name="gpu_auto_keeps">يحافظ على الزوج الموصى به محدَّثًا</string> + <string name="gpu_bar_auto">تلقائي · %1$s</string> + <string name="gpu_separately">برنامجا تشغيل بيئة التشغيل والعرض بشكل منفصل</string> + <string name="gpu_use_title">استخدام %1$s؟</string> + <string name="proton_remove_title">إزالة %1$s؟</string> + <string name="setup_tab_diagnostics">التشخيص</string> + <string name="setup_ready">جاهز للّعب</string> + <string name="setup_not_ready">غير جاهز للّعب بعد</string> + <string name="setup_ready_runtime">بيئة التشغيل %1$s</string> + <string name="setup_ready_limit_off">حدّ العمليات الفرعية غير مفعَّل</string> + <string name="setup_ready_signed_in">تم تسجيل الدخول باسم %1$s</string> + <string name="setup_needs_you">عندما يحتاج الأمر إلى تدخّلك</string> + <string name="setup_group_look">المظهر</string> + <string name="setup_group_device">هذا الجهاز</string> + <string name="setup_group_art">صور المكتبة الفنية</string> + <string name="flathub_eyebrow">تطبيقات Linux</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">تطبيقات سطح المكتب لسطح مكتب Linux، تُفتح من سطح المكتب.</string> + <string name="flathub_setup">إعداد Flathub</string> + <string name="flathub_remove_title">إزالة %1$s؟</string> + <string name="flathub_remove_note">تُحفَظ إعداداته وملفاته.</string> + <string name="flathub_keep">الاحتفاظ</string> + <string name="desktop_size">الحجم على القرص</string> + <string name="desktop_uninstall_title">إلغاء تثبيت سطح المكتب؟</string> + <string name="desktop_all_systems">الكل</string> + <string name="gog_achievements">إنجازات GOG</string> + <string name="gog_achievements_hint">مزامنة الإنجازات للألعاب المتوافقة من GOG. يُطبَّق عند التشغيل التالي.</string> + <string name="gog_achievements_off">الإنجازات متوقفة</string> + <string name="gog_achievements_on">الإنجازات مفعّلة</string> + <string name="gog_achievements_signin">سجّل الدخول إلى GOG مرة أخرى</string> + <string name="gog_achievements_uploaded">تم رفع الإنجازات</string> + <string name="gog_achievements_pending">رفع الإنجاز معلّق</string> + <string name="gog_achievements_enable_store">فعّل إنجازات GOG في إعدادات المتاجر</string> + <string name="gog_achievements_unavailable">الإنجازات غير متوفرة؛ تحقّق من سجلات الجلسة</string> + <string name="achievement_unlocked">تم فتح الإنجاز</string> + <string name="touch_paste">لصق</string> + <string name="touch_dpad">لوحة الاتجاهات</string> + <string name="touch_left_stick">العصا اليسرى</string> + <string name="touch_right_stick">العصا اليمنى</string> + <string name="touch_menu">القائمة</string> + <string name="touch_keyboard">لوحة المفاتيح</string> + <string name="touch_dpad_up">لوحة الاتجاهات للأعلى</string> + <string name="touch_dpad_down">لوحة الاتجاهات للأسفل</string> + <string name="touch_dpad_left">لوحة الاتجاهات لليسار</string> + <string name="touch_dpad_right">لوحة الاتجاهات لليمين</string> + <string name="touch_title">أدوات التحكم باللمس</string> + <string name="touch_controller">وحدة تحكم باللمس</string> + <string name="touch_mouse">الفأرة</string> + <string name="touch_on">تشغيل</string> + <string name="touch_off">إيقاف</string> + <string name="touch_speed_slow">بطيئة</string> + <string name="touch_speed_normal">عادية</string> + <string name="touch_speed_fast">سريعة</string> + <string name="touch_edit_layout">تعديل التخطيط</string> + <string name="touch_droiddeck_menu">قائمة DroidDeck</string> + <string name="touch_drag_hint">اسحب أدوات التحكم إلى الشاشة</string> + <string name="touch_arrange">ترتيب</string> + <string name="touch_discard">تجاهل</string> + <string name="touch_saving">جارٍ الحفظ…</string> + <string name="touch_icon">الأيقونة</string> + <string name="touch_narrower">ع−</string> + <string name="touch_wider">ع+</string> + <string name="touch_shorter">ط−</string> + <string name="touch_taller">ط+</string> + <string name="touch_hide">إخفاء</string> + <string name="touch_color_opacity">اللون والعتامة…</string> + <string name="touch_copy_layout">نسخ هذا التخطيط</string> + <string name="touch_layout_copied">تم نسخ التخطيط؛ يمكنك لصقه في تخطيط اللمس لأي لعبة</string> + <string name="touch_paste_layout">لصق التخطيط المنسوخ</string> + <string name="touch_reset_layout">إعادة الضبط إلى تخطيط Steam الافتراضي</string> + <string name="touch_no_binding">لا يوجد ارتباط لهذا العنصر في Steam لإعطائه أيقونة</string> + <string name="touch_icon_for">أيقونة %1$s</string> + <string name="touch_layout_color">لون تخطيط اللمس</string> + <string name="touch_done">تم</string> + <string name="touch_save_failed">تعذّر حفظ تخطيط اللمس</string> + <string name="touch_saved">تم حفظ تخطيط اللمس في Steam</string> + <string name="touch_app_id">التطبيق %1$d</string> + <string name="touch_label_hint">التسمية (اختياري)</string> + <string name="touch_icons_missing">لم يتم العثور على أيقونات Steam</string> + <plurals name="touch_icons_found"> + <item quantity="zero">لا توجد أيقونات من Steam</item> + <item quantity="one">أيقونة واحدة من Steam</item> + <item quantity="two">أيقونتان من Steam</item> + <item quantity="few">%1$d أيقونات من Steam</item> + <item quantity="many">%1$d أيقونة من Steam</item> + <item quantity="other">%1$d أيقونة من Steam</item> + </plurals> + <string name="touch_icon_color">لون الأيقونة</string> + <string name="touch_button_color">لون الزر</string> + <string name="touch_no_icon">بلا أيقونة</string> + + <string name="stores_sync_sign_in">جارٍ التحقق من تسجيل الدخول…</string> + <string name="stores_sync_library">جارٍ جلب مكتبتك…</string> + <string name="stores_sync_game_list">جارٍ جلب قائمة الألعاب…</string> + <string name="stores_sync_details">جارٍ تحميل تفاصيل الألعاب… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="zero">لا مزامنة لألعاب</item> + <item quantity="one">جارٍ مزامنة لعبة واحدة…</item> + <item quantity="two">جارٍ مزامنة لعبتين…</item> + <item quantity="few">جارٍ مزامنة %1$d ألعاب…</item> + <item quantity="many">جارٍ مزامنة %1$d لعبة…</item> + <item quantity="other">جارٍ مزامنة %1$d لعبة…</item> + </plurals> + <string name="stores_sync_progress">جارٍ المزامنة… %1$d/%2$d</string> + <string name="stores_sync_cover_art">جارٍ جلب أغلفة الألعاب…</string> + <string name="stores_sync_failed">فشلت المزامنة</string> + <string name="stores_sync_no_games">لم يتم العثور على ألعاب في مكتبة %1$s الخاصة بك</string> + <string name="stores_sync_amazon_no_answer">لم يستجب Amazon Games بخصوص مكتبتك: حدّث الصفحة، أو سجّل الدخول من جديد</string> + <string name="stores_sync_gog_unreachable">تعذّر الاتصال بـ GOG</string> + <string name="stores_sync_gog_unreadable">تعذّر قراءة مكتبتك على GOG</string> + <string name="stores_registering">جارٍ التسجيل في Steam…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">وضع Cardboard</string> + <string name="cardboard_mode_hint">عرض للعين اليسرى وآخر لليمنى لنظارة واقع افتراضي للهاتف.</string> + <string name="cardboard_source">الصورة</string> + <string name="cardboard_source_screen">شاشة افتراضية</string> + <string name="cardboard_source_sbs">ثلاثي الأبعاد جنبًا إلى جنب</string> + <string name="cardboard_viewer">النظارة</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">بلا عدسة</string> + <string name="cardboard_ipd">المسافة بين العينين</string> + <string name="cardboard_screen_width">عرض الشاشة</string> + <string name="cardboard_screen_distance">بُعد الشاشة</string> + <string name="cardboard_head_tracking">تتبّع الرأس</string> + <string name="cardboard_no_sensor">لا يحتوي هذا الجهاز على مستشعر دوران.</string> + <string name="cardboard_recenter">إعادة توسيط العرض</string> + <string name="cardboard_recenter_button">إعادة التوسيط</string> + <string name="cardboard_steamvr">SteamVR (تجريبي)</string> + <string name="cardboard_steamvr_install">تثبيت</string> + <string name="cardboard_steamvr_install_hint">ينزّل SteamVR لمعمارية ARM64 من Valve ‏(571 ميغابايت).</string> + <string name="cardboard_steamvr_installing">جارٍ تنزيل SteamVR وفك ضغطه…</string> + <string name="cardboard_steamvr_start">بدء</string> + <string name="cardboard_steamvr_ready_hint">يشغّل SteamVR مع استخدام رأسك كنظارة؛ وتتحول الصورة إلى ثلاثي الأبعاد جنبًا إلى جنب.</string> + <string name="cardboard_steamvr_stop">إيقاف</string> + <string name="cardboard_steamvr_running_hint">SteamVR قيد التشغيل.</string> + <string name="cardboard_steamvr_unavailable">غير متوفر في هذه الجلسة.</string> + <string name="cardboard_steamvr_failed">لم يستجب SteamVR؛ تحقّق من سجل الجلسة.</string> + <string name="drawer_component_downloads">تنزيل المكوّنات</string> + <string name="drawer_component_downloads_hint">التنزيلات مشتركة مع صفحة «المكوّنات». اختر نسخة منزّلة أعلاه لاستخدامها عند التشغيل التالي للعبة.</string> + <string name="drawer_component_choose_download">اختر نسخة لتنزيلها</string> + <string name="stores_fact_developer">المطوّر</string> + <string name="stores_fact_publisher">الناشر</string> + <string name="stores_fact_released">الإصدار</string> + <string name="stores_hltb_main">القصة الرئيسية</string> + <string name="stores_hltb_extras">+ الإضافات</string> + <string name="stores_hltb_completionist">الإكمال التام</string> + <string name="stores_hltb_hours">%1$s س</string> + <string name="stores_hltb_minutes">%1$d د</string> +</resources> diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml new file mode 100644 index 000000000..982623fd1 --- /dev/null +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -0,0 +1,93 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string-array name="loading_hints"> + <item>La primera vez se descarga Steam. Puede tardar unos minutos.</item> + <item>De forma predeterminada, al salir de la app Steam se pausa y continúa donde lo dejaste cuando vuelves.</item> + <item>Tu control aparece como un control de Xbox 360. Si no, usa los controles en pantalla.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>La primera vez se prepara el escritorio. Puede tardar unos minutos.</item> + <item>De forma predeterminada, al salir de la app la sesión se pausa y continúa donde la dejaste cuando vuelves.</item> + <item>El tacto funciona como un touchpad: arrastra para mover el puntero y toca para hacer clic.</item> + <item>Tu control aparece como un control de Xbox 360. Si no, usa los controles en pantalla.</item> + </string-array> + <string name="second_library_import_hint">Agrega una biblioteca de Steam y busca juegos de Windows en ella. Los juegos que tienes usan sus entradas de Steam; el resto se convierten en accesos directos ajenos a Steam.</string> + <string name="game_env_add">Agregar variable</string> + <string name="game_env_xalia">Pon 0 para desactivar el asistente de navegación con control de Proton en este juego.</string> + <string name="user_apps_add">Agregar</string> + <string name="user_apps_added">%1$s agregada</string> + <string name="user_apps_remove_confirm">Presiona de nuevo para quitar</string> + <string name="user_apps_failed">No se pudo agregar la app</string> + <string name="add_app_title">Agregar una app</string> + <string name="add_app_confirm">Agregar</string> + <string name="common_add_ellipsis">Agregar…</string> + <string name="mode_osc_note">Auto muestra los controles en pantalla cuando no hay ningún control conectado.</string> + <string name="mode_controller">Control</string> + <string name="mode_controller_hint">Cómo ve Steam tu control. Se aplica en la próxima sesión.</string> + <string name="mode_controller_deck">Control de Steam Deck</string> + <string name="mode_controller_x360">Control de Xbox 360</string> + <string name="games_add">Agregar juego</string> + <string name="games_add_title">Agregar un juego:</string> + <string name="mode_storage_note">Los juegos que cargan recursos desde la SD o el almacenamiento compartido pueden tener microcortes.</string> + <string name="comp_no_proton">Aún no hay ningún Proton instalado. Inicia Steam una vez para descargar su Proton ARM64, o agrega GE-Proton o CachyOS desde Configuración.</string> + <string name="perf_gpuclock_hint">Solo Adreno. Menos microcortes, más consumo de batería y calor.</string> + <string name="perf_xalia_hint">Desactiva el asistente de navegación con control de Proton, que consume CPU en cada juego.</string> + <string name="perf_fastpath_hint">Búsquedas de archivos más rápidas y menos microcortes. Desactívalo si un programa no encuentra sus archivos.</string> + <string name="drawer_page_controller">Control</string> + <string name="setup_manage">Administrar</string> + <string name="setup_tab_controller">Control</string> + <string name="setup_sign_in">Presiona Jugar en Steam e inicia sesión</string> + <string name="setup_controller">Control</string> + <string name="setup_controller_unavailable">Los ajustes del control no están disponibles.</string> + <string name="setup_tool_files_hint">Explorar y administrar archivos</string> + <string name="store_setup_text">Agrega Flatpak y lo conecta a Flathub (unos 20 MB). La primera app también descarga su runtime.</string> + <string name="store_press_again">Presiona de nuevo para quitar</string> + <string name="gate_no_toggle">Esta versión de Android no tiene un interruptor para ello, así que DroidDeck lo cambia mediante la Depuración inalámbrica. Tarda un minuto y no necesitas una computadora.</string> + <string name="gate_code_needed">Ventana encontrada. Ingresa su código en la notificación.</string> + <string name="gate_other_toggle">Depuración inalámbrica, dirección manual, ADB desde una computadora</string> + <string name="gate_other_no_toggle">Dirección manual, ADB desde una computadora, Opciones de desarrollador</string> + <string name="gate_some_roms">Algunas ROM agregan el interruptor de todos modos. Busca “procesos secundarios” en Opciones de desarrollador.</string> + <string name="gate_manual_hint">Si la notificación no funciona, ingresa manualmente la dirección y el código de la ventana.</string> + <string name="gate_enter_manually">Ingresar dirección manualmente</string> + <string name="gate_from_computer">Desde una computadora con ADB:</string> + <string name="wadb_enter_port">Vinculado. Ingresa el puerto de Ajustes.</string> + <string name="wadb_computer_last">3 · ADB desde una computadora (último recurso)</string> + <string name="wadb_computer_group">3 · ADB desde una computadora (último recurso)</string> + <string name="ctrl_select_steam">Presiona Select para abrir el menú de Steam</string> + <string name="ctrl_reset_all">Restablecer control</string> + <string name="ctrl_reset_all_hint">Restaura todos los ajustes del control y la disposición</string> + <string name="content_add_roms">Agrega juegos de %1$s a ROMs/%2$s.</string> + <string name="desktop_remove_text">Elimina el escritorio y sus apps incluidas. Se conservan los juegos, las apps agregadas, los ajustes y las bibliotecas compartidas.</string> + <string name="games_manage_saves">Administrar partidas guardadas</string> + <string name="games_added">Juego agregado</string> + <string name="deck_tab_gamepad">Control</string> + <string name="game_shortcut_add">Agregar a la pantalla de inicio</string> + <string name="game_shortcut_unsupported">Este launcher no puede agregar accesos directos. Usa Copiar enlace de inicio.</string> + <string name="session_runtime_catalog_unreachable">No se pudo acceder al catálogo del runtime. Revisa la conexión y vuelve a presionar Jugar.</string> + <string name="session_runtime_install_failed">El runtime de Linux no se instaló. Revisa el runtime en Configuración y vuelve a presionar Jugar.</string> + <string name="session_desktop_install_failed">El escritorio no se instaló (%1$s). Revisa la conexión y presiona Escritorio de nuevo.</string> + <string name="store_cat_audio_video">Audio y video</string> + <string name="theme_graphite_detail">Superficies grafito, con el azul del orbe del logotipo para todo lo seleccionado, enfocado o presionado.</string> + <string name="theme_icon_blue_detail">Fondo negro, texto blanco y el azul para todo lo seleccionado, enfocado o presionado.</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="game_sync_err_added_games">La carpeta de juegos agregados no está disponible</string> + <string name="fm_favorite_add">Agregar a Favoritos</string> + <string name="fm_conflict_merge_hint">Combinar agrega y reemplaza archivos dentro de la carpeta existente.</string> + <string name="fm_favorite_added">Se agregó \"%1$s\" a Favoritos</string> + <string name="adbfix_enter_address">Ingresa la dirección IP y el puerto actuales de la Depuración inalámbrica</string> + <string name="adbpair_six_digits">Ingresa los 6 dígitos del código</string> + <string name="adbpair_code_rejected">Ese código no funcionó. Vuelve a abrir la ventana de vinculación e ingresa su nuevo código.</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 ingresa en esta notificación.</string> + <string name="adbpair_code_hint">Toca Ingresar código y escribe los 6 dígitos que muestra Ajustes.</string> + <string name="adbpair_title_code">Ingresa el código de vinculación Wi-Fi</string> + <string name="adbpair_enter_code">Ingresar código</string> + <string name="phantom_instructions_fix">%1$s Si no es posible, conecta este dispositivo a una computadora con ADB y ejecuta el comando de abajo. Después vuelve aquí y comprueba de nuevo.</string> + <string name="stores_kv_steam_on">se agrega a Steam al instalar (interfaz Deck + Steam Input)</string> + <string name="stores_download_manager">Administrador de descargas</string> + <string name="games_add_all">Agregar todos los juegos de esta carpeta</string> + <plurals name="games_added_n"> + <item quantity="one">%1$d juego agregado</item> + <item quantity="many">%1$d juegos agregados</item> + <item quantity="other">%1$d juegos agregados</item> + </plurals> +</resources> diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml new file mode 100644 index 000000000..ec2f9ff6c --- /dev/null +++ b/app/src/main/res/values-bg/strings.xml @@ -0,0 +1,2235 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>Първото стартиране изтегля Steam. Това може да отнеме няколко минути.</item> + <item>По подразбиране излизането от приложението поставя Steam на пауза; при връщане продължава от същото място.</item> + <item>Контролерът ви се разпознава като контролер Xbox 360. В противен случай използвайте екранните контроли.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>Първото стартиране подготвя работния плот. Това може да отнеме няколко минути.</item> + <item>По подразбиране излизането от приложението поставя сесията на пауза; при връщане тя продължава от същото място.</item> + <item>Докосването действа като тъчпад: плъзгане премества показалеца, докосване щраква.</item> + <item>Контролерът ви се разпознава като контролер Xbox 360. В противен случай използвайте екранните контроли.</item> + </string-array> + <string name="session_channel">Сесия</string> + <string name="session_channel_description">Показва се, докато има отворена сесия на Steam или работния плот</string> + <string name="session_notification">Steam работи</string> + <string name="session_notification_downloads">Активно изтегляне в Steam</string> + <string name="session_notification_paused">Сесията е на пауза – докоснете „Продължи“, когато сте готови</string> + <string name="resume_session">Продължи</string> + <string name="suspend_now">Приспи сега</string> + <string name="stop_session">Спри сесията</string> + <string name="frame_gen_title">Генериране на кадри</string> + <string name="frame_gen_hint">Изберете режима на генериране на кадри</string> + <string name="frame_gen_off">Изключено</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG Adaptive %1$d</string> + <string name="lsfg_needs_steam">Изисква Lossless Scaling в Steam</string> + <string name="lsfg_needs_install">Инсталирайте Lossless Scaling в Steam или импортирайте Lossless.dll</string> + <string name="lsfg_import">Импортиране на Lossless.dll…</string> + <string name="lsfg_using_steam">Използва се копието от Steam</string> + <string name="lsfg_using_import">Използва се импортирано копие</string> + <string name="lsfg_pick_title">Изберете Lossless.dll</string> + <string name="lsfg_imported">Lossless.dll е импортиран</string> + <string name="lsfg_import_unchanged">Този Lossless.dll вече се използва</string> + <string name="lsfg_import_not_owned">Lossless Scaling не е закупен за този акаунт в Steam</string> + <string name="lsfg_import_invalid">Този файл не е Lossless.dll на Lossless Scaling</string> + <string name="lsfg_import_failed">LSFG не можа да се подготви от този Lossless.dll</string> + <string name="lsfg_problem_missing">LSFG изисква Lossless Scaling от Steam или импортиран Lossless.dll</string> + <string name="lsfg_problem_failed">LSFG не можа да подготви шейдърите си от Lossless.dll</string> + <string name="hud_fg_unsupported">%1$s: не се поддържа</string> + <string name="hud_fg_failed">%1$s: грешка в двигателя</string> + <string name="hud_fg_needs_lossless">%1$s: изисква Lossless Scaling</string> + <string name="hud_fg_starting">%1$s %2$s · стартиране</string> + <string name="second_library_import_hint">Добавя библиотека на Steam и я сканира за игри за Windows. Притежаваните игри използват записите си в Steam; останалите стават преки пътища извън Steam.</string> + <string name="game_env_title">Среда на играта</string> + <string name="game_env_hint">Променливи на средата за игри с Proton.</string> + <string name="game_env_edit">Редактиране</string> + <string name="game_env_shared">Всички игри с Proton</string> + <string name="game_env_scope">Профил</string> + <string name="game_env_app_id">ID на приложението в Steam</string> + <string name="game_env_open">Отвори</string> + <string name="game_env_profile">Игра %1$s</string> + <string name="game_env_add">Добавяне на променлива</string> + <string name="game_env_name">Име на променливата</string> + <string name="game_env_value">Стойност</string> + <string name="game_env_save">Запиши</string> + <string name="game_env_remove">Премахни</string> + <string name="game_env_restore">Наследи отново</string> + <string name="game_env_reset">Изчистване на промените в средата</string> + <string name="game_env_unset">Премахни за този профил</string> + <string name="game_env_empty">Няма зададени променливи.</string> + <string name="game_env_inherited">Наследена</string> + <string name="game_env_custom">Персонализирана променлива</string> + <string name="game_env_error">Не можа да се зареди или запише. Проверете свободното място в хранилището и опитайте пак.</string> + <string name="game_env_invalid">Използвайте валидно име и стойност до 8192 знака.</string> + <string name="game_env_unused">Стандартната среда на Proton не отчита тази променлива.</string> + <string name="game_env_loading">Зареждане…</string> + <string name="game_env_feature">Замяна на нивото на възможности на D3D12. 12_2 показва възможности на DirectX 12 Ultimate; неподдържаните функции може да дадат грешка. Оставете незададено за автоматично откриване.</string> + <string name="game_env_shader">Замяна на модела на шейдъри на D3D12. 6_9 изисква достатъчно нов VKD3D и съответстващи възможности на GPU. Оставете незададено за автоматично откриване.</string> + <string name="game_env_cache">Дръжте кеширането на шейдъри на Mesa включено, за да намалите повтарящото се компилиране. false разрешава кеширането.</string> + <string name="game_env_cache_size">Максимален размер на кеша за шейдъри на Mesa, например 1G. По-големите кешове използват повече място в хранилището.</string> + <string name="game_env_vkd3d_config">Настройки на VKD3D за D3D12. nodxr изключва трасирането на лъчи; използвайте само за игра, която го изисква.</string> + <string name="game_env_proton_log">Задайте 1 за диагностичен дневник на Proton. Записването на дневник може да намали производителността и да използва място в хранилището.</string> + <string name="game_env_wined3d">Задайте 1, за да използвате WineD3D, базиран на OpenGL, вместо DXVK за тестване на съвместимост с D3D9–11.</string> + <string name="game_env_no_ntsync">Задайте 1, за да изключите ntsync (droiddeck-ntsync или драйвер на ядрото) за тази игра; тогава се използва droiddeck-esync, ако има пакет, или droiddeck-fsync в противен случай.</string> + <string name="game_env_no_esync">Задайте 1, за да изключите droiddeck-esync за тази игра; вместо това се използва droiddeck-fsync, или droiddeck-ntsync, ако fsync също е изключен.</string> + <string name="game_env_no_fsync">Задайте 1, за да изключите droiddeck-fsync за тази игра; тогава се използва droiddeck-esync, ако има пакет, или droiddeck-ntsync в противен случай.</string> + <string name="game_env_fsync_first">Задайте 1, за да стартирате тази игра с droiddeck-fsync, или 0, за да поставите droiddeck-esync на първо място, ако има пакет, подходящ за нейния Proton.</string> + <string name="game_env_xalia">Задайте 0, за да изключите помощника на Proton за навигация с контролер за тази игра.</string> + <string name="game_env_hud">Насложен слой на DXVK за D3D9–11. fps показва честотата на кадрите; незададено го скрива.</string> + <string name="game_env_dxvk_config">Настройки на DXVK, разделени с точка и запетая. По-новите версии използват dxvk.maxFrameRate за ограничаване на кадрите; за обикновена игра предпочитайте ограничителя на кадри на сесията.</string> + <string name="game_env_vkd3d_limit">Ограничение на честотата на кадрите за D3D12. 0 изключва ограничението.</string> + <string name="game_env_glthread">Многонишковост на Mesa OpenGL. true може да помогне при игри с OpenGL, ограничени от процесора; тествайте за всяка игра.</string> + <string name="sync_backend_title">Синхронизация на Wine</string> + <string name="sync_backend_hint">Как игрите поддържат нишките си синхронизирани. Прилага се от следващата сесия.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">Предварителна настройка на FEX</string> + <string name="fex_next_launch">Прилага се при следващото стартиране на играта.</string> + <string name="game_settings_title">Игри</string> + <string name="fex_default_label">Стандартни настройки на FEX</string> + <string name="fex_default_detail">Използва стандартните настройки на FEX.</string> + <string name="fex_stability_label">Стабилност</string> + <string name="fex_stability_detail">Най-безопасните и най-бавните настройки.</string> + <string name="fex_compatibility_label">Съвместимост</string> + <string name="fex_compatibility_detail">Пълен TSO с multiblock.</string> + <string name="fex_intermediate_label">Средно</string> + <string name="fex_intermediate_detail">Балансиран TSO с намалена точност на x87.</string> + <string name="fex_performance_label">Производителност</string> + <string name="fex_performance_detail">Целият TSO е изключен; multiblock.</string> + <string name="fex_performance_tso_label">Производителност + TSO</string> + <string name="fex_performance_tso_detail">Настройки за производителност с включен основен TSO.</string> + <string name="fex_extreme_label">Екстремно</string> + <string name="fex_extreme_detail">Изключва проверките за самопроменящ се код. Игри с JIT и DRM може да спрат да работят.</string> + <string name="fex_extreme_tso_label">Екстремно + TSO</string> + <string name="fex_extreme_tso_detail">Екстремни настройки с включен основен TSO.</string> + <string name="fex_extreme_gn_label">Extreme-gn</string> + <string name="fex_extreme_gn_detail">Производителност с TSC и подсказки за метаданни. Пробвайте преди Екстремно.</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">Производителност с пълни проверки на кода и скрит hypervisor бит.</string> + <string name="game_env_custom_value">Персонализирана стойност</string> + <string name="game_env_select_value">Избор на стойност</string> + <string name="user_apps_section">Емулатори и приложения</string> + <string name="user_apps_add">Добави</string> + <string name="user_apps_add_detail">Ваше собствено приложение или игра</string> + <string name="user_apps_kind_script">Скрипт</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d%%</string> + <string name="user_apps_starting">Стартиране…</string> + <string name="user_apps_added">Добавено: %1$s</string> + <string name="user_apps_removed">Премахнато: %1$s</string> + <string name="user_apps_back">Работен плот</string> + <string name="user_apps_gone">Това приложение вече не съществува. Изберете го отново от работния плот.</string> + <string name="user_apps_open">Отвори %1$s</string> + <string name="user_apps_remove">Премахни</string> + <string name="user_apps_remove_confirm">Натиснете отново, за да премахнете</string> + <string name="user_apps_stop_session">Спрете сесията, за да премахнете</string> + <string name="user_apps_script_linked">Стартира %1$s от папката му, свързана към сесията там, където се намира. Премахването на приложението не докосва тази папка.</string> + <string name="user_apps_script_copied">Стартира %1$s от копие на папката му в DroidDeck. Премахването на приложението изтрива копието, а не оригинала.</string> + <string name="user_apps_runtime_required">Първо инсталирайте средата на Linux</string> + <string name="user_apps_file_gone">Файлът вече не съществува</string> + <string name="user_apps_no_space">Няма достатъчно свободно място: копирането на папката изисква %1$s</string> + <string name="user_apps_copying">Копиране на %1$s</string> + <string name="user_apps_failed">Добавянето на приложението не бе успешно</string> + <string name="user_apps_github_checking">Търсене на издание на %1$s</string> + <string name="user_apps_github_unreachable">Изданията на %1$s не можаха да се прочетат</string> + <string name="user_apps_github_no_appimage">Никое издание на %1$s няма AppImage за ARM64 или x86_64</string> + <string name="user_apps_downloading">Изтегляне на %1$s</string> + <string name="user_apps_download_failed">Изтеглянето не бе успешно</string> + <string name="user_apps_update_failed">Актуализирането на приложението не бе успешно</string> + <string name="user_apps_icon_failed">Тази икона не можа да бъде прочетена</string> + <string name="user_apps_saved">Запазено: %1$s</string> + <string name="user_apps_edit">Редактиране</string> + <string name="user_apps_check_updates">Проверка за актуализации</string> + <string name="user_apps_checking_updates">Проверка за актуализации…</string> + <string name="user_apps_update_to">Актуализиране до %1$s</string> + <string name="user_apps_up_to_date">Актуално · %1$s</string> + <string name="user_apps_updated">Актуализирано: %1$s</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">Добавяне на приложение</string> + <string name="add_app_tab_script">Скрипт</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">Файл .sh. Стартира от собствената си папка, на цял екран под gamescope.</string> + <string name="add_app_appimage_hint">AppImage за ARM64 (aarch64) или за x86_64, който се стартира през FEX. Разопакова се в средата на Linux само веднъж.</string> + <string name="add_app_github_hint">Инсталира AppImage за ARM64 от най-новото издание на хранилището, или вариант за x86_64 през FEX.</string> + <string name="app_fex_title">Използване на FEX</string> + <string name="app_fex_auto">Автоматично</string> + <string name="app_fex_on">Включено</string> + <string name="app_fex_off">Изключено</string> + <string name="app_fex_hint">FEX стартира x86 Linux програми на това ARM64 устройство. Автоматично го използва, когато програмата е компилирана за x86.</string> + <string name="app_fex_x86">%1$s през FEX</string> + <string name="app_fex_not_ready">Тази програма е компилирана за %1$s компютри и се стартира през FEX, който още не е настроен. Отворете Steam веднъж, докато сте влезли: DroidDeck ще изтегли FEX и необходимата Steam Linux Runtime, след което тя ще се отвори.</string> + <string name="session_end_program">Тази програма</string> + <string name="session_end_fex_missing">%1$s е компилирана за x86 компютри и се стартира през FEX, който още не е инсталиран. Стартирайте Steam веднъж, докато сте влезли, и го оставете да изтегли FEX и Steam Linux Runtime, след което стартирайте отново.</string> + <string name="add_app_flatpak_hint">ID на приложение в Flathub или страницата му в Flathub. Flatpak се настройва първо, ако още не е.</string> + <string name="add_app_file">Файл</string> + <string name="add_app_no_file">Не е избран файл</string> + <string name="add_app_choose_file">Избор на файл</string> + <string name="add_app_change_file">Промяна</string> + <string name="add_app_pick_script">Изберете скрипт</string> + <string name="add_app_pick_appimage">Изберете AppImage</string> + <string name="add_app_pick_icon">Изберете икона</string> + <string name="add_app_repository">Хранилище</string> + <string name="add_app_repository_placeholder">owner/repo или връзка към github.com</string> + <string name="add_app_repository_invalid">Това не е хранилище в GitHub</string> + <string name="add_app_flatpak_id">ID на приложението</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">Това не е ID на приложение в Flathub</string> + <string name="add_app_flatpak_bundle_hint">Или файл .flatpak за приложение, което не е в Flathub.</string> + <string name="add_app_pick_flatpak">Изберете файл .flatpak</string> + <string name="add_app_source">Източник</string> + <string name="add_app_appearance">На вашия работен плот</string> + <string name="add_app_preview_name">Ново приложение</string> + <string name="add_app_name">Име</string> + <string name="add_app_name_placeholder">От приложението</string> + <string name="add_app_icon">Икона</string> + <string name="add_app_icon_default">Стандартна икона</string> + <string name="add_app_choose_icon">Избор на икона</string> + <string name="add_app_clear_icon">Изчистване</string> + <string name="add_app_icon_suggestions">От хранилището</string> + <string name="add_app_icon_searching">Търсене на икони…</string> + <string name="add_app_icon_reset">Стандартна</string> + <string name="edit_app_title">Редактиране на %1$s</string> + <string name="edit_app_save">Запиши</string> + <string name="add_app_checking_folder">Проверка на папката…</string> + <string name="add_app_folder_linked">Папката му е свързана към сесията: %1$s</string> + <string name="add_app_folder_copied">Папката му съдържа Linux програми, които общото хранилище не може да стартира, затова се копира в DroidDeck (%1$s).</string> + <string name="add_app_confirm">Добави</string> + <string name="add_app_cancel">Отказ</string> + <string name="add_app_runtime_required">Първо инсталирайте средата на Linux от Настройка.</string> + <string name="add_app_busy">Изчакайте %1$s да завърши.</string> + <string name="steam_compat_label">Съвместим</string> + <string name="proton_compatible_note">Версиите, отбелязани с „%1$s“ в менюто за съвместимост на Steam, имат поправки на DroidDeck за префикса и стартират игри тук. По подразбиране е Proton Experimental ARM64 (%1$s), или Proton 11.0 ARM64 (%1$s), след като се инсталира тази по-нова версия. Собствените записи на Proton от Valve без тази отметка не могат да стартират игра на това устройство.</string> + <string name="proton_installed_compatible">Инсталирано %1$s · %2$s</string> + <string name="common_cancel">Отказ</string> + <string name="common_auto">Автоматично</string> + <string name="common_never">Никога</string> + <string name="common_always">Винаги</string> + <string name="common_uninstall">Деинсталиране</string> + <string name="common_check">Проверка</string> + <string name="common_update">Актуализация</string> + <string name="common_checking">Проверка…</string> + <string name="common_working">Обработва се…</string> + <string name="common_forget">Забрави</string> + <string name="common_add_ellipsis">Добави…</string> + <string name="common_use">Използвай</string> + <string name="common_applies_next_session">Прилага се от следващата сесия.</string> + <string name="mode_steam_title">Сесия на Steam</string> + <string name="mode_desktop_title">Сесия на работния плот</string> + <string name="mode_display">Екран</string> + <string name="mode_resolution">Резолюция</string> + <string name="mode_res_720">До 720p</string> + <string name="mode_res_900">До 900p</string> + <string name="mode_res_1080">До 1080p</string> + <string name="mode_res_native">Естествена</string> + <string name="mode_res_default">%1$s (по подразбиране)</string> + <string name="mode_res_custom_value">Персонализирана · %1$d×%2$d</string> + <string name="mode_res_custom">Персонализирана…</string> + <string name="mode_res_note">720p може да направи менютата по-отзивчиви.</string> + <string name="mode_ratio">Пропорции на екрана</string> + <string name="mode_ratio_custom">Определени от персонализираната резолюция.</string> + <string name="mode_ratio_auto">Автоматично използва поне 16:9.</string> + <string name="mode_fps">Ограничение на кадрите</string> + <string name="mode_fps_note">Ограничава цялата сесия. Екранът преминава към честота на опресняване, която се дели точно на ограничението.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">HDR10 изход</string> + <string name="mode_hdr_unavailable">Не е налично: %1$s.</string> + <string name="mode_hdr_restart">Рестартирайте приложението, за да се приложи.</string> + <string name="mode_drivers">Драйвери</string> + <string name="mode_gpu_drivers">GPU драйвери</string> + <string name="mode_gpu_drivers_hint">Общи за Steam и работния плот. Задават се в страницата Компоненти.</string> + <string name="mode_touch_controls">Докосване и контроли</string> + <string name="mode_touch">Докосване</string> + <string name="mode_touch_touchpad">Тъчпад</string> + <string name="mode_touch_direct">Директно</string> + <string name="mode_touch_note">Автоматично: тъчпад на работния плот, директно в Steam.</string> + <string name="mode_osc">Екранни контроли</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">Автоматично показва контролите, когато няма свързан контролер.</string> + <string name="osc_steam_touch">Докосване в Steam (бета)</string> + <string name="osc_style_next_session">Сензорните контроли на Steam ще стартират от следващата сесия на Steam</string> + <string name="osc_steam_layout">Разположение на докосването в Steam</string> + <string name="osc_steam_layout_hint">Премествайте, преоразмерявайте или скривайте сензорните контроли на тази игра; записва се в Steam</string> + <string name="mode_controller">Контролер</string> + <string name="mode_controller_hint">Как Steam разпознава контролера ви. Прилага се от следващата сесия.</string> + <string name="mode_controller_deck">Контролер на Steam Deck</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Контролер Xbox 360</string> + <string name="mode_controller_note">Deck: бутон Quick Access и гироскоп. Steam Controller: същото, с усещане за захват на задните бутони. DualShock 4 и DualSense: гироскоп, а тракпадовете служат като тъчпад. Xbox 360: отворете Quick Access с Guide+A.</string> + <string name="mode_back">Back</string> + <string name="back_menu_then_qam">Меню → QAM</string> + <string name="back_qam_then_menu">QAM → Меню</string> + <string name="mode_session">Сесия</string> + <string name="mode_suspend">Поведение във фонов режим</string> + <string name="mode_suspend_hint">Когато приложението е скрито или екранът се изключи.</string> + <string name="mode_suspend_manual">Ръчно</string> + <string name="mode_suspend_native">Системно</string> + <string name="mode_suspend_steam_note">Автоматично поставя на пауза и продължава, когато се покаже отново. Системно подготвя Steam за заспиване първо. Ръчно изчаква „Продължи“. Никога продължава да работи.</string> + <string name="mode_background_downloads">Продължаване на изтеглянията</string> + <string name="mode_background_downloads_hint">Отлага фоновото спиране, докато Steam активно изтегля.</string> + <string name="mode_suspend_note">Автоматично поставя на пауза във фонов режим и продължава при показване. Ръчно изчаква „Продължи“. Никога продължава да работи.</string> + <string name="mode_startup">Стартиране</string> + <string name="mode_steam_startup">Стартиране на Steam заедно с DroidDeck</string> + <string name="mode_steam_startup_hint">Отваря сесията на Steam при стартиране на DroidDeck.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">Проверка за издания…</string> + <string name="mode_decky_ready">Готово за инсталиране на %1$s.</string> + <string name="mode_decky_none">Не е намерена съвместима версия. Опитайте по-късно.</string> + <string name="mode_decky_installed">Инсталирано · %1$s</string> + <string name="mode_decky_update">Налична актуализация · %1$s</string> + <string name="mode_decky_current">Актуално · %1$s</string> + <string name="mode_decky_loader">Loader</string> + <string name="mode_decky_install_latest">Инсталиране на последната версия</string> + <string name="mode_decky_on">Стартира заедно със Steam. Докато е включено, други приложения на това устройство може да могат да контролират Steam.</string> + <string name="mode_decky_off">Изключено. Портът за отдалечено дебъгване на Steam остава затворен.</string> + <string name="mode_decky_remove_title">Деинсталиране на Decky Loader?</string> + <string name="mode_decky_remove_text">Премахва зареждащия модул. Плъгините и настройките ви се запазват.</string> + <string name="mode_client">Клиент</string> + <string name="mode_mangoapp">Насложен слой за производителност</string> + <string name="mode_mangoapp_hint">Изключете, ако игрите забиват или екранът става черен в режим Deck. Прилага се от следващата сесия.</string> + <string name="mode_steam_repair">Поправка на клиента на Steam</string> + <string name="mode_steam_repair_hint">Изтегля отново клиента на Steam при следващото стартиране на Steam. Използвайте, ако Steam остава на екрана за зареждане. Игрите, влизането в профила и настройките се запазват.</string> + <string name="mode_steam_repair_queued">Клиентът на Steam ще бъде изтеглен отново при следващото стартиране на Steam.</string> + <string name="mode_steam_repair_button">Поправка</string> + <string name="mode_added_art">Изображения</string> + <string name="mode_sgdb_key">API ключ за SteamGridDB</string> + <string name="mode_sgdb_key_yours">Вашият ключ</string> + <string name="mode_sgdb_edit">Редактиране…</string> + <string name="mode_added_art_hint">Попълва липсващите изображения от Steam, а след това от SteamGridDB. Собствените ви файлове cover.jpg, hero.jpg, logo.png или icon.png в папката на играта имат предимство.</string> + <string name="summary_remove_game">Премахване на %1$s</string> + <string name="mode_added_remove_confirm">Премахване?</string> + <string name="games_add">Добавяне на игра</string> + <string name="games_add_title">Добавяне на игра:</string> + <string name="games_pick_exe_title">Изберете .exe файла:</string> + <string name="games_add_unreachable">Това местоположение не е достъпно в сесията</string> + <string name="mode_added_exe_check">Проверка на този .exe файл</string> + <string name="wincomp_title">Компоненти на Windows</string> + <string name="wincomp_pad_hint">LB / RB преминават между разделите.</string> + <string name="wincomp_card_none">Няма</string> + <string name="wincomp_applies">DLL файлове за Windows, които Proton не включва, от списъка с компоненти. Включете тези, от които тази игра се нуждае: те отиват в собствената Windows папка на играта, никога в нейните файлове, и остават независимо на кой Proton се изпълнява тя.</string> + <string name="wincomp_offline">Списъкът с компоненти не можа да се достигне. Вече изтеглените компоненти все още могат да се превключват.</string> + <string name="wincomp_recommended">Препоръчани за тази игра</string> + <string name="wincomp_checking">Проверка какво изисква тази игра…</string> + <string name="wincomp_when_online">Достъпно, когато сте онлайн</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">Включване на %1$d компонент…</item> + <item quantity="other">Включване на %1$d компонента…</item> + </plurals> + <string name="wincomp_auto_section">Включени за тази игра</string> + <string name="wincomp_auto_steam">Steam инсталира %1$s с %2$s (приложение %3$s)</string> + <string name="wincomp_auto_gog">От списъка на GOG за тази игра</string> + <string name="wincomp_auto_epic">От изискванията на Epic за тази игра</string> + <string name="wincomp_auto_amazon">От стъпките за инсталиране на Amazon за тази игра</string> + <string name="wincomp_steam_by_name">Steam инсталира %1$s с приложение %2$s, съпоставено по име</string> + <string name="wincomp_suggestions">Също в списъка</string> + <string name="wincomp_suggestion_reason">Посочено от %1$s; не е компонент тук</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">Играта включва %1$s</string> + <string name="wincomp_found_shipped">Играта вече има %1$s (често работи така)</string> + <string name="wincomp_already_here">Вече в тази игра</string> + <string name="wincomp_no_recommendation">Нищо в папката на играта не насочва към компонент.</string> + <string name="wincomp_active">Инсталирани за тази игра (%1$d)</string> + <string name="wincomp_all">Всички компоненти (%1$d)</string> + <string name="wincomp_installers">Нуждаят се от инсталатор за Windows (%1$d)</string> + <string name="wincomp_installers_note">Това са самостоятелни инсталационни програми без пакет вътре за разглобяване и все още без запис на какво инсталират. Повечето са стари среди за изпълнение, за които Mono в Proton вече служи като заместник; записи се добавят с течение на времето.</string> + <string name="wincomp_show">Показване</string> + <string name="wincomp_hide">Скриване</string> + <string name="wincomp_needs_installer">Изпълнява собствена инсталационна програма (все още без запис)</string> + <string name="wincomp_unsupported">Все още не се поддържа тук</string> + <string name="wincomp_loading">Зареждане на компонентите…</string> + <string name="wincomp_failed">Инсталирането не бе успешно: %1$s</string> + <string name="wincomp_downloaded">Изтеглено</string> + <string name="wincomp_downloading">Изтегляне на %1$s</string> + <string name="wincomp_installing">Инсталиране: %1$s</string> + <string name="wincomp_reverting">Изключване…</string> + <string name="wincomp_download_size">Изтегляне %1$s</string> + <string name="wincomp_next_launch">Влиза в сила при следващото стартиране на играта.</string> + <string name="mode_stretch169">Разтягане на игри в 16:9</string> + <string name="mode_stretch169_hint">На екрани, по-квадратни от 16:9, разтяга игрите, за да запълнят екрана, вместо да показва ленти. Прилага се от следващата сесия.</string> + <string name="mode_audio">Звук</string> + <string name="mode_directaudio">DirectAudio за игри</string> + <string name="mode_directaudio_hint">Пропуска PulseAudio за по-ниска латентност.</string> + <string name="mode_mic">Микрофон</string> + <string name="mode_mic_hint">За гласов чат.</string> + <string name="mode_network">Мрежа</string> + <string name="mode_wifi_discovery">Откриване на Wi-Fi</string> + <string name="mode_wifi_discovery_hint">Показва имената на Wi-Fi и близките мрежи в Steam.</string> + <string name="mode_wifi_location_off">Услугите за местоположение са изключени.</string> + <string name="mode_wifi_permission_denied">Разрешението за местоположение не е предоставено.</string> + <string name="mode_wifi_open_settings">Отваряне на настройките</string> + <string name="mode_wifi_continue">Продължи</string> + <string name="mode_wifi_explain_title">Включване на откриването на Wi-Fi?</string> + <string name="mode_wifi_explain_text">Android изисква разрешение за местоположение, за да прочете имената на Wi-Fi и да открие близките мрежи. DroidDeck използва този достъп, за да показва тези мрежи в Steam. Не следим местоположението ви. Достъпът до интернет работи без това разрешение.</string> + <string name="mode_storage">Хранилище за игри</string> + <string name="mode_storage_auto">Автоматично – SD картата, когато е налична, плюс папките ви Games</string> + <string name="mode_storage_auto_none">Автоматично – папките ви Games, плюс SD картата, когато е налична (в момента няма)</string> + <string name="mode_storage_internal">Само вътрешна памет</string> + <string name="mode_storage_folder">Папка: %1$s</string> + <string name="mode_second_library">Втора библиотека</string> + <string name="mode_storage_note">Игри, които стриймват ресурси от SD картата или общото хранилище, може да заекват.</string> + <string name="mode_storage_diagnostics">Диагностика на хранилището</string> + <string name="mode_storage_diagnostics_hint">Записва резултатите от заделянето и извадкови времена на хранилището в „Споделяне на дневници“. Може да забави инсталациите, докато е включено. Прилага се от следващата сесия.</string> + <string name="mode_choose_folder">Изберете папка…</string> + <string name="mode_custom_res_title">Персонализирана резолюция</string> + <string name="mode_custom_res_text">Заменя ограничението и пропорцията. Размер, който не съвпада с екрана, получава ленти.</string> + <string name="mode_width">Широчина</string> + <string name="mode_height">Височина</string> + <string name="mode_custom_res_range">Между 320×240 и 3840×2160.</string> + <plurals name="mode_added_count"> + <item quantity="one">%1$d игра</item> + <item quantity="other">%1$d игри</item> + </plurals> + <string name="common_ok">ОК</string> + <string name="common_delete">Изтрий</string> + <string name="common_restore">Възстанови</string> + <string name="common_download">Изтегли</string> + <string name="common_delete_named">Изтриване на %1$s</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Драйвер за изпълнение</string> + <string name="comp_runtime_driver_hint">Използва се от Steam, игрите и работния плот. Прилага се от следващата сесия.</string> + <string name="comp_import_turnip">Импортиране на Turnip zip…</string> + <string name="comp_display_driver">Драйвер за дисплея</string> + <string name="comp_display_driver_hint">Изобразява кадрите на екрана в двата режима. Рестартирайте приложението, за да се приложи.</string> + <string name="comp_import_adrenotools">Импортиране на AdrenoTools zip…</string> + <string name="comp_never">никога</string> + <string name="comp_nightlies_checked">Nightlies проверени %1$s</string> + <string name="comp_title">Компоненти</string> + <string name="comp_about">За Компоненти</string> + <string name="comp_check_drivers">Проверка за нови драйвери</string> + <string name="comp_check_nightlies">Проверка на Nightlies за нови пакети</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">Избраният тук Proton стартира всяка игра, която не е избрала свой собствен в Steam, а промяна, направена в Steam, се показва и тук. Steam заменя Proton на Valve при актуализация; оригиналите се запазват за всяка версия.</string> + <string name="comp_scope">Конфигурация</string> + <string name="comp_profile_configure">Конфигуриране на профил</string> + <string name="comp_profile">Профил</string> + <string name="comp_scope_general">Профил по подразбиране</string> + <string name="comp_scope_general_detail">Използва се автоматично от игри без промени.</string> + <string name="comp_profile_game_detail">Записани настройки, използвани автоматично при стартиране на %1$s.</string> + <string name="comp_game_hint">Изберете само настройките, от които тази игра се нуждае. Всичко останало продължава да използва профила по подразбиране.</string> + <string name="comp_game_fex">Предварителна настройка на FEX</string> + <string name="comp_game_fex_inherited">Използва настройката на профила по подразбиране.</string> + <string name="comp_game_fex_override">Персонализирана настройка, запазена за тази игра.</string> + <string name="comp_game_fex_custom">Персонализирани променливи на FEX</string> + <string name="comp_game_use_general">Използване на профила по подразбиране</string> + <string name="comp_game_compatibility">Съвместимост на играта</string> + <string name="comp_game_environment_hint">Настройки на средата за Proton, DXVK, VKD3D и синхронизация за тази игра.</string> + <string name="comp_game_proton_inherited">Използва Proton от профила по подразбиране.</string> + <string name="comp_game_proton_override">Персонализиран Proton, запазен за тази игра чрез таблицата за съвместимост на Steam.</string> + <string name="comp_no_proton_short">Без Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Използва пакета от профила по подразбиране от %1$s, освен ако тази игра избере друг пакет.</string> + <string name="comp_game_component_general">Използва пакета с компоненти от профила по подразбиране.</string> + <string name="comp_game_component_override">Използва %1$s само за тази игра.</string> + <string name="comp_game_runtime_options">Настройки за изпълнение</string> + <string name="comp_game_environment_other_hint">Други настройки на средата за Proton, DXVK, VKD3D-Proton и синхронизация за отделни игри.</string> + <string name="comp_game_saved_next">Профилът на %1$s е запазен за следващото стартиране на играта.</string> + <string name="game_no_steam_title">Старт без Steam</string> + <string name="game_no_steam_detail">За игра, която се затваря веднага: стартира я без настройката на Steam в Proton. Функции на Steam, като оверлея, са изключени за нея.</string> + <string name="game_no_steam_on">Включено</string> + <string name="comp_game_proton_live">Изборът на Proton за %1$s е запазен и вече е достъпен в Steam.</string> + <string name="comp_game_proton_next">Изборът на Proton за %1$s е запазен. Рестартирайте Steam, преди да стартирате играта.</string> + <string name="comp_game_proton_not_runnable">%1$s не може да използва избрания Proton на това устройство.</string> + <string name="comp_game_load_failed">Профилът на %1$s не можа да се зареди: %2$s</string> + <string name="comp_game_save_failed">Профилът на играта не можа да се запази: %1$s</string> + <string name="comp_game_reset">Нулиране на профила на играта</string> + <string name="comp_game_reset_title">Нулиране на профила на %1$s?</string> + <string name="comp_game_reset_body">Премахване на Proton, FEX, пакета с компоненти, средата, филтрирането на текстури и промените в компонентите на Windows за тази игра? Тя ще използва профила по подразбиране.</string> + <string name="comp_game_reset_done">%1$s вече използва профила по подразбиране.</string> + <string name="comp_game_reset_failed">Профилът на играта не можа да се нулира напълно: %1$s</string> + <string name="comp_default_live">%1$s вече стартира всяка игра, която не е избрала свой собствен Proton в Steam.</string> + <string name="comp_default_next">%1$s ще стартира всяка игра, която не е избрала свой собствен Proton, от следващото стартиране на Steam.</string> + <string name="comp_default_not_runnable">%1$s не може да стартира игри тук, затова не може да бъде по подразбиране.</string> + <string name="comp_default_failed">Proton по подразбиране не можа да се запази: %1$s</string> + <string name="comp_swapped">%1$s · %2$d заменени</string> + <string name="comp_all_original">%1$s · всички оригинални</string> + <string name="comp_game_running_suffix">%1$s · играта работи</string> + <string name="comp_esync_pack">%1$s · пакет droiddeck-esync</string> + <string name="comp_esync_wanted">%1$s · все още няма пакет droiddeck-esync</string> + <string name="comp_esync_builtin">%1$s · esync вграден, не е нужен пакет</string> + <string name="comp_gpu_drivers">GPU драйвери</string> + <string name="comp_reapplied">%1$s · приложен отново при стартиране %2$s</string> + <string name="comp_next_queued">Следващо: %1$s · след затваряне на играта</string> + <string name="comp_in_use">%1$s в употреба: %2$s</string> + <string name="comp_waits">Изчаква затварянето на играта</string> + <string name="comp_applies_next_game">Прилага се при следващото стартиране на игра</string> + <string name="comp_import_zip">Импортиране на .zip</string> + <string name="comp_reading">Четене на Proton версиите…</string> + <string name="comp_no_proton">Още няма инсталиран Proton. Стартирайте Steam веднъж, за да изтегли своя ARM64 Proton, или добавете GE-Proton, CachyOS или DroidDeck-Proton от Настройка.</string> + <string name="comp_original">Оригинал · %1$s</string> + <string name="comp_this_build">%1$s · тази версия</string> + <string name="comp_earlier_build">%1$s · по-стара версия</string> + <string name="comp_installed_count">Инсталирани · %1$d</string> + <string name="comp_nothing_stored">Все още няма запазено за %1$s. Собствените файлове на този Proton се запазват като негов оригинал.</string> + <string name="comp_waits_long">Игра работи на този Proton, затова това изчаква затварянето й.</string> + <string name="comp_applies_long">Прилага се при следващото стартиране на игра.</string> + <string name="comp_swap_title">Замяна на %1$s?</string> + <string name="comp_swap_body">Поставяне на %1$s в %2$s? Оригиналните му файлове остават запазени. %3$s</string> + <string name="comp_restore_title">Възстановяване на %1$s?</string> + <string name="comp_restore_body">Поставяне на оригинала на %1$s обратно в %2$s? %3$s</string> + <string name="comp_delete_package_body">Премахва пакета от приложението. Можете да го изтеглите отново.</string> + <string name="comp_delete_original_title">Изтриване на този оригинал?</string> + <string name="comp_delete_original_body">Този оригинал на %1$s е от по-стара версия на %2$s. Оригиналът на текущата версия винаги се запазва.</string> + <string name="comp_nightlies_count">В Nightlies · %1$d</string> + <string name="comp_refresh_to_list">Обновете, за да видите пакетите в Nightlies.</string> + <string name="comp_nothing_new">Нищо ново от последната проверка.</string> + <string name="comp_import_wcp">Импортиране на .wcp</string> + <string name="comp_restore_original">Възстановяване на оригинала на този Proton</string> + <string name="comp_restore_original_body">Поставяне обратно на оригиналния %2$s на %1$s?</string> + <string name="comp_swap">Замяна</string> + <string name="comp_about_text">FEX, DXVK и VKD3D-Proton за всеки Proton, с който Steam стартира игри.\n\nИзберете инсталиран ред, за да го замените. Прилага се при следващото стартиране на игра или след затваряне на текущо работещата игра.\n\nИзборите ви се проверяват повторно преди всяко стартиране и се възстановяват, ако нещо ги е променило. Оригиналните файлове на всеки Proton се запазват, така че актуализациите на Steam никога не ги губят.</string> + <string name="comp_tag_original">Оригинал</string> + <string name="comp_tag_in_use">В употреба</string> + <string name="comp_tag_new">Ново</string> + <string name="comp_tag_stored">Запазено</string> + <string name="perf_title">Производителност</string> + <string name="perf_lede">CPU ядра, корекции на сесията и името на хоста. Прилага се от следващата сесия.</string> + <string name="perf_client_cores_group">Ядра за клиента на Steam</string> + <string name="perf_override">Замяна на избора на ядра на Steam</string> + <string name="perf_override_hint">Фиксира Steam, помощника за интерфейса му и gamescope към ядрата по-долу.</string> + <string name="perf_client_cores">Ядра на клиента</string> + <string name="perf_client_cores_on">Ядра за Steam.</string> + <string name="perf_client_cores_off">Включете замяната, за да изберете.</string> + <string name="perf_game_cores_group">Ядра за игри</string> + <string name="perf_game_cores">Ядра за игри</string> + <string name="perf_game_cores_hint">Ядра за игри. Когато всички са избрани, Android ги разпределя свободно.</string> + <string name="perf_client_ui">Интерфейс на клиента</string> + <string name="perf_glthread">Многонишков GL</string> + <string name="perf_glthread_hint">Може да направи менютата на Steam по-отзивчиви.</string> + <string name="perf_zink">Zink: лениви дескриптори</string> + <string name="perf_zink_hint">Препоръчва се за драйвери без буфери за дескриптори.</string> + <string name="perf_noglerror">Без проверки за GL грешки</string> + <string name="perf_noglerror_hint">Изключва проверката на GL за всяко извикване.</string> + <string name="perf_gsrealtime">gamescope: GPU опашка в реално време</string> + <string name="perf_gsrealtime_hint">Дава приоритет на GPU работата на компоситора. Може да изглади темпото на кадрите с известна цена за игрите.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">Задръжте GPU на максималната му честота</string> + <string name="perf_gpuclock_hint">Само за Adreno. По-малко засечки, по-голямо използване на батерията и повече топлина.</string> + <string name="perf_fixes">Корекции на сесията</string> + <string name="perf_sysmem">Turnip: рендиране sysmem</string> + <string name="perf_sysmem_hint">По-бързо за Steam и повечето игри. Задължително на Adreno 710/720/722.</string> + <string name="perf_xalia">Пропускане на помощника xalia на Steam</string> + <string name="perf_fastsync">droiddeck-ntsync (експериментално)</string> + <string name="perf_fastsync_hint">Заместител на потребителско ниво за драйвера ntsync на Linux, за тестване. Докато е включен, Proton го използва вместо droiddeck-esync и droiddeck-fsync. Работи с Proton Experimental ARM64, GE-Proton и proton-cachyos. Прилага се от следващата сесия.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync първо</string> + <string name="perf_fsync_hint">Стартира собствения fsync на Proton върху оригиналните файлове на Proton, с futex_waitv, подаден от потребителско ниво, затова не изисква пакет и продължава да работи веднага след актуализация на Proton от Steam. Докато е включен, игрите го използват преди droiddeck-esync, освен малкото, които Proton държи извън fsync. Прилага се от следващата сесия.</string> + <string name="perf_esync_hint">Windows нишките на игрите се изчакват взаимно чрез eventfd и обща памет, вместо през обиколки през wineserver. Използва пакет droiddeck-esync, направен точно за тази версия на Proton; пакетите се извличат автоматично. Докато не съществува такъв за нова версия на Proton, droiddeck-fsync го замества. Прилага се от следващата сесия.</string> + <string name="perf_xalia_hint">Изключва помощника на Proton за навигация с контролер, който използва процесорно време във всяка игра.</string> + <string name="perf_seccomp">Стартиране на proot без seccomp</string> + <string name="perf_seccomp_hint">Може да отстрани грешки с липсващи системни извиквания на някои ядра. Може да намали производителността.</string> + <string name="perf_fastpath">Бърз път на proot</string> + <string name="perf_fastpath_needs_seccomp">Изисква филтъра seccomp на proot.</string> + <string name="perf_fastpath_hint">По-бързо търсене на файлове и по-малко заекване. Изключете, ако програма не може да намери файловете си.</string> + <string name="perf_identity">Идентичност на сесията</string> + <string name="perf_hostname">Име на хоста</string> + <string name="perf_hostname_hint">Името, което Steam вижда за това устройство. Оставено празно възстановява %1$s.</string> + <string name="perf_hostname_invalid">Само букви, цифри и тирета по средата, до 63 знака.</string> + <string name="perf_phantom_warning">Android е настроен да прекратява тази сесия</string> + <string name="driver_installed">Инсталиран</string> + <string name="driver_available">Налично за изтегляне</string> + <string name="driver_nothing_new">Нищо ново от последната проверка.</string> + <string name="driver_restore_builtin">Възстановяване на вградените драйвери</string> + <string name="driver_delete_builtin">Вграден в приложението: това премахва разопакованите му файлове и го скрива. „Възстановяване на вградените драйвери“ го връща обратно.</string> + <string name="driver_delete_bundle">Половина от пакет Android + Linux: премахват се и двете половини. Драйвер в употреба се връща към стандартния.</string> + <string name="driver_delete_plain">Файловете му се премахват от приложението.</string> + <string name="driver_delete_in_use">Файловете му се премахват. В употреба е, затова стандартният драйвер поема.</string> + <string name="drawer_page_display">Екран</string> + <string name="drawer_page_effects">Ефекти</string> + <string name="drawer_page_controls">Контроли</string> + <string name="drawer_page_components">Компоненти</string> + <string name="drawer_page_controller">Контролер</string> + <string name="drawer_page_settings">Настройки</string> + <string name="drawer_page_desc">Страница %1$s</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Работен плот</string> + <string name="drawer_steam_menu">Меню на Steam</string> + <string name="drawer_quick_access">Бърз достъп</string> + <string name="drawer_open_qam">Отваряне на менюто за бърз достъп</string> + <string name="drawer_prev_page">Предишна страница</string> + <string name="drawer_next_page">Следваща страница</string> + <string name="drawer_hud">HUD за производителност</string> + <string name="drawer_scaling">Режим на мащабиране</string> + <string name="drawer_scaling_sharpness">Острота на мащабиране</string> + <string name="drawer_effects">Ефекти на екрана</string> + <string name="drawer_effects_hint">На живо. Използва малко GPU време, докато има включен ефект.</string> + <string name="drawer_look">Предварителни настройки</string> + <string name="drawer_look_custom">Персонализирано</string> + <string name="drawer_effects_advanced">Разширени</string> + <string name="widgets_expanded">Разгънато</string> + <string name="widgets_collapsed">Свито</string> + <string name="drawer_cas">Изостряне (CAS)</string> + <string name="drawer_cas_level">Ниво на изостряне</string> + <string name="drawer_fake_hdr">Имитация на HDR</string> + <string name="drawer_deband">Премахване на цветови ленти</string> + <string name="drawer_deband_strength">Сила на премахването на цветови ленти</string> + <string name="drawer_brightness">Яркост</string> + <string name="drawer_contrast">Контраст</string> + <string name="drawer_gamma">Гама</string> + <string name="drawer_saturation">Наситеност</string> + <string name="drawer_fxaa">FXAA (изглаждане на ръбове)</string> + <string name="drawer_toon">Карикатурни контури</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Филтриране на текстури</string> + <string name="drawer_texture_hint">Игри с DirectX 9–11, от следващото им стартиране.</string> + <string name="drawer_anisotropy">Анизотропно филтриране</string> + <string name="drawer_texture_sharpness">Острота на текстурите</string> + <string name="drawer_texture_sharpness_note">Отрицателно отместване на mip нивото: по-остри текстури, леко трептене. Автоматично съответства на степента, до която сесията е увеличена спрямо екрана.</string> + <string name="drawer_touch_auto">Автоматично (%1$s)</string> + <string name="drawer_keyboard">Клавиатура</string> + <string name="drawer_pc_keyboard">Клавиатура за компютър</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">Втори екран</string> + <string name="drawer_second_screen_mode">Режим</string> + <string name="drawer_second_screen_shows">Показва</string> + <string name="drawer_android_apps">Приложения за Android</string> + <string name="drawer_launch_app">Стартиране на приложение</string> + <string name="common_hide">Скриване</string> + <string name="common_show">Показване</string> + <string name="drawer_no_apps">Няма приложения за стартиране</string> + <string name="drawer_session_behavior">Поведение на сесията</string> + <string name="drawer_suspend_hint">Прилага се сега и за бъдещи сесии в този режим.</string> + <string name="drawer_next_session">Следваща сесия</string> + <string name="drawer_support">Поддръжка</string> + <string name="drawer_logs">Дневници на сесията</string> + <string name="drawer_logs_hint">Прикачете към доклад за грешка</string> + <string name="drawer_share_logs">Споделяне на дневници</string> + <string name="drawer_background">Фонов режим</string> + <string name="drawer_stop">Спри</string> + <string name="drawer_stop_title">Спиране на сесията?</string> + <string name="drawer_running_now">В момента работи</string> + <string name="drawer_no_game">Няма игра, работеща на Proton</string> + <string name="drawer_selected_profile">Профил на играта</string> + <string name="drawer_selected_profile_none">Все още няма данни за профила.</string> + <string name="drawer_profile_refresh_failed_initial">Профилите на играта не можаха да се заредят. Автоматично се прави нов опит.</string> + <string name="drawer_profile_refresh_failed_stale">Профилите на играта не можаха да се обновят. Показват се последните налични данни; автоматично се прави нов опит.</string> + <string name="drawer_profile_inspect_hint">Преглед и редактиране на запазените настройки, използвани при следващото стартиране.</string> + <string name="drawer_profile_default">Използва се профилът по подразбиране</string> + <string name="drawer_profile_custom">Персонализиран профил</string> + <string name="drawer_component_inherited">Използва се %1$s от профила по подразбиране.</string> + <string name="drawer_game_components_note">Промените се запазват в профила на %1$s и се прилагат при следващото стартиране на играта.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d промяна за играта</item> + <item quantity="other">%1$d промени за играта</item> + </plurals> + <string name="drawer_no_proton">Все още няма инсталиран Proton.</string> + <string name="drawer_game_running">играта работи</string> + <string name="drawer_reapplied">приложено отново при стартиране %1$s</string> + <string name="drawer_original">Оригинал</string> + <string name="drawer_components_note">Импортиране и изтриване в страницата Компоненти.</string> + <string name="setup_check_done">Готово</string> + <string name="setup_check_warn">Нужно е внимание</string> + <string name="setup_check_busy">Обработва се</string> + <string name="setup_limit_adb_failed">Проверете IP адреса и порта за безжично отстраняване на грешки или сдвоете отново, ако Android е премахнал това устройство.</string> + <string name="setup_install">Инсталиране</string> + <string name="setup_manage">Управление</string> + <string name="setup_tab_overview">Общ преглед</string> + <string name="setup_tab_controller">Контролер</string> + <string name="setup_tab_session">Сесия</string> + <string name="setup_tab_launcher">Лаунчър</string> + <string name="setup_title">Настройка</string> + <string name="setup_all_set">● Всичко е готово</string> + <string name="setup_n_ready">%1$d от %2$d готови</string> + <string name="setup_system_check">Проверка на системата</string> + <string name="setup_gpu_supported">Устройството се поддържа</string> + <string name="setup_gpu_untested">Непроверен GPU</string> + <string name="setup_gpu_unsupported">GPU не се поддържа</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">Steam изисква драйвер за Adreno; %1$s може да покаже черен екран</string> + <string name="setup_runtime">Linux среда за изпълнение</string> + <string name="setup_runtime_progress">%1$s · %2$d%%</string> + <string name="setup_runtime_missing">Не е инсталирано · около 3 GB, инсталира се при първото Play</string> + <string name="setup_installed">Инсталирано</string> + <string name="setup_runtime_update">%1$s · налична актуализация</string> + <string name="setup_runtime_current">%1$s · актуално</string> + <string name="setup_limit">Ограничение на дъщерните процеси</string> + <string name="setup_limit_on">Включено · Android може да затвори Steam. Изключването отнема минута.</string> + <string name="setup_limit_unset">Не е зададено · Android може да затвори Steam. Изключването отнема минута.</string> + <string name="setup_limit_unknown">Не можа да се провери · изключването отнема минута.</string> + <string name="setup_fix_it">Поправи</string> + <string name="setup_details">Подробности</string> + <string name="setup_dev_options">Опции за програмисти</string> + <string name="setup_use_wireless">Използване на безжично отстраняване на грешки</string> + <string name="setup_check_again">Проверка отново</string> + <string name="setup_limit_turn_on">Включване на ограничението</string> + <string name="setup_account">Акаунт в Steam</string> + <string name="setup_signed_in_offline">Влезли сте като %1$s · офлайн режим</string> + <string name="setup_signed_in">Влезли сте като %1$s</string> + <string name="setup_sign_in">Натиснете Play и влезте в Steam</string> + <string name="setup_tools">Инструменти</string> + <string name="setup_controller">Контролер</string> + <string name="setup_controller_unavailable">Настройките на контролера не са достъпни.</string> + <string name="setup_session">Сесия</string> + <string name="setup_logs">Дневници на сесията</string> + <string name="setup_logs_hint">Запазват се след всяка сесия</string> + <string name="setup_agent_commands">Инструменти за отстраняване на грешки</string> + <string name="setup_latest_logs">Последни дневници на сесията</string> + <string name="setup_saved_logs">Запазени дневници на сесията</string> + <string name="setup_saved_logs_hint">Запазва най-новите %1$d</string> + <string name="setup_clear_logs">Изчистване на дневниците</string> + <string name="setup_offline">Офлайн режим</string> + <string name="setup_sign_in_first">Първо влезте в Steam</string> + <string name="setup_launcher">Лаунчър</string> + <string name="setup_language">Език</string> + <string name="setup_language_hint">Steam също го използва, от следващото му стартиране.</string> + <string name="setup_language_system">По подразбиране от системата (%1$s)</string> + <string name="setup_language_system_short">По подразбиране от системата</string> + <string name="setup_app_scale">Мащаб на приложението</string> + <string name="setup_app_scale_hint">Текст и контроли на DroidDeck</string> + <string name="setup_app_scale_default">%1$d%% (по подразбиране)</string> + <string name="setup_theme">Тема</string> + <string name="setup_theme_hint">Облик на лаунчъра</string> + <string name="setup_home">Използване като начален екран</string> + <string name="setup_home_on">DroidDeck може да бъде приложението за начален екран</string> + <string name="setup_home_off">DroidDeck никога не се предлага като приложение за начален екран</string> + <string name="setup_orientation">Ориентация</string> + <string name="setup_orientation_auto">Автоматичен хоризонтален</string> + <string name="setup_orientation_landscape">Хоризонтална</string> + <string name="setup_orientation_reverse">Обърната хоризонтална</string> + <string name="setup_fullscreen">Цял екран</string> + <string name="setup_fullscreen_on">Скрива лентите за състояние и навигация на Android</string> + <string name="setup_fullscreen_off">Показва лентите за състояние и навигация на Android</string> + <string name="setup_default_home">Приложение за начален екран по подразбиране</string> + <string name="setup_choose_home">Изберете приложение за начален екран</string> + <string name="setup_choose">Избор</string> + <string name="setup_linux_apps">Приложения за Linux (бета)</string> + <string name="setup_tool_files">Файлове</string> + <string name="setup_tool_files_hint">Преглед и управление на файлове</string> + <string name="setup_tool_protons">Версии на Proton</string> + <string name="setup_tool_protons_hint">Инсталиране на версии на Proton за ARM64</string> + <string name="setup_tool_performance">Производителност</string> + <string name="setup_tool_performance_hint">Разпределение на CPU ядрата</string> + <string name="setup_tool_roms">Папка за ROM файлове</string> + <string name="setup_tool_roms_hint">Къде се съхраняват игрите за емулатори</string> + <string name="setup_card_components">Компоненти</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Контроли</string> + <string name="setup_card_controls_hint">Разпределение на бутоните</string> + <string name="store_tab_discover">Откриване</string> + <string name="store_tab_search">Търсене</string> + <string name="store_tab_installed">Инсталирани</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Магазин</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak е готов</string> + <string name="store_needs_runtime">Първо инсталирайте средата на Linux от Настройка.</string> + <string name="store_setup_title">Настройване на Flatpak</string> + <string name="store_setup_text">Добавя Flatpak и го свързва с Flathub (около 20 MB). Първото приложение също изтегля своята среда за изпълнение.</string> + <string name="store_setting_up">Настройване…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Актуализации</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d%%</string> + <string name="store_starting">Стартиране…</string> + <string name="store_categories">Категории</string> + <string name="store_unreachable">Flathub не можа да бъде достигнат. Проверете връзката си и опитайте отново.</string> + <string name="store_try_again">Опитайте отново</string> + <string name="store_search_label">Търсене във Flathub</string> + <string name="store_search_placeholder">Firefox, игри, емулатори…</string> + <string name="store_search">Търсене</string> + <string name="store_all">Всички</string> + <string name="store_searching">Търсене…</string> + <string name="store_search_hint">Търсене по име или избор на категория. Показват се само приложения за ARM64.</string> + <string name="store_no_results">Няма приложения за ARM64 във Flathub, съответстващи на това.</string> + <string name="store_results">Резултати</string> + <string name="store_check_updates">Проверка за актуализации</string> + <string name="store_update_all">Актуализиране на всички (%1$d)</string> + <string name="store_all_current">● Всичко е актуално</string> + <string name="store_no_apps">Все още няма приложения. Намерете такива в Откриване или Търсене.</string> + <string name="store_setup_first_note">Настройте Flatpak, за да инсталирате приложения.</string> + <string name="store_update_available_prefix">Налична актуализация · %1$s</string> + <string name="store_open">Отвори</string> + <string name="store_details">Подробности</string> + <string name="store_remove">Премахни</string> + <string name="store_press_again">Натиснете отново, за да премахнете</string> + <string name="store_installing">Инсталиране…</string> + <string name="store_install">Инсталиране</string> + <string name="store_setup_first">Първо настройте Flatpak</string> + <string name="store_download_size">Изтегляне %1$s</string> + <string name="store_installed_chip">● Инсталирано</string> + <string name="store_details_failed">Страницата на това приложение във Flathub не можа да се зареди.</string> + <string name="store_loading">Зареждане от Flathub…</string> + <string name="store_no_arm">Няма версия за ARM64 във Flathub, затова не може да се инсталира тук.</string> + <string name="store_screenshots">Екранни снимки</string> + <string name="store_about">За приложението</string> + <string name="store_app_id">ID на приложението</string> + <string name="store_license">Лиценз</string> + <string name="store_runtime">Среда за изпълнение</string> + <string name="store_installed_size">Инсталиран размер</string> + <string name="store_installed_size_value">%1$s (плюс споделената му среда за изпълнение)</string> + <string name="store_website">Уебсайт</string> + <string name="store_verified">Потвърдено</string> + <string name="upd_title">Актуализации</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Проверено %1$s</string> + <string name="upd_not_checked">Още не е проверено</string> + <string name="upd_perm_title">Разрешаване на DroidDeck да се актуализира самостоятелно</string> + <string name="upd_perm_text">На следващия екран включете „Разрешаване от този източник“, след което се върнете. Актуализацията продължава сама.</string> + <string name="upd_open_settings">Отваряне на настройките</string> + <string name="upd_not_now">Не сега</string> + <string name="upd_signed_status">Различно подписано</string> + <string name="upd_signed_headline">Не може да се актуализира директно</string> + <string name="upd_signed_detail">Това копие не е подписано с ключа за издания на DroidDeck, затова актуализациите не могат да се инсталират върху него. Деинсталирайте го, за да превключите.</string> + <string name="upd_available">Налична актуализация</string> + <string name="upd_blocked_status">Не може да се инсталира</string> + <string name="upd_switch">Готово за превключване</string> + <string name="upd_ahead_status">Пред Stable</string> + <string name="upd_ahead_headline">Вие сте пред Stable</string> + <string name="upd_ahead_detail">Тази версия е по-нова от последната Stable. Ще преминете към Stable с нейното следващо издание.</string> + <string name="upd_gone_status">Тестът приключи</string> + <string name="upd_gone_headline">Този тест приключи</string> + <string name="upd_gone_detail">Поправката му е обединена или изоставена. Следвайте Preview за най-новите поправки.</string> + <string name="upd_current_status">Актуално</string> + <string name="upd_current_headline">Имате най-новата версия %1$s</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Актуализиране%1$s</string> + <string name="upd_install_button">Инсталиране%1$s</string> + <string name="upd_install_anyway">Инсталиране на Stable %1$s все пак</string> + <string name="upd_follow_preview">Следване на Preview</string> + <string name="upd_stop_session">Спрете работещата сесия, за да актуализирате.</string> + <string name="upd_less">По-малко</string> + <string name="upd_more">Повече</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Нова версия на Preview</string> + <string name="upd_new_test">Тестова версия на PR #%1$d</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">КАНАЛ ЗА АКТУАЛИЗАЦИИ</string> + <string name="upd_stable">Stable</string> + <string name="upd_stable_hint">Тествани издания, за повечето хора</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Preview</string> + <string name="upd_preview_hint">Най-новите поправки, преди Stable</string> + <string name="upd_preview_current">Актуално</string> + <string name="upd_tests">Тестови версии</string> + <string name="upd_tests_hint">Изпробвайте поправка, преди да бъде издадена</string> + <string name="upd_tests_none">Няма в момента</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Не е компилирана за това копие на DroidDeck</string> + <string name="upd_updated">Актуализирано %1$s</string> + <string name="upd_test_name">теста на PR #%1$d</string> + <string name="upd_while_ago">преди време</string> + <string name="upd_just_now">току-що</string> + <plurals name="upd_tests_count"> + <item quantity="one">%1$d за изпробване</item> + <item quantity="other">%1$d за изпробване</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="one">Промени от вашата версия · %1$d версия назад</item> + <item quantity="other">Промени от вашата версия · %1$d версии назад</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="one">Скорошни промени · %1$d версия</item> + <item quantity="other">Скорошни промени · %1$d версии</item> + </plurals> + <string name="gate_title">Една настройка на Android за промяна</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Android спира приложения, които стартират много фонови процеси, а Steam стартира десетки. Докато това не се промени, игрите се затварят без грешка.</string> + <string name="gate_fix_for_me">Поправи го вместо мен</string> + <string name="gate_toggle_steps">Включете „Disable child process restrictions“, след което се върнете. Оставете Опции за програмисти включени, иначе превключвателят се изключва отново.</string> + <string name="gate_open_dev">Отваряне на Опции за програмисти</string> + <string name="gate_no_toggle">Тази версия на Android няма превключвател за това, затова DroidDeck го променя чрез безжично отстраняване на грешки. Отнема около минута, не е нужен компютър.</string> + <string name="gate_needs_wifi">Безжичното отстраняване на грешки изисква Wi-Fi. Работи с всяка мрежа; не е необходим интернет.</string> + <string name="gate_needs_notifications">Кодът за сдвояване се показва в известие. Разрешете известията за DroidDeck или използвайте „Други начини“.</string> + <string name="gate_allow_notifications">Разрешаване на известията</string> + <string name="gate_step1">Отворете Настройки → Опции за програмисти → Безжично отстраняване на грешки и го включете</string> + <string name="gate_step2">Докоснете „Сдвояване на устройство с код за сдвояване“</string> + <string name="gate_step3">Въведете кода в известието на DroidDeck</string> + <string name="gate_waiting">Изчакване на изскачащия прозорец за сдвояване…</string> + <string name="gate_code_needed">Изскачащият прозорец е намерен. Въведете кода му в известието.</string> + <string name="gate_status">Състояние</string> + <string name="gate_polling">Проверява се на всеки 2 секунди. Тази страница се затваря, когато е изключено.</string> + <string name="gate_other_ways">Други начини</string> + <string name="gate_other_toggle">Безжично отстраняване на грешки, ръчен адрес, ADB от компютър</string> + <string name="gate_other_no_toggle">Ръчен адрес, ADB от компютър, Опции за програмисти</string> + <string name="gate_use_wireless_instead">Използване на безжично отстраняване на грешки вместо това</string> + <string name="gate_some_roms">Някои ROM-и добавят превключвателя въпреки това. Потърсете „child process“ в Опции за програмисти.</string> + <string name="gate_manual_hint">Ако известието не работи, въведете сами адреса и кода от изскачащия прозорец.</string> + <string name="gate_enter_manually">Ръчно въвеждане на адреса</string> + <string name="gate_from_computer">От компютър с ADB:</string> + <string name="gate_copy_command">Копиране на командата</string> + <string name="gate_choose_display">Изберете на кой екран да се отворят настройките на Android.</string> + <string name="gate_main_screen">Основен екран</string> + <string name="wadb_finding_port">Сдвоено. Търсене на порта за безжично отстраняване на грешки…</string> + <string name="wadb_enter_port">Сдвоено. Въведете порта от Настройки.</string> + <string name="wadb_applying">Устройството е намерено. Прилагане на настройката…</string> + <string name="wadb_apply_failed">Не можа да се приложи автоматично. Проверете адреса по-долу и опитайте отново. %1$s</string> + <string name="wadb_title_wireless">Безжично отстраняване на грешки</string> + <string name="wadb_title_done">Ограничението на дъщерните процеси е актуализирано</string> + <string name="wadb_title">Промяна на ограничението на дъщерните процеси</string> + <string name="wadb_lede_compact0">Ако опцията липсва, сдвоете чрез безжично отстраняване на грешки.</string> + <string name="wadb_lede_compact1">Сдвоено. Проверете IP адреса и порта в безжичното отстраняване на грешки.</string> + <string name="wadb_lede_done">Ограничението на дъщерните процеси беше актуализирано.</string> + <string name="wadb_lede0">Използвайте стъпка 2, ако Опции за програмисти нямат настройка за дъщерни процеси. Отворете Опции за програмисти → Безжично отстраняване на грешки → Сдвояване на устройство с код за сдвояване.</string> + <string name="wadb_lede1">Сдвоено. Използвайте адреса по-долу или го обновете от Безжично отстраняване на грешки → IP адрес и порт.</string> + <string name="wadb_lede_done_long">Ограничението на дъщерните процеси беше актуализирано чрез безжично отстраняване на грешки.</string> + <string name="wadb_confirmed_on">Android потвърди, че ограничението е включено.</string> + <string name="wadb_confirmed_off">Android потвърди, че ограничението е изключено.</string> + <string name="wadb_pair_group">2 · Сдвояване на безжично отстраняване на грешки</string> + <string name="wadb_pair_address">IP адрес и порт на изскачащия прозорец за сдвояване</string> + <string name="wadb_pin">Код за сдвояване (PIN)</string> + <string name="wadb_pin_placeholder">6 цифри</string> + <string name="wadb_pair">Сдвояване</string> + <string name="wadb_apply_group">2 · Прилагане чрез безжично отстраняване на грешки</string> + <string name="wadb_connect_address">IP адрес и порт за връзка</string> + <string name="wadb_pair_again">Сдвояване отново</string> + <string name="wadb_turn_off">Изключване на ограничението</string> + <string name="wadb_other_ways">Други начини за промяна</string> + <string name="wadb_dev_try_first">1 · Опции за програмисти (пробвайте първо)</string> + <string name="wadb_restrict_on">Включете „Restrict child processes“.</string> + <string name="wadb_restrict_off">Изключете „Restrict child processes“.</string> + <string name="wadb_computer_last">3 · ADB от компютър (последна възможност)</string> + <string name="wadb_only_if">Само ако безжичното отстраняване на грешки не е достъпно.</string> + <string name="wadb_computer_group">3 · ADB от компютър (последна възможност)</string> + <string name="ctrl_osc_hint">Кога се появяват екранните контроли в сесия</string> + <string name="ctrl_color">Цвят</string> + <string name="ctrl_color_hint">Оттенък на екранните бутони</string> + <string name="ctrl_custom">Персонализиран</string> + <string name="ctrl_opacity">Прозрачност</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">Размер на бутоните</string> + <string name="ctrl_size_hint" formatted="false">100% е стандартният размер</string> + <string name="ctrl_stick_click">Натискане на стика</string> + <string name="ctrl_stick_click_hint">Докоснете двукратно и задръжте стик за L3 или R3</string> + <string name="ctrl_adaptive">Адаптивни стикове</string> + <string name="ctrl_rumble">Вибрация от играта</string> + <string name="ctrl_steam_button">Бутон Steam</string> + <string name="ctrl_qam_button">Бутон QAM</string> + <string name="ctrl_keyboard_button">Бутон за клавиатура</string> + <string name="ctrl_select_steam">Натиснете Select, за да отворите менюто на Steam</string> + <string name="ctrl_select_steam_hint">Вместо View/Select.</string> + <string name="setup_animations">Анимации</string> + <string name="ctrl_adaptive_hint">Стиковете се появяват там, където докоснете, и се скриват при отпускане</string> + <string name="ctrl_layout">Разположение</string> + <string name="ctrl_layout_custom">Запазени персонализирани позиции</string> + <string name="ctrl_layout_default">Нагласено за този екран</string> + <string name="ctrl_edit">Редактиране</string> + <string name="ctrl_reset">Нулиране</string> + <string name="ctrl_mapping">Разпределение на бутоните</string> + <string name="ctrl_mapping_default">Всеки бутон изпраща собствения си сигнал</string> + <string name="ctrl_mapping_count">%1$d от %2$d бутона преназначени</string> + <string name="ctrl_configure">Конфигуриране</string> + <string name="ctrl_reset_all">Нулиране на контролера</string> + <string name="ctrl_reset_all_hint">Възстановява всички настройки на контролера и разположението</string> + <string name="ctrl_mapping_lede">Какво изпраща всеки екранен бутон към играта. „Скрит“ премахва бутона.</string> + <string name="ctrl_onscreen_buttons">Екранни бутони</string> + <string name="ctrl_defaults">Стандартни настройки</string> + <string name="ctrl_reset_mapping">Нулиране на разпределението</string> + <string name="content_android_apps">Приложения за Android</string> + <string name="content_apps_count">%1$d приложения</string> + <string name="content_no_android_apps">Не са намерени приложения за Android за стартиране.</string> + <string name="content_desktop_installed">● Работният плот е инсталиран</string> + <string name="content_desktop_first_open">Инсталира се при първото отваряне</string> + <string name="content_available">Налично за инсталиране</string> + <string name="content_not_installed">Не е инсталирано.</string> + <string name="content_not_installed_suffix">%1$s · не е инсталирано</string> + <string name="content_open_named">Отвори %1$s</string> + <string name="content_games">Игри</string> + <string name="content_choose_roms">Изберете папка за ROM файлове.</string> + <string name="content_retroarch_roms">Отидете на /root/ROMs в RetroArch.</string> + <string name="content_add_roms">Добавете игри за %1$s в ROMs/%2$s.</string> + <string name="content_install_named">Инсталиране на %1$s</string> + <string name="content_stop_to_install">Спрете сесията, за да инсталирате</string> + <string name="content_runtime_required">Изисква се среда за изпълнение</string> + <string name="content_loading_details">Зареждане на подробности за инсталирането…</string> + <string name="content_details_unavailable">Подробностите за инсталирането не са достъпни. Опитайте по-късно.</string> + <string name="content_needs_runtime">Първо инсталирайте средата на Linux от Настройка.</string> + <string name="content_stop_first">Спрете работещата сесия, за да инсталирате приложения за работния плот.</string> + <string name="content_install_into">Инсталира %1$s в работния плот на Linux.</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">Скриването го премахва от работния плот. Файловете му остават в средата на Linux.</string> + <string name="content_rom_gone">Тази игра вече не е в папката за ROM файлове.</string> + <string name="content_desktop_system">Работен плот · %1$s</string> + <string name="content_launch_in">Стартиране в %1$s</string> + <string name="content_also_in">Също в %1$s</string> + <string name="content_page_gone">Тази страница вече не съществува. Изберете раздел от лявата страна.</string> + <string name="content_open_desktop">Отваряне на работния плот</string> + <string name="content_install_open_desktop">Инсталиране и отваряне на работния плот</string> + <string name="desktop_remove_title">Деинсталиране на работния плот</string> + <string name="desktop_remove_text">Премахва работния плот и включените в него приложения. Запазва игрите, добавените приложения, настройките и споделените библиотеки.</string> + <string name="desktop_removing">Премахване на работния плот</string> + <string name="desktop_removed">Работният плот е премахнат</string> + <string name="desktop_cleaning_legacy">Премахване на старите файлове на LXQt</string> + <string name="desktop_removal_unknown">Актуализирайте DroidDeck, преди да премахнете тази версия на работния плот.</string> + <string name="content_linux_desktop">Работен плот на Linux</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox и вашите емулатори</string> + <string name="games_empty">Игрите, които инсталирате в Steam, се показват тук след сесията.</string> + <string name="games_play_steam">Пускане на Steam</string> + <string name="games_launch_settings">Настройки за стартиране</string> + <string name="games_launch">Стартиране</string> + <string name="games_files">Файлове на играта</string> + <string name="games_prefix">Префикс на Proton</string> + <string name="games_saves_looking">Търсене на записи…</string> + <string name="games_saves_none">Не е намерена папка за записи по име. Експортирането взема цялата потребителска папка.</string> + <string name="games_saves_folders">%1$d папки за записи</string> + <string name="games_saves_summary">%1$s · %2$d файла · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">Управление на записите</string> + <string name="games_saves">Записи на играта</string> + <string name="games_import_saves">Импортиране на записи…</string> + <string name="games_export_gamehub">Експортиране като GameHub zip…</string> + <string name="games_export_winlator">Експортиране като Winlator zip…</string> + <string name="games_open_save_folder">Отваряне на папката за записи</string> + <string name="games_never_played">Никога не е играна</string> + <string name="games_runtime_busy_percent">Средата за изпълнение е заета · %1$d%%</string> + <string name="games_runtime_busy">Средата за изпълнение е заета</string> + <string name="games_runtime_first_play">Средата за изпълнение се инсталира при първото Play</string> + <string name="games_runtime_update">Налична актуализация на средата за изпълнение</string> + <string name="games_runtime_ready">● Средата за изпълнение е готова</string> + <string name="games_last_played">Последна игра %1$s</string> + <string name="games_internal">Вътрешна памет</string> + <string name="games_added">Добавена игра</string> + <string name="games_custom">Персонализирана игра</string> + <string name="games_store_storage">Хранилище за игри</string> + <string name="gpu_auto_hint">Най-добрата двойка за този GPU, поддържана актуална</string> + <string name="gpu_manual_hint">Изберете двойка сами</string> + <string name="gpu_checking">Проверка за драйвери…</string> + <string name="gpu_none_listed">Все още няма изброени драйвери. Обновете, за да ги потърсите.</string> + <string name="gpu_hide_other">Скриване на драйверите за други GPU</string> + <string name="gpu_show_other">Показване на драйверите за други GPU</string> + <string name="gpu_advanced">Разширени</string> + <string name="gpu_set_with_display">Задава се заедно с драйвера за дисплея: %1$s включва и двете.</string> + <string name="gpu_set_with_runtime">Задава се заедно с драйвера за изпълнение: %1$s включва и двете.</string> + <string name="gpu_runtime_hint">Използва се от игрите и работния плот.</string> + <string name="gpu_runtime_default">Стандартен за изпълнение</string> + <string name="gpu_display_hint">Изобразява кадрите на екрана. Прилага се след рестартиране на DroidDeck.</string> + <string name="gpu_auto_by_gpu">Автоматично – избрано според GPU</string> + <string name="gpu_manage">Изтегляне, импортиране или изтриване…</string> + <string name="gpu_in_use_caps">В УПОТРЕБА</string> + <string name="gpu_builtin">Драйверите, вградени в DroidDeck</string> + <string name="gpu_matched_pair">Драйверите за изпълнение и за дисплея са зададени заедно като съответстваща двойка.</string> + <string name="gpu_recommended_for">Препоръчан за този GPU: %1$s.</string> + <string name="gpu_refresh_hint">Обновете, за да потърсите драйвери за този GPU.</string> + <string name="gpu_recommended_caps">ПРЕПОРЪЧАНО</string> + <string name="gpu_unavailable">Недостъпно</string> + <string name="load_runtime">Linux среда за изпълнение</string> + <string name="load_runtime_detail">Подготвяне на средата на Linux</string> + <string name="load_session">Сесия</string> + <string name="load_session_detail">Стартиране на екрана и звука</string> + <string name="load_steam">Клиент на Steam</string> + <string name="load_steam_detail">Проверка на Steam и инструментите му за съвместимост</string> + <string name="load_opening_steam">Отваряне на Steam</string> + <string name="load_opening_steam_detail">Изчакване на първия кадър на Steam</string> + <string name="load_desktop">Работен плот</string> + <string name="load_desktop_detail">Подготвяне на работния плот</string> + <string name="load_opening">Отваряне</string> + <string name="load_opening_detail">Изчакване на първия кадър</string> + <string name="load_starting_steam">Стартиране на Steam</string> + <string name="load_ended">Сесията приключи</string> + <string name="load_step">Стъпка %1$d от %2$d</string> + <string name="load_steam_paused">Steam е на пауза</string> + <string name="deck_tab_grips">Захвати</string> + <string name="deck_tab_left">Ляв пад</string> + <string name="deck_tab_right">Десен пад</string> + <string name="deck_tab_both">Двата</string> + <string name="deck_tab_gamepad">Геймпад</string> + <string name="deck_surface_stick">Стик</string> + <string name="deck_surface_trackpad">Тракпад</string> + <string name="deck_surface_switch">Превключване между стик и тракпад</string> + <string name="deck_more_controls">Още контроли на Deck</string> + <string name="deck_panels">ПАНЕЛИ</string> + <string name="deck_close">Затваряне на контролите за втория екран</string> + <string name="deck_open_steam_menu">Отваряне на менюто на Steam</string> + <string name="deck_left_upper">Ляво · горе</string> + <string name="deck_left_lower">Ляво · долу</string> + <string name="deck_right_upper">Дясно · горе</string> + <string name="deck_right_lower">Дясно · долу</string> + <string name="deck_click">Клик</string> + <string name="deck_right_trackpad">Десен тракпад</string> + <string name="deck_left_trackpad">Ляв тракпад</string> + <string name="second_trackpad">Тракпад</string> + <string name="second_show_keyboard">Показване на клавиатурата</string> + <string name="second_hide_keyboard">Скриване на клавиатурата</string> + <string name="second_terminal">Терминал на Linux</string> + <string name="second_terminal_not_ready">Сесията на Linux все още не е готова за терминал.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Игри</string> + <string name="rail_desktop">Desktop</string> + <string name="rail_store">Магазин</string> + <string name="rail_components">Компоненти</string> + <string name="rail_apps">Приложения</string> + <string name="rail_setup">Настройка</string> + <string name="rail_updates">Актуализации</string> + <string name="rail_percent">%1$d%%</string> + <string name="rail_resume_named">Продължаване на %1$s</string> + <string name="roms_text">Показва се като /root/ROMs във всяка сесия. Вътрешната памет е на /root/Storage.</string> + <string name="roms_none">Няма избрана папка</string> + <string name="roms_applies">Прилага се от следващата сесия. SD картите работят.</string> + <string name="roms_choose">Избор на папка</string> + <string name="roms_change">Смяна на папка</string> + <string name="proton_lede">Инсталирай ARM64 версия на Proton, после я избери за всяка игра в Steam → Properties → Compatibility.</string> + <string name="proton_stop_first">Спри текущата сесия, за да инсталираш или премахнеш инструмент за съвместимост.</string> + <string name="proton_available">Налични версии</string> + <string name="proton_installed">Инсталирано: %1$s</string> + <string name="proton_queued">Чака от по-ранна заявка</string> + <string name="proton_not_installed">Не е инсталирано</string> + <string name="proton_update_available">Налична актуализация → %1$s</string> + <string name="proton_remove_older">Премахване на старите версии на %1$s</string> + <string name="proton_remove_older_body">Премахва %1$s. Игрите, зададени на тези версии, трябва да бъдат превключени към друг Proton.</string> + <string name="proton_remove_body">Игрите, зададени на тази версия, трябва да бъдат превключени към друг Proton.</string> + <string name="proton_install_now">Инсталиране сега</string> + <string name="proton_cancel_queued">Отказ на изчакването</string> + <string name="pckb_android">Клавиатура на Android</string> + <string name="pckb_hide">Скриване ✕</string> + <string name="main_launch_named">Пускане на %1$s</string> + <string name="main_choose_screen">Избери екран. Втори: %1$s.</string> + <string name="main_secondary_gone">Вторият екран вече не е достъпен.</string> + <string name="main_primary">Основен екран</string> + <string name="main_secondary">Втори екран</string> + <string name="widgets_prev_tab">Предишен раздел</string> + <string name="widgets_next_tab">Следващ раздел</string> + <string name="widgets_on">Вкл.</string> + <string name="widgets_all_n">Всички %1$d</string> + <string name="widgets_none">Нищо</string> + <string name="widgets_n_of_m">%1$d от %2$d</string> + <string name="widgets_off">Изкл.</string> + <string name="screen_bottom">Долен екран</string> + <string name="game_env_applies">Прилага се при следващото пускане на играта. Профилът на играта има превес над общия.</string> + <string name="game_env_other">Друга игра по ID на приложението…</string> + <string name="game_env_done">Готово</string> + <string name="game_env_default">По подразбиране</string> + <string name="game_env_off">Изкл.</string> + <string name="game_env_change">Смяна…</string> + <string name="game_env_change_title">Промяна на променлива</string> + <string name="game_env_restore_default">Връщане по подразбиране</string> + <string name="game_env_eyebrow">Proton игри</string> + <string name="pip_title">Картина в картината</string> + <string name="pip_auto">Автоматично влизане в „Картина в картината“</string> + <string name="unfolded_controls">Контроли на разгънатия екран</string> + <string name="unfolded_controls_hint">Разгънат, играта стои над тракпадите и задните захвати на Steam Deck. Сгънат, играта е на цял екран.</string> + <string name="pip_suspended">Прекъсната</string> + <string name="pip_not_ready">Картина в картината е достъпна само когато сесията е готова.</string> + <string name="pip_permission">Разреши картина в картината за DroidDeck в настройките на Android.</string> + <string name="pip_unavailable">Android не можа да отвори картина в картината.</string> + <string name="game_shortcut">Пряк път</string> + <string name="game_shortcut_add">Добавяне към началния екран</string> + <string name="game_shortcut_choose">Избор на игра</string> + <string name="game_shortcut_scanning">Търсене на инсталирани игри…</string> + <string name="game_link_copy">Копиране на връзката за пускане</string> + <string name="game_link_copied">Връзката за пускане е копирана</string> + <string name="game_shortcut_unsupported">Текущият начален екран не поддържа преки пътища. Използвай „Копиране на връзката за пускане“ вместо това.</string> + <string name="game_link_invalid">Невалидна връзка за пускане на игра.</string> + <string name="game_link_missing">Тази игра вече не е в библиотеката ти в DroidDeck.</string> + <string name="game_link_session_busy">Спри текущата сесия, преди да пуснеш игра в Steam.</string> + <string name="game_link_failed">Играта не можа да се изпрати в Steam. Опитай да я пуснеш отново.</string> + <string name="setup_retry_removal">Нов опит за премахване</string> + <string name="runtime_removing">Премахване</string> + <string name="runtime_removal_incomplete">Премахването не е завършено</string> + <string name="game_file_export">Експортиране на файл на играта</string> + <string name="game_file_sync">Синхронизиране на всички файлове на игрите в папка</string> + <string name="game_file_stop_sync">Спиране на синхронизирането на файловете на игрите</string> + <string name="game_file_folder">Избор на папка за файловете на фронтенда на игрите</string> + <string name="game_file_exported">Файловете на игрите са запазени в %1$s</string> + <string name="game_frontend_files">Файлове на фронтенда</string> + + <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> + <string name="main_saves_importing">Импортиране в %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="one">Импортиран %1$d файл от %2$s в %3$s</item> + <item quantity="other">Импортирани %1$d файла от %2$s в %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">Импортиран %1$d файл от %2$s в %3$s. Старите запазвания са архивирани в Download/DroidDeck/Saves/backups</item> + <item quantity="other">Импортирани %1$d файла от %2$s в %3$s. Старите запазвания са архивирани в Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Избор на ZIP файл със запазвания за %1$s</string> + <string name="main_saves_exporting">Експортиране на %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="one">Експортиран %1$d файл като %2$s: %3$s</item> + <item quantity="other">Експортирани %1$d файла като %2$s: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Избор на папка за запазване на %1$s (%2$s)</string> + <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> + <plurals name="main_logs_cleared"> + <item quantity="one">Изчистена %1$d папка с логове от сесия.</item> + <item quantity="other">Изчистени %1$d папки с логове от сесии.</item> + </plurals> + <string name="main_wadb_pair_failed">Чифтосването за безжично отстраняване на грешки се провали</string> + <string name="main_wadb_command_failed">Командата за безжично отстраняване на грешки се провали</string> + <string name="main_wadb_unavailable">Безжичното отстраняване на грешки не е достъпно</string> + <string name="main_adb_copied">Командата за ADB е копирана</string> + <string name="main_pick_roms">Избор на папката ROMs</string> + <string name="main_non_adreno_title">Не е Adreno GPU</string> + <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> + <string name="main_applied">Приложено: %1$s</string> + <string name="android_app_open_failed_primary">%1$s не можа да се отвори на основния екран</string> + <string name="android_app_open_failed_on">%1$s не можа да се отвори на %2$s</string> + <string name="android_app_open_failed_display">%1$s не можа да се отвори на екран %2$d</string> + <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> + <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> + <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> + <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">Избор на ZIP с плъгин за Decky</string> + <string name="main_storage_not_usable">Не може да се използва: %1$s</string> + <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> + + <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> + <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> + + <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> + + <string name="drivers_not_checked">Все още не е проверено - докосни опресняване, за да провериш за нови драйвери</string> + <string name="drivers_bundle_sets_display">Android + Linux: задава драйвера за дисплея заедно с него</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: задава драйвера за средата заедно с него</string> + <string name="drivers_runtime_builtin">вграденият в средата Turnip</string> + <string name="drivers_system_vulkan">системен Vulkan: няма вградена версия за този GPU</string> + <string name="drivers_bundled">%1$s (включен)</string> + <string name="drivers_manual">Ръчно</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">В момента е публикувана само една от двете части</string> + <string name="drivers_pair_size">%1$.0f MB за двете</string> + <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> + <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> + <string name="drivers_switched_to">Превключено към %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Превключено към %1$s %2$s. Драйверът за дисплея се прилага след рестартиране на DroidDeck.</string> + <string name="drivers_using">Използва се %1$s %2$s.</string> + <string name="drivers_using_restart">Използва се %1$s %2$s. Драйверът за дисплея се прилага след рестартиране на DroidDeck.</string> + <string name="drivers_not_a_driver">Не е ZIP с драйвер: нито драйвер на AdrenoTools, нито -Linux Turnip, нито пакет Android + Linux</string> + <string name="drivers_import_failed">Импортирането се провали: %1$s</string> + <string name="drivers_imported_unnamed">Импортиран драйвер</string> + <string name="drivers_using_bundle">Използва се %1$s като драйвер и за средата, и за дисплея.</string> + <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> + <string name="drivers_release_status">Най-нови: %1$s · проверено %2$s</string> + <string name="drivers_release_status_failed">Най-нови: %1$s · %2$s недостъпни · проверено %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">преди %1$d мин</item> + <item quantity="other">преди %1$d мин</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">преди %1$d ч</item> + <item quantity="other">преди %1$d ч</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">преди %1$d ден</item> + <item quantity="other">преди %1$d дни</item> + </plurals> + <string name="drivers_checking_sources">Проверка на Banners-Turnip, WinNative и DroidDeck…</string> + <string name="drivers_check_failed">проверката се провали</string> + <string name="drivers_installed_pick_either">Инсталирано: %1$s - избери го в който и да е от списъците, за да зададеш и двата драйвера</string> + <string name="drivers_installed_pick">Инсталирано: %1$s - избери го от менюто</string> + + <string name="session_wait_removal">Изчакай премахването на средата да приключи</string> + <string name="session_title_game">Игра</string> + <string name="session_touch_auto_touchpad">тракпад</string> + <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> + <string name="session_named_paused">%1$s е на пауза</string> + <string name="session_program_paused">Програмата е на пауза</string> + <string name="session_starting_desktop">Стартиране на работния плот</string> + <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> + <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> + <string name="session_runtime_unpacking">разопаковане на средата на Linux</string> + <string name="session_desktop_downloading">изтегляне на работния плот</string> + <string name="session_desktop_downloading_mb">изтегляне на работния плот · %1$d от %2$d MB</string> + <string name="session_desktop_checking">проверка на работния плот</string> + <string name="session_desktop_unpacking">разопаковане на работния плот</string> + <string name="session_proton_downloading">изтегляне на Proton Experimental (ARM64)</string> + <string name="session_proton_downloading_mb">изтегляне на Proton Experimental (ARM64) · %1$d от %2$d MB</string> + <string name="session_proton_checking">проверка на Proton Experimental (ARM64)</string> + <string name="session_proton_unpacking">разопаковане на Proton Experimental (ARM64)</string> + <string name="session_runtime_catalog_unreachable">Каталогът на средата не можа да бъде достигнат. Провери връзката и натисни „Пускане“ отново.</string> + <string name="session_runtime_install_failed">Средата на Linux не се инсталира. Провери средата в „Настройка“ и натисни „Пускане“ отново.</string> + <string name="session_desktop_install_failed">Работният плот не се инсталира (%1$s). Провери връзката и натисни „Desktop“ отново.</string> + <string name="session_ended_named">%1$s спря неочаквано. Сподели логовете в отчет за грешка или опитай отново.</string> + <string name="session_ended_program">Програмата спря неочаквано. Сподели логовете в отчет за грешка или опитай отново.</string> + <string name="session_ended_desktop">Работният плот спря неочаквано. Сподели логовете в отчет за грешка или опитай отново.</string> + <string name="session_ended_detail">Код на изход %1$d · %2$s</string> + <string name="session_ended_no_log">няма лог</string> + <plurals name="session_hint_enosys"> + <item quantity="one">Логът показва %1$d грешка „Function not implemented“: ускорението на proot чрез seccomp се проваля при помощен процес на това устройство. Пробвай %2$s → „%3$s“ (или „%4$s“) и стартирай отново.</item> + <item quantity="other">Логът показва %1$d грешки „Function not implemented“: ускорението на proot чрез seccomp се проваля при помощен процес на това устройство. Пробвай %2$s → „%3$s“ (или „%4$s“) и стартирай отново.</item> + </plurals> + + <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> + <string name="store_cat_games">Игри</string> + <string name="store_cat_audio_video">Аудио и видео</string> + <string name="store_cat_graphics">Графика</string> + <string name="store_cat_internet">Интернет</string> + <string name="store_cat_office">Офис</string> + <string name="store_cat_developer">Разработка</string> + <string name="store_cat_education">Образование</string> + <string name="store_cat_science">Наука</string> + <string name="store_cat_system">Система</string> + <string name="store_cat_utilities">Инструменти</string> + <string name="store_section_popular">Популярни</string> + <string name="store_section_trending">Тенденции</string> + <string name="store_section_new">Нови</string> + <string name="store_section_updated">Актуализирани</string> + <string name="store_section_loading">зареждане…</string> + <string name="store_flatpak_setup">Настройка на Flatpak</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Flathub не можа да бъде проверен за актуализации</string> + <string name="content_rom_installed">инсталирана</string> + <string name="content_rom_folder">папка</string> + <string name="theme_graphite">Графит</string> + <string name="theme_graphite_detail">Графитени повърхности, със собственото синьо на сферата от логото за всичко избрано, на фокус или натиснато.</string> + <string name="theme_paper">Хартия на черно</string> + <string name="theme_paper_detail">Бялото е основният цвят, синьото от иконата е сигналът за линии, кръгове и точки.</string> + <string name="theme_icon_blue">Синя икона</string> + <string name="theme_icon_blue_detail">Черен фон, бял текст, синьото за всичко избрано, на фокус или натиснато.</string> + <string name="theme_electric">Яркосиньо</string> + <string name="theme_electric_detail">По-яркото синьо, върху повърхности, по-близки до тъмносиньо, вместо сиво.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Space</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <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> + <string name="content_system_many">Много системи</string> + <string name="fm_drive_internal">Вътрешна</string> + <string name="fm_drive_linux">Система на Linux</string> + <string name="fm_drive_sd">SD карта</string> + <string name="fm_drive_storage">Хранилище</string> + <string name="fm_select">Избор</string> + <string name="fm_properties">Свойства</string> + <string name="fm_favorite_add">Добавяне към Любими</string> + <string name="fm_favorite_remove">Премахване от Любими</string> + <string name="fm_rename">Преименуване</string> + <string name="fm_copy">Копиране</string> + <string name="fm_cut">Изрязване</string> + <string name="fm_actions">Действия</string> + <string name="fm_favorites">Любими</string> + <string name="fm_pin_current">Закачане на текущата папка</string> + <string name="fm_no_favorites">Още няма любими - закачи папка от менюто ѝ ⋮, за да се връщаш тук бързо.</string> + <string name="fm_unpin_favorite">Премахване от любими</string> + <string name="fm_size_zero">0 байта</string> + <string name="fm_unit_bytes">байта</string> + <string name="fm_prop_location">Местоположение</string> + <string name="fm_prop_type">Тип</string> + <string name="fm_prop_size">Размер</string> + <string name="fm_prop_modified">Последна промяна</string> + <string name="fm_type_folder">Папка</string> + <string name="fm_type_file">Файл</string> + <string name="fm_type_ext_file">%1$s файл</string> + <string name="fm_read_only">Само за четене</string> + <string name="fm_read_only_hint">Пречи на игрите (и Wine) да презаписват или изтриват този файл.</string> + <string name="fm_read_only_failed">Статусът „Само за четене“ не можа да се промени</string> + <string name="fm_hidden">Скрит</string> + <string name="fm_hidden_unsupported">Това хранилище не може да съхранява флага за скрит файл.</string> + <string name="fm_hidden_hint">Отбелязва файла като скрит в Windows (атрибутът за скрити файлове на Wine).</string> + <string name="fm_hidden_failed">Статусът „Скрит“ не можа да се промени за това хранилище</string> + <string name="fm_done">Готово</string> + <string name="fm_op_deleting">Изтриване...</string> + <string name="fm_delete_failed">Изтриването се провали</string> + <string name="fm_op_moving">Преместване...</string> + <string name="fm_op_copying">Копиране...</string> + <string name="fm_op_moving_item">Преместване - %1$s</string> + <string name="fm_op_copying_item">Копиране - %1$s</string> + <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> + <string name="fm_paste_failed">%1$d успешни, %2$d неуспешни</string> + <string name="fm_paste_skipped">%1$d успешни, %2$d пропуснати</string> + <plurals name="fm_paste_moved"> + <item quantity="one">%1$d елемент преместен</item> + <item quantity="other">%1$d елемента преместени</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">%1$d елемент копиран</item> + <item quantity="other">%1$d елемента копирани</item> + </plurals> + <string name="fm_already_exists">„%1$s“ вече съществува</string> + <string name="fm_op_renaming">Преименуване...</string> + <string name="fm_rename_failed">Преименуването се провали</string> + <string name="fm_op_creating_folder">Създаване на папка...</string> + <string name="fm_create_folder_failed">Папката не можа да се създаде</string> + <string name="fm_new_folder">Нова папка</string> + <string name="fm_folder_name">Име на папката</string> + <string name="fm_create">Създаване</string> + <string name="fm_new_name">Ново име</string> + <string name="fm_delete_title">Изтриване?</string> + <string name="fm_delete_text">Изтриване на „%1$s“ завинаги?</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">Изтриване на %1$d елемент?</item> + <item quantity="other">Изтриване на %1$d елемента?</item> + </plurals> + <string name="fm_cannot_undo">Това не може да бъде отменено.</string> + <plurals name="fm_and_more"> + <item quantity="one">…и още %1$d</item> + <item quantity="other">…и още %1$d</item> + </plurals> + <string name="fm_op_deleting_batch">Изтриване %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">%1$d не можа да бъде изтрит</item> + <item quantity="other">%1$d не можаха да бъдат изтрити</item> + </plurals> + <string name="fm_conflict_merge_hint">Сливането добавя и заменя файлове в съществуващата папка.</string> + <string name="fm_conflict_overwrite_hint">Презаписването заменя съществуващия файл.</string> + <string name="fm_conflict_apply_all">Приложи за всички конфликти</string> + <string name="fm_conflict_merge">Сливане</string> + <string name="fm_conflict_overwrite">Презаписване</string> + <string name="fm_conflict_keep_both">Запазване на двете</string> + <string name="fm_conflict_skip">Пропускане</string> + <string name="fm_select_this_folder">Избор на тази папка</string> + <string name="fm_back">Назад</string> + <string name="fm_drive_not_readable">%1$s е монтирано, но в момента не може да се прочете</string> + <string name="fm_list_view">Изглед като списък</string> + <string name="fm_grid_view">Изглед като мрежа</string> + <string name="fm_search">Търсене</string> + <string name="fm_sort">Сортиране</string> + <string name="fm_sort_name">Име</string> + <string name="fm_sort_date">Дата на промяна</string> + <string name="fm_rows_comfortable">Удобни редове</string> + <string name="fm_rows_compact">Компактни редове</string> + <string name="fm_hide_hidden">Скриване на скритите файлове</string> + <string name="fm_show_hidden">Показване на скритите файлове</string> + <string name="fm_hide_favorites">Скриване на любимите</string> + <string name="fm_show_favorites">Показване на любимите</string> + <string name="fm_filter_folder">Филтриране на тази папка</string> + <string name="fm_free_space">%1$s свободни</string> + <string name="fm_selected_count">%1$d избрани</string> + <string name="fm_select_none">Нищо</string> + <string name="fm_select_all">Всички</string> + <string name="fm_paste">Поставяне</string> + <plurals name="fm_items"> + <item quantity="one">%1$d елемент</item> + <item quantity="other">%1$d елемента</item> + </plurals> + <string name="fm_paste_here">Поставяне на %1$s тук</string> + <string name="fm_paste_move_here">Поставяне на %1$s (премести) тук</string> + <string name="fm_rail_title">Файлове</string> + <string name="fm_rail_storage">ХРАНИЛИЩЕ</string> + <string name="fm_rail_quick">БЪРЗИ</string> + <string name="fm_rail_favorites">ЛЮБИМИ</string> + <string name="fm_rail_downloads">Изтеглени</string> + <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> + <string name="fm_favorite_added">„%1$s“ е добавена към Любими</string> + <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> + <string name="ctrl_tint_steam_blue">Синьо на Steam</string> + <string name="ctrl_tint_sky">Небесно</string> + <string name="ctrl_tint_white">Бяло</string> + <string name="ctrl_tint_violet">Виолетово</string> + <string name="ctrl_tint_green">Зелено</string> + <string name="ctrl_tint_amber">Кехлибарено</string> + <string name="ctrl_tint_red">Червено</string> + <string name="ctrl_tint_pink">Розово</string> + <string name="ctrl_button_a">Бутон A</string> + <string name="ctrl_button_b">Бутон B</string> + <string name="ctrl_button_x">Бутон X</string> + <string name="ctrl_button_y">Бутон Y</string> + <string name="ctrl_button_lb">Ляв бутон (LB)</string> + <string name="ctrl_button_rb">Десен бутон (RB)</string> + <string name="ctrl_button_lt">Ляв спусък (LT)</string> + <string name="ctrl_button_rt">Десен спусък (RT)</string> + <string name="ctrl_button_view">Бутон View</string> + <string name="ctrl_button_menu">Бутон Menu</string> + <string name="ctrl_target_view">View</string> + <string name="ctrl_target_menu">Menu</string> + <string name="ctrl_target_up">D-pad нагоре</string> + <string name="ctrl_target_down">D-pad надолу</string> + <string name="ctrl_target_left">D-pad наляво</string> + <string name="ctrl_target_right">D-pad надясно</string> + <string name="ctrl_target_hidden">Скрит</string> + <string name="ctrl_editor_drag">Плъзни, за да преместиш</string> + <string name="ctrl_editor_snap">Прилепване</string> + <string name="ctrl_editor_save">Запазване</string> + <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> + + <string name="rtinst_removing">Премахване на средата на Linux</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">Премахване на средата на Linux · %1$d запис</item> + <item quantity="other">Премахване на средата на Linux · %1$d записа</item> + </plurals> + <string name="rtinst_removed">Средата на Linux е премахната</string> + <string name="rtinst_removal_failed">Премахването на средата на Linux не можа да завърши. Опитай отново от „Настройка“.</string> + <string name="rtinst_removing_leftovers">Премахване на остатъците от средата</string> + <string name="rtinst_downloading">Изтегляне</string> + <string name="rtinst_verifying">Проверка</string> + <string name="rtinst_extracting">Разопаковане</string> + <string name="rtinst_channel_description">Показва се, докато средата на Linux се инсталира или премахва</string> + <string name="rtinst_title_installing">Инсталиране на средата на Linux</string> + <string name="rtinst_title_removing">Премахване на средата на Linux</string> + + <string name="deskpkg_runtime_missing">Средата на Linux не е инсталирана</string> + <string name="deskpkg_no_checksum">Каталогът няма контролна сума за %1$s</string> + <string name="deskpkg_checksum_mismatch">Несъответствие в контролната сума - нищо не е променено</string> + <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> + + <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> + <string name="flatpakmgr_installing">Инсталиране на %1$s</string> + <string name="flatpakmgr_updating">Актуализиране на %1$s</string> + <string name="flatpakmgr_removing">Премахване на %1$s</string> + <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> + <string name="flatpakmgr_ref_translations">преводи</string> + <string name="flatpakmgr_ref_graphics_drivers">драйвери за графика</string> + <string name="flatpakmgr_ref_media_codecs">медийни кодеци</string> + <string name="flatpakmgr_ref_runtime">средата %1$s</string> + <string name="flatpakmgr_ref_runtime_branch">средата %1$s %2$s</string> + + <string name="appimg_too_small">Файлът е твърде малък, за да бъде AppImage</string> + <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">Могат да се импортират само AppImage от тип 2 (този е от тип %1$d)</string> + <string name="appimg_no_space_import">Няма достатъчно свободно място: импортирането изисква около %1$s</string> + <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> + <string name="appimg_extracting">Разопаковане на %1$s</string> + <string name="appimg_extract_failed">AppImage не можа да бъде разопакован</string> + <string name="appimg_extract_failed_detail">AppImage не можа да бъде разопакован: %1$s</string> + + <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> + <string name="deckymgr_checksum_download_failed">Контролната сума на версията не можа да бъде изтеглена</string> + <string name="deckymgr_checksum_missing">Липсва контролна сума на версията</string> + <string name="deckymgr_checksum_invalid">Контролната сума на версията е невалидна</string> + <string name="deckymgr_checksum_mismatch">Несъответствие в контролната сума на Decky Loader</string> + <string name="deckymgr_not_elf">Файлът на версията не е съвместим 64-битов изпълним файл за Linux</string> + <string name="deckymgr_session_started_loader">По време на изтеглянето започна сесия - спри я, преди да инсталираш Decky Loader</string> + <string name="deckymgr_loader_not_executable">PluginLoader не можа да бъде означен като изпълним</string> + <string name="deckymgr_cef_failed">Отстраняването на грешки от разстояние на Steam CEF не можа да бъде включено</string> + <string name="deckymgr_stage_failed">PluginLoader не можа да бъде подготвен</string> + <string name="deckymgr_loader_preserve_failed">Съществуващият PluginLoader не можа да бъде запазен</string> + <string name="deckymgr_loader_activate_failed">Новият PluginLoader не можа да бъде активиран</string> + <string name="deckymgr_stop_session_plugin">Спри активната сесия, преди да инсталираш плъгин</string> + <string name="deckymgr_loader_required">Инсталирай Decky Loader, преди да импортираш плъгин</string> + <string name="deckymgr_zip_unreadable">Избраният ZIP файл не можа да бъде прочетен</string> + <string name="deckymgr_plugins_dir_failed">Директорията с плъгини на Decky не можа да бъде създадена</string> + <string name="deckymgr_extracting_zip">Разопаковане на ZIP на плъгина</string> + <string name="deckymgr_zip_one_folder">ZIP на плъгина трябва да съдържа една папка с плъгина на най-горно ниво</string> + <string name="deckymgr_zip_missing_files">В ZIP на плъгина липсва plugin.json, package.json или dist/index.js</string> + <string name="deckymgr_plugin_name_missing">Липсва име на плъгина</string> + <string name="deckymgr_session_started_plugin">По време на инсталацията започна сесия - спри я, преди да инсталираш плъгин</string> + <string name="deckymgr_plugin_installed">Плъгинът е инсталиран · рестартирай Steam, за да се зареди</string> + <string name="deckymgr_plugin_install_failed">Инсталирането на ZIP на плъгина се провали</string> + <string name="deckymgr_zip_too_many">ZIP на плъгина съдържа твърде много записи</string> + <string name="deckymgr_zip_invalid_path">ZIP на плъгина съдържа невалиден път</string> + <string name="deckymgr_zip_duplicate_paths">ZIP на плъгина съдържа повтарящи се пътища</string> + <string name="deckymgr_zip_unsafe_path">ZIP на плъгина съдържа опасен път</string> + <string name="deckymgr_folder_failed">Папката на плъгина не можа да бъде създадена</string> + <string name="deckymgr_zip_too_large">ZIP на плъгина надвишава ограничението от 1 GiB при разопаковане</string> + <string name="deckymgr_bundled_not_executable">Вграденият изпълним файл на плъгина не можа да бъде означен като изпълним</string> + <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> + <string name="deckymgr_remote_hash_invalid">Отдалеченият изпълним файл %1$s има невалиден SHA-256 хеш</string> + <string name="deckymgr_bin_dir_failed">Директорията bin на плъгина не можа да бъде създадена</string> + <string name="deckymgr_downloading_binary">Изтегляне на изпълним файл на плъгина · %1$s</string> + <string name="deckymgr_binary_download_failed">Изпълнимият файл на плъгина %1$s не можа да бъде изтеглен</string> + <string name="deckymgr_binary_checksum_mismatch">Несъответствие в контролната сума на изпълнимия файл на плъгина · %1$s</string> + <string name="deckymgr_binary_not_executable">Изпълнимият файл на плъгина не можа да бъде означен като изпълним · %1$s</string> + <string name="deckymgr_binary_replace_failed">Изпълнимият файл на плъгина не можа да бъде заменен · %1$s</string> + <string name="deckymgr_binary_install_failed">Изпълнимият файл на плъгина не можа да бъде инсталиран · %1$s</string> + <string name="deckymgr_folder_taken">Папката на плъгина принадлежи на друг плъгин</string> + <string name="deckymgr_preserve_failed">Съществуващият плъгин не можа да бъде запазен</string> + <string name="deckymgr_activate_failed">Импортираният плъгин не можа да бъде активиран</string> + <string name="deckymgr_settings_failed">Настройките на плъгина на Decky не можаха да бъдат обновени</string> + <string name="deckymgr_restore_failed">Съществуващият плъгин не можа да бъде възстановен</string> + + <string name="adbfix_save_failed">Идентичността на безжичното отстраняване на грешки не можа да бъде запазена</string> + <string name="adbfix_command_failed">Командата на ADB се провали (%1$d)</string> + <string name="adbfix_not_confirmed">Android не потвърди промяната на ограничението за дъщерни процеси: %1$s</string> + <string name="adbfix_no_pairing">Няма запазено чифтосване за безжично отстраняване на грешки</string> + <string name="adbfix_enter_address">Въведи текущия IP адрес и порт за безжично отстраняване на грешки</string> + + <string name="adbpair_timed_out">Изтече времето за изчакване на изскачащия прозорец за чифтосване. Опитай отново от DroidDeck.</string> + <string name="adbpair_discovery_failed_code">Не можа да се търси изскачащият прозорец за чифтосване (%1$d). Проверете дали Wi-Fi е включен.</string> + <string name="adbpair_discovery_failed">Не можа да се търси изскачащият прозорец за чифтосване: %1$s</string> + <string name="adbpair_six_digits">Въведи всичките 6 цифри на кода</string> + <string name="adbpair_pairing">Чифтосване…</string> + <string name="adbpair_code_rejected">Този код не работи. Отвори отново изскачащия прозорец за чифтосване и въведи новия му код.</string> + <string name="adbpair_connecting">Чифтосано. Свързване…</string> + <string name="adbpair_port_not_found">Чифтосано, но портът за безжично отстраняване на грешки не беше намерен. Остави безжичното отстраняване на грешки включено и опитай отново.</string> + <string name="adbpair_applying">Прилагане на настройката…</string> + <string name="adbpair_command_failed">Командата за безжично отстраняване на грешки се провали</string> + <string name="adbpair_channel_name">Настройка на безжично отстраняване на грешки</string> + <string name="adbpair_channel_description">Пита за кода за чифтосване за безжично отстраняване на грешки, докато Настройки е отворено</string> + <string name="adbpair_title_pair">Чифтосване на безжично отстраняване на грешки</string> + <string name="adbpair_waiting_text">Включи безжичното отстраняване на грешки, после докосни „Чифтосване на устройство с код за чифтосване“.</string> + <string name="adbpair_waiting_long">Включи безжичното отстраняване на грешки, после докосни „Чифтосване на устройство с код за чифтосване“. Остани в Настройки: кодът ще се появи в това известие.</string> + <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> + <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> + + <string name="selfinst_http">Изтеглянето отговори с HTTP %1$d</string> + <string name="selfinst_size_mismatch">Размерът на изтеглянето не съответстваше на публикуваното APK и беше отхвърлен - опитай отново</string> + <string name="selfinst_checksum_mismatch">Изтеглянето не съответстваше на контролната си сума и беше отхвърлено - опитай отново</string> + <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> + <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> + <string name="selfinst_no_signing">Изтегленото APK няма информация за подпис</string> + <string name="selfinst_wrong_signer">Изтегленото APK не е подписано с ключа за версии на DroidDeck</string> + <string name="appupd_http">Каталогът с актуализации отговори с HTTP %1$d</string> + + <string name="loadstate_starting">Стартиране на сесията…</string> + <string name="loadstate_elapsed">%1$d:%2$02d изминали · все още работи</string> + <string name="loadstate_steam_update">Изтегляне на актуализацията на клиента на Steam · %1$d%%</string> + <string name="loadstate_steam_client">Изтегляне на клиента на Steam · %1$s</string> + + <string name="pextra_busy">Вече тече инсталация на инструмент за съвместимост</string> + <string name="pextra_stop_session">Спри активната сесия, преди да инсталираш инструменти за съвместимост</string> + <string name="pextra_no_release">Не беше намерена ARM64 версия на %1$s</string> + <string name="pextra_no_space">За инсталирането на %1$s са необходими поне 4 GB свободно място плюс размера на изтеглянето</string> + <string name="pextra_downloading">Изтегляне на %1$s %2$s</string> + <string name="pextra_retrying">Повторен опит за изтегляне · опит %1$d от %2$d</string> + <string name="pextra_interrupted">Изтеглянето е прекъснато</string> + <string name="pextra_download_failed">Изтеглянето се провали; частичният файл е запазен, за да може да се продължи при нов опит</string> + <string name="pextra_too_big">Изтеглянето надхвърли размера на версията и беше изтрито</string> + <string name="pextra_incomplete">Изтеглянето беше непълно; частичният файл е запазен за продължаем повторен опит</string> + <string name="pextra_verifying">Проверка на изтеглянето</string> + <string name="pextra_checksum_unreadable">Контролната сума на версията не можа да бъде прочетена; опитай отново</string> + <string name="pextra_no_checksum">%1$s %2$s не публикува контролна сума; нищо не беше инсталирано</string> + <string name="pextra_checksum_mismatch">Несъответствие в контролната сума; изтеглянето беше изтрито</string> + <string name="pextra_session_started">По време на изтеглянето започна сесия - спри я, преди да инсталираш инструменти за съвместимост</string> + <string name="pextra_installing">Инсталиране на %1$s</string> + <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> + + <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> + <string name="cmgr_not_present">не е налично</string> + <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> + <string name="cmgr_restore_queued">%1$s на %2$s ще се върне към оригинала си, когато играта, която го използва, се затвори.</string> + <string name="cmgr_restore_done">%1$s на %2$s е възстановен от оригинала си %3$s.</string> + <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> + <string name="cmgr_deleted_original">Изтрит е оригиналът %1$s на %2$s.</string> + <string name="cmgr_nightlies_only">Изтеглянията идват само от версиите Nightlies</string> + <string name="cmgr_list_predates_checksums">Този списък с пакети е отпреди въвеждането на контролни суми - обнови го и опитай отново</string> + <string name="cmgr_checksum_mismatch">Несъответствие в контролната сума</string> + + <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> + + <string name="look_off_desc">Нищо не е приложено.</string> + <string name="look_game_clarity">Яснота на играта</string> + <string name="look_game_clarity_desc">Изостряне с лек контраст - за всеки ден.</string> + <string name="look_vivid">Наситен</string> + <string name="look_vivid_desc">Силно наситен цвят. Deband е включен, за да не се набраздява небето.</string> + <string name="look_cinematic">Кино</string> + <string name="look_cinematic_desc">По-дълбок контраст, по-хладен цвят, заглаждане на ръбовете.</string> + <string name="look_competitive">Състезателен</string> + <string name="look_competitive_desc">Повдигнати сенки и силно изостряне - за да забележиш хората първи.</string> + <string name="look_adaptive_sharpen">Адаптивно изостряне</string> + <string name="look_adaptive_sharpen_desc">Само изостряне, цветът остава непроменен.</string> + <string name="look_filmic">Филмов</string> + <string name="look_filmic_desc">Крива като на филм, леко притъпена.</string> + <string name="look_arcade">Аркаден</string> + <string name="look_arcade_desc">Силен и ефектен.</string> + <string name="look_retro_crt">Ретро CRT</string> + <string name="look_retro_crt_desc">Ефектът на CRT тръбата и аналоговия сигнал заедно.</string> + <string name="look_upscale_sharp">Остро мащабиране</string> + <string name="look_upscale_sharp_desc">За работа под разделителната способност на екрана.</string> + <string name="look_pixel_clean">Чисти пиксели</string> + <string name="look_pixel_clean_desc">Ясни пиксели за 2D и стари игри.</string> + <string name="look_anime_edge">Аниме контури</string> + <string name="look_anime_edge_desc">Контури като в анимация плюс изостряне.</string> + + <string name="gpuinfo_not_adreno">Не е GPU Adreno</string> + <string name="gpuinfo_supported">Поддържан</string> + <string name="gpuinfo_experimental">Експериментален: драйверите му са тестови версии</string> + <string name="gpuinfo_below_tested">Под тестваните устройства (Adreno 725 и по-нови): може да не работи</string> + <string name="gpuinfo_unsupported">Не се поддържа: DroidDeck изисква GPU Adreno (Snapdragon)</string> + + <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> + <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> + <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 - това е Turnip \"-Linux\", който принадлежи към \"Драйвер за средата на Linux\"</string> + <string name="lvd_no_library">В този zip файл няма libvulkan_freedreno*.so. Zip на Turnip за Android (AdrenoTools) или -Wayland не е драйвер за средата на Linux.</string> + <string name="lvd_not_aarch64">%1$s не е 64-битова AArch64 ELF споделена библиотека.</string> + <string name="lvd_not_glibc">%1$s не е glibc драйвер - той се свързва с libc на Android. Средата на Linux изисква zip \"-Linux\"; обикновен Turnip или \"-Wayland\" не може да се зареди от клиента на Steam.</string> + <string name="bundle_no_manifest">Не е пакет с драйвери: няма manifest.json, посочващ драйвер за Android и за Linux</string> + <string name="bundle_entry_outside">Не е пакет с драйвери: %1$s сочи извън него</string> + <string name="bundle_too_large">Не е пакет с драйвери: при разопаковане надхвърля %1$d MB</string> + + <string name="gstore_sd_card">SD карта</string> + <string name="gstore_folder">Папка</string> + + <string name="logshare_chooser">Споделяне на логовете от сесията</string> + + <string name="hdr_needs_android_10">изисква Android 10 или по-нова версия (слоеве на екрана с цветово пространство)</string> + <string name="hdr_none">този екран не отчита поддръжка на HDR</string> + <string name="hdr_not_hdr10">този екран поддържа %1$s, не HDR10</string> + + <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> + <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">Включи „Disable child process restrictions“ в Опции за програмисти и остави Опции за програмисти включени след това.</string> + <string name="phantom_fix_no_toggle">Тази версия на Android няма превключвател за това. Използвай безжично отстраняване на грешки, за да го промениш от това устройство.</string> + <string name="upd_block_no_apk">Тази версия няма изтегляне за това копие на DroidDeck.</string> + <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> + <string name="upd_fail_with_message">Актуализацията не се инсталира: %1$s</string> + <string name="upd_fail_generic">Актуализацията не се инсталира.</string> + <string name="saves_layout_zip">ZIP на %1$s</string> + <string name="saves_zip">zip</string> + <string name="saves_not_launched">%1$s все още не е стартирана</string> + <string name="saves_none_found">Не са намерени файлове със записи за %1$s</string> + <string name="saves_not_save_zip">%1$s няма папка drive_c, затова не е zip със записи от GameHub или Winlator</string> + <string name="gstore_cannot_create">не могат да се създават папки в %1$s</string> + <string name="gstore_cannot_write">не може да се пише в %1$s</string> + <string name="gstore_cannot_write_detail">не може да се пише в %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">Папката не е достъпна</string> + <string name="gfiles_name_taken">Друг файл вече използва %1$s</string> + <string name="gfiles_art_unavailable">Папката с визуални материали не е достъпна</string> + <string name="gfiles_unrelated_inventory">Папката има несвързан инвентар за синхронизация</string> + <string name="gfiles_remove_failed">%1$s не можа да бъде премахнат</string> + <string name="gfiles_outside_folder">Файлът излиза извън папката за експортиране</string> + <string name="cmgr_fex_unnamed">FEX (версия без име)</string> + <string name="cmgr_no_profile">няма profile.json в %1$s</string> + <string name="cmgr_not_component_package">%1$s не е пакет с компоненти за Linux (тип %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s свободни</string> + <string name="mode_storage_auto_short">Автоматично</string> + <string name="appupd_catalog_rejected">Каталогът с актуализации не премина проверките (%1$s)</string> + <string name="main_adb_clip_label">Настройка на DroidDeck за дъщерни процеси</string> + <string name="setup_signed_in_offline_unnamed">Вписан · офлайн режим</string> + <string name="setup_signed_in_unnamed">Вписан</string> + <string name="driver_tag_bundled">ВГРАДЕН</string> + <string name="driver_tag_downloaded">ИЗТЕГЛЕН</string> + <string name="driver_tag_imported">ИМПОРТИРАН</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Екран на сесията</string> + <string name="display_resolution">Разделителна способност</string> + <string name="display_game_fit">Напасване на игра 16:9</string> + <string name="display_game_fit_hint">Как игрите с 16:9 се напасват на този екран. При следващата сесия.</string> + <string name="display_game_fit_unavailable">Избери „Напасване на екрана“ или собствен размер, по-тесен от 16:9.</string> + <string name="display_preserve">Запазване на пропорциите</string> + <string name="display_stretch">Разтягане до екрана</string> + <string name="display_image_scaling">Мащабиране на изображението</string> + <string name="display_filter">Филтър за мащабиране</string> + <string name="display_filter_hint">Общ за Steam и Desktop.</string> + <string name="display_filter_live">Прилага се веднага. Общ за Steam и Desktop.</string> + <string name="display_filter_note">FSR, GSR и NIS се прилагат при уголемяване на изображението на сесията. „Само изостряне“ работи и без уголемяване.</string> + <string name="display_sharpness">Острота на филтъра</string> + <string name="display_frame_rate">Честота на кадрите</string> + <string name="display_look_filter">%1$s Задава мащабиране на %2$s.</string> + <string name="display_match_screen">Напасване на екрана (%1$d×%2$d)</string> + <string name="display_follow_screen">Следване на екрана (преоразмерява при сгъване или разгъване)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Покажи данните за устройството</string> + <string name="device_info_hide">Скрий данните за устройството</string> + <string name="device_info_loading">Прочитане на устройството…</string> + <string name="device_info_copy">Копирай</string> + <string name="device_info_copy_hint">Копирай тези данни в доклад за грешка.</string> + <string name="device_info_copied">Копирано.</string> + <string name="force_ssbs_title">Принудително SSBS</string> + <string name="force_ssbs_hint">Поддържа настройка за скоростта на процесора, която Wine непрекъснато изключва. Може да ускори Windows игрите на Snapdragon 8 Elite; при по-стари чипове промяната е малка. Прилага се при следващото стартиране на игра.</string> + + <string name="rail_stores">Магазини</string> + <string name="source_steam">Steam</string> + <string name="source_added">Персонализирана</string> + <string name="setup_stores">Магазини</string> + <string name="setup_stores_open_on">Отваряне на магазин на</string> + <string name="setup_stores_speed">Скорост на изтегляне</string> + <string name="setup_stores_engine">Ядро на магазините</string> + <string name="setup_stores_engine_chip_ready">● Ядрото е готово</string> + <string name="setup_stores_engine_chip_missing">Не е компилирано</string> + <string name="stores_tier_balanced">Балансирано</string> + <string name="stores_tier_fast">Бързо</string> + <string name="stores_tier_max">Максимално</string> + <string name="stores_title">Магазини</string> + <string name="stores_settings">Настройки на магазините</string> + <string name="stores_settings_eyebrow">Настройки</string> + <string name="stores_chip_signed_out">(не е вписан)</string> + <string name="stores_tab_store">Магазин</string> + <string name="stores_tab_installed">Инсталирани</string> + <string name="stores_tab_library">Библиотека</string> + <string name="stores_tab_all">Всички</string> + <string name="stores_tab_downloads">Изтегляния</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Вход</string> + <string name="stores_signout_named">%1$s · изход</string> + <string name="stores_signout_title">Изход от %1$s?</string> + <string name="stores_signout_note">Инсталираните игри остават. Библиотеката се изтегля отново, когато се впишеш пак.</string> + <string name="stores_signout_stay">Остани вписан</string> + <string name="stores_signout_go">Изход</string> + <string name="stores_search_catalog">Търсене в каталога на %1$s…</string> + <string name="stores_search_installed">Търсене в инсталираните игри…</string> + <string name="stores_search_library">Търсене в твоята библиотека…</string> + <string name="stores_search_open">Търсене</string> + <string name="stores_installed_empty">Нищо инсталирано</string> + <string name="stores_library_loading">Зареждане</string> + <string name="stores_nothing_matches">Няма игри</string> + <string name="stores_shelves_loading">Зареждане на магазина…</string> + <string name="stores_amazon_no_catalog">Няма каталог</string> + <string name="stores_shelf_new">Новости</string> + <string name="stores_shelf_deals">Промоции</string> + <string name="stores_shelf_free">Безплатни игри</string> + <string name="stores_shelf_trending">Актуални</string> + <string name="stores_shelf_library">Твоята библиотека</string> + <string name="stores_play">Играй</string> + <string name="stores_install">Инсталирай</string> + <string name="stores_resume_install">Продължи инсталацията</string> + <string name="stores_dl_interrupted">Прекъснато</string> + <string name="stores_install_size">Инсталация · %1$s</string> + <string name="stores_get_free">Вземи безплатно</string> + <string name="stores_view_on">Виж в %1$s</string> + <string name="stores_buy_price">Купи · %1$s</string> + <string name="stores_installed_chip">Инсталирана</string> + <string name="stores_in_library">В библиотеката</string> + <string name="stores_free">Безплатна</string> + <string name="stores_resume">Продължи</string> + <string name="stores_refresh">Обнови</string> + <string name="stores_show_mature">Показвай съдържание за възрастни</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Офлайн</string> + <string name="epic_card_none">Изключено</string> + <string name="epic_eos">Вход в Epic (EOS)</string> + <string name="epic_offline">Стартирай офлайн</string> + <string name="epic_resolve">Разреши входа в Epic</string> + <string name="store_cloud_saves">Облачни запазвания</string> + <string name="cloud_upload">Качи в облака</string> + <string name="cloud_download">Изтегли от облака</string> + <string name="cloud_none">Няма облачни запазвания</string> + <string name="cloud_conflict">Конфликт в облака</string> + <string name="cloud_conflict_files">Конфликт в облака · %1$d файла</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Запази облачната</string> + <string name="cloud_keep_local">Запази локалната</string> + <string name="cloud_checking">Проверка на облака…</string> + <string name="cloud_last_sync">Последна синхронизация %1$s</string> + <string name="cloud_never">Още не е синхронизирано</string> + <string name="cloud_done">%1$d файла</string> + <string name="stores_eyebrow_installed">инсталирана</string> + <string name="stores_eyebrow_library">в библиотеката ти</string> + <string name="stores_eyebrow_store">магазин</string> + <string name="achievements_title">Постижения</string> + <string name="achievements_count">%1$d от %2$d отключени</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Отключени · %1$d</string> + <string name="achievements_group_locked">Заключени · %1$d</string> + <string name="achievements_group_hidden">Скрити · %1$d</string> + <string name="achievements_hidden">Скрито постижение</string> + <string name="achievements_hidden_detail">Подробностите се показват след отключването му.</string> + <string name="achievements_all">Всички постижения са отключени</string> + <string name="achievements_reading">Зареждане…</string> + <string name="achievements_signed_out">Влезте в %1$s, за да видите постиженията си.</string> + <string name="achievements_unreachable">Неуспешна връзка с %1$s. Проверете отново, когато сте онлайн.</string> + <string name="achievements_none">%1$s не показва постижения за тази игра.</string> + <string name="achievements_show_all">Покажи всички %1$d</string> + <string name="achievements_dialog_title">Постижения · %1$s</string> + <string name="stores_in_steam">✓ В Steam</string> + <string name="stores_uninstall">Деинсталирай</string> + <string name="stores_uninstalling">Деинсталиране…</string> + <string name="stores_uninstall_title">Деинсталиране на %1$s?</string> + <string name="stores_uninstall_body_size">Освобождава %1$s и я премахва от Steam.</string> + <string name="stores_uninstall_body">Премахва файловете ѝ и пряката ѝ пътека в Steam.</string> + <string name="stores_launch_not_registered">Steam ще засече играта при следващото стартиране; стартирай я оттам или отвори Steam сега.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Избор на Steam за всяка игра</string> + <string name="stores_card_audio">Звук</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Компоненти</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Ядро</string> + <string name="stores_card_engine_value">%1$s · собствено</string> + <string name="stores_card_engine_java">%1$s · вградено</string> + <string name="stores_how_it_runs">Как се стартира</string> + <string name="stores_kv_fetched">Изтеглена от</string> + <string name="stores_kv_fetched_value">%1$s, с твоя профил</string> + <string name="stores_kv_installed_to">Инсталирана в</string> + <string name="stores_kv_installed_to_value">Хранилище за игри (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Стартирана от</string> + <string name="stores_kv_launched_by_value">Steam и Proton, по същия начин както игрите от Steam</string> + <string name="stores_kv_launched_by_epic">Steam и Proton · вход в Epic при всяко стартиране, оверлеят на EOS е изключен</string> + <string name="stores_kv_launched_by_amazon">Steam и Proton · зададени са променливите на Amazon Games Services</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">добавена в Steam при инсталацията (Deck UI + Steam Input)</string> + <string name="stores_downloads_empty">Няма изтегляния</string> + <string name="stores_dl_paused">На пауза</string> + <string name="stores_dl_queued">На опашка</string> + <string name="stores_dl_queued_at">На опашка · №%1$d</string> + <string name="stores_dl_cancelled">Отказано</string> + <string name="stores_dl_failed">Неуспешно</string> + <string name="stores_dl_failed_reason">Неуспешно · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Пауза</string> + <string name="stores_dl_resume">Продължи</string> + <string name="stores_dl_cancel">Откажи</string> + <string name="stores_dl_retry">Опитай отново</string> + <string name="stores_dl_clear">Изчисти</string> + <string name="stores_dl_clear_all">Изчисти завършените</string> + <string name="stores_stage_manifest">Манифест</string> + <string name="stores_stage_download">Изтегляне</string> + <string name="stores_stage_verify">Проверка</string> + <string name="stores_stage_install">Инсталация</string> + <string name="stores_stage_done">Готово</string> + <string name="stores_stage_checking">Проверка</string> + <string name="stores_stage_downloading">Изтегляне</string> + <string name="stores_dl_cancel_confirm">Изтриване на изтеглянето?</string> + <string name="stores_dl_failed_kept">Неуспешно · %1$s е запазен</string> + <string name="stores_stage_verifying">Проверка</string> + <string name="stores_stage_installing">Инсталиране</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d файла</string> + <string name="stores_download_manager">Диспечер на изтеглянията</string> + <string name="stores_parallel">Изтегляния едновременно</string> + <string name="stores_install_where">Инсталирай в</string> + <string name="stores_install_title">Инсталация на %1$s:</string> + <string name="stores_target_internal">Вътрешна памет</string> + <string name="stores_target_free">%1$s свободни</string> + <string name="stores_notification_channel">Изтегляния от магазина</string> + <string name="stores_notification_channel_hint">Показва напредъка на изтеглянето на игри и ги държи активни на заден фон</string> + <string name="stores_notification_one">Изтегляне</string> + <string name="stores_notification_many">%1$d изтегляния</string> + <string name="stores_notification_waiting">Подготовка…</string> + <string name="stores_notification_done_channel">Завършени изтегляния от магазина</string> + <string name="stores_notification_installed">%1$s е инсталирана</string> + <string name="stores_notification_failed">Изтеглянето на %1$s се провали</string> + <string name="stores_notification_delete">Изтрий</string> + <string name="stores_notification_keep">Запази</string> + <string name="stores_login_title">Вход в %1$s</string> + <string name="stores_login_provider_title">Вход с твоя доставчик</string> + <string name="stores_login_reload">Презареди</string> + <string name="stores_login_close">Затвори</string> + <string name="stores_login_retry">Опитай отново</string> + <string name="stores_login_finishing">Завършване на входа…</string> + <string name="stores_login_error_network">Страницата за вход не можа да се зареди: %1$s</string> + <string name="stores_login_error_idp_blocked">Този доставчик отказва вход от вградена страница. Впиши се с паролата си за магазина.</string> + <string name="stores_login_error_http">Страницата за вход отговори с грешка (HTTP %1$d).</string> + <string name="stores_login_error_ssl">Връзката със страницата за вход не е защитена.</string> + <string name="stores_login_error_verification">Входът не можа да бъде потвърден. Опитай отново.</string> + <string name="stores_login_error_generic">Входът се провали. Опитай отново.</string> + <string name="common_close">Затвори</string> + <string name="common_back">Назад</string> + <string name="common_edit">Редактирай</string> + <string name="games_add_all">Добави всички игри от тази папка</string> + <string name="games_added_none">Нищо ново в тази папка</string> + <string name="edit_name">Име</string> + <string name="edit_target">Цел</string> + <string name="edit_start_in">Старт в</string> + <string name="edit_launch_options">Настройки за стартиране</string> + <string name="edit_artwork">Визия</string> + <string name="edit_other_exe">Друг exe…</string> + <string name="edit_remove">Премахни</string> + <string name="art_cover">Обложка</string> + <string name="art_background">Фон</string> + <string name="art_logo">Лого</string> + <string name="art_icon">Икона</string> + <string name="art_src_folder">Папка на играта</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">Твой файл</string> + <string name="art_src_exe">От exe файла</string> + <string name="art_src_auto">Автоматично</string> + <string name="art_pick_image">Избери изображение…</string> + <string name="art_pick_title">Избери изображение: %1$s</string> + <string name="art_reset">Връщане към автоматично</string> + <plurals name="games_added_n"> + <item quantity="one">%1$d игра добавена</item> + <item quantity="other">%1$d игри добавени</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="one">%1$d вече в Игри</item> + <item quantity="other">%1$d вече в Игри</item> + </plurals> + <string name="hop_back">B · Назад към %1$s</string> + <string name="hop_from_games">играта</string> + <string name="hop_from_play">Настройки за игра</string> + <string name="sheet_graphics_drivers">Графични драйвери</string> + <string name="comp_this_proton">Този Proton</string> + <string name="comp_every_proton">Всеки Proton</string> + <string name="comp_for_every_proton">За всеки Proton</string> + <string name="comp_available_count">Налични · %1$d</string> + <string name="comp_checked">Проверено %1$s</string> + <string name="comp_check_all">Проверка за нови компоненти и драйвери</string> + <string name="comp_get">Вземи</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton и FEX по-долу са на този Proton</string> + <string name="comp_device_tested">%1$s · тестван</string> + <string name="comp_device_untested">%1$s · нетестван</string> + <string name="comp_device_unsupported">%1$s · неподдържан</string> + <string name="gpu_auto_keeps">Поддържа препоръчаната двойка актуална</string> + <string name="gpu_bar_auto">Автоматично · %1$s</string> + <string name="gpu_separately">Драйверите за среда и екран поотделно</string> + <string name="gpu_use_title">Използвай %1$s?</string> + <string name="proton_remove_title">Премахване на %1$s?</string> + <string name="setup_tab_diagnostics">Диагностика</string> + <string name="setup_ready">Готово за игра</string> + <string name="setup_not_ready">Още не е готово за игра</string> + <string name="setup_ready_runtime">среда %1$s</string> + <string name="setup_ready_limit_off">ограничението за процеси е изключено</string> + <string name="setup_ready_signed_in">вписан като %1$s</string> + <string name="setup_needs_you">Когато нещо изисква действие</string> + <string name="setup_group_look">Визия</string> + <string name="setup_group_device">Това устройство</string> + <string name="setup_group_art">Визия на библиотеката</string> + <string name="flathub_eyebrow">ПРИЛОЖЕНИЯ ЗА LINUX</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Приложения за десктоп на Linux, отваряни от Desktop.</string> + <string name="flathub_setup">Настрой Flathub</string> + <string name="flathub_remove_title">Премахване на %1$s?</string> + <string name="flathub_remove_note">Настройките и файловете му се запазват.</string> + <string name="flathub_keep">Запази</string> + <string name="desktop_size">Размер на диска</string> + <string name="desktop_uninstall_title">Деинсталиране на десктопа?</string> + <string name="desktop_all_systems">Всички</string> + <string name="gog_achievements">Постижения в GOG</string> + <string name="gog_achievements_hint">Синхронизира постиженията за поддържаните игри от GOG. Влиза в сила при следващото стартиране.</string> + <string name="gog_achievements_off">Постиженията са изключени</string> + <string name="gog_achievements_on">Постиженията са включени</string> + <string name="gog_achievements_signin">Влезте в GOG отново</string> + <string name="gog_achievements_uploaded">Постиженията са качени</string> + <string name="gog_achievements_pending">Качването на постижение е в изчакване</string> + <string name="gog_achievements_enable_store">Включи постиженията на GOG в настройките на магазините</string> + <string name="gog_achievements_unavailable">Постиженията не са достъпни; проверете логовете на сесията</string> + <string name="achievement_unlocked">Постижение отключено</string> + <string name="touch_paste">Поставяне</string> + <string name="touch_dpad">Кръстат бутон</string> + <string name="touch_left_stick">Ляв стик</string> + <string name="touch_right_stick">Десен стик</string> + <string name="touch_menu">Меню</string> + <string name="touch_keyboard">Клавиатура</string> + <string name="touch_dpad_up">Кръстат бутон нагоре</string> + <string name="touch_dpad_down">Кръстат бутон надолу</string> + <string name="touch_dpad_left">Кръстат бутон наляво</string> + <string name="touch_dpad_right">Кръстат бутон надясно</string> + <string name="touch_title">Сензорно управление</string> + <string name="touch_controller">Сензорен контролер</string> + <string name="touch_mouse">Мишка</string> + <string name="touch_on">Вкл.</string> + <string name="touch_off">Изкл.</string> + <string name="touch_speed_slow">Бавна</string> + <string name="touch_speed_normal">Нормална</string> + <string name="touch_speed_fast">Бърза</string> + <string name="touch_edit_layout">Редактиране на разположението</string> + <string name="touch_droiddeck_menu">Меню на DroidDeck</string> + <string name="touch_drag_hint">Плъзнете елементите за управление върху екрана</string> + <string name="touch_arrange">Разположи</string> + <string name="touch_discard">Отхвърли</string> + <string name="touch_saving">Запазване…</string> + <string name="touch_icon">Икона</string> + <string name="touch_narrower">Ш−</string> + <string name="touch_wider">Ш+</string> + <string name="touch_shorter">В−</string> + <string name="touch_taller">В+</string> + <string name="touch_hide">Скрий</string> + <string name="touch_color_opacity">Цвят и прозрачност…</string> + <string name="touch_copy_layout">Копиране на това разположение</string> + <string name="touch_layout_copied">Разположението е копирано; поставете го в сензорното разположение на всяка игра</string> + <string name="touch_paste_layout">Поставяне на копираното разположение</string> + <string name="touch_reset_layout">Връщане към стандартното на Steam</string> + <string name="touch_no_binding">Този елемент няма зададено действие в Steam, за да му се даде икона</string> + <string name="touch_icon_for">Икона за %1$s</string> + <string name="touch_layout_color">Цвят на сензорното разположение</string> + <string name="touch_done">Готово</string> + <string name="touch_save_failed">Сензорното разположение не можа да се запази</string> + <string name="touch_saved">Сензорното разположение е запазено в Steam</string> + <string name="touch_app_id">Приложение %1$d</string> + <string name="touch_label_hint">Етикет (незадължително)</string> + <string name="touch_icons_missing">Иконите на Steam не бяха намерени</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d икона от Steam</item> + <item quantity="other">%1$d икони от Steam</item> + </plurals> + <string name="touch_icon_color">Цвят на иконата</string> + <string name="touch_button_color">Цвят на бутона</string> + <string name="touch_no_icon">Без икона</string> + + <string name="stores_sync_sign_in">Проверка на вписването…</string> + <string name="stores_sync_library">Извличане на библиотеката ви…</string> + <string name="stores_sync_game_list">Извличане на списъка с игри…</string> + <string name="stores_sync_details">Зареждане на подробности за игрите… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">Синхронизиране на %1$d игра…</item> + <item quantity="other">Синхронизиране на %1$d игри…</item> + </plurals> + <string name="stores_sync_progress">Синхронизиране… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Извличане на обложки…</string> + <string name="stores_sync_failed">Синхронизирането е неуспешно</string> + <string name="stores_sync_no_games">В библиотеката ви в %1$s не бяха намерени игри</string> + <string name="stores_sync_amazon_no_answer">Amazon Games не отговори за библиотеката ви: обновете или влезте отново</string> + <string name="stores_sync_gog_unreachable">Неуспешно свързване с GOG</string> + <string name="stores_sync_gog_unreadable">Библиотеката ви в GOG не можа да се прочете</string> + <string name="stores_registering">Регистриране в Steam…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Режим Cardboard</string> + <string name="cardboard_mode_hint">Изображения за лявото и дясното око за VR очила за телефон.</string> + <string name="cardboard_source">Картина</string> + <string name="cardboard_source_screen">Виртуален екран</string> + <string name="cardboard_source_sbs">3D един до друг</string> + <string name="cardboard_viewer">Очила</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Без леща</string> + <string name="cardboard_ipd">Разстояние между очите</string> + <string name="cardboard_screen_width">Ширина на екрана</string> + <string name="cardboard_screen_distance">Разстояние до екрана</string> + <string name="cardboard_head_tracking">Проследяване на главата</string> + <string name="cardboard_no_sensor">Това устройство няма сензор за въртене.</string> + <string name="cardboard_recenter">Центриране на изгледа</string> + <string name="cardboard_recenter_button">Центриране</string> + <string name="cardboard_steamvr">SteamVR (експериментално)</string> + <string name="cardboard_steamvr_install">Инсталиране</string> + <string name="cardboard_steamvr_install_hint">Изтегля SteamVR за ARM64 на Valve (571 MB).</string> + <string name="cardboard_steamvr_installing">Изтегляне и разархивиране на SteamVR…</string> + <string name="cardboard_steamvr_start">Стартиране</string> + <string name="cardboard_steamvr_ready_hint">Стартира SteamVR с главата ви като шлем; картината превключва на 3D един до друг.</string> + <string name="cardboard_steamvr_stop">Спиране</string> + <string name="cardboard_steamvr_running_hint">SteamVR работи.</string> + <string name="cardboard_steamvr_unavailable">Не е налично в тази сесия.</string> + <string name="cardboard_steamvr_failed">SteamVR не отговори; проверете лога на сесията.</string> + <string name="drawer_component_downloads">Изтегляне на компоненти</string> + <string name="drawer_component_downloads_hint">Изтеглянията са общи със страницата Компоненти. Изберете по-горе изтеглена версия, за да се използва при следващото стартиране на играта.</string> + <string name="drawer_component_choose_download">Изберете версия за изтегляне</string> + <string name="stores_fact_developer">Разработчик</string> + <string name="stores_fact_publisher">Издател</string> + <string name="stores_fact_released">Издадена</string> + <string name="stores_hltb_main">Основна</string> + <string name="stores_hltb_extras">+ Допълнително</string> + <string name="stores_hltb_completionist">Пълно</string> + <string name="stores_hltb_hours">%1$s ч</string> + <string name="stores_hltb_minutes">%1$d мин</string> +</resources> diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml new file mode 100644 index 000000000..545774482 --- /dev/null +++ b/app/src/main/res/values-cs/strings.xml @@ -0,0 +1,2285 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>První spuštění stáhne Steam. Může to trvat několik minut.</item> + <item>Ve výchozím nastavení se po opuštění aplikace Steam pozastaví; po návratu pokračuje, kde skončil.</item> + <item>Váš ovladač se zobrazí jako ovladač Xbox 360. Jinak použijte ovládání na obrazovce.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>První spuštění připraví Plochu. Může to trvat několik minut.</item> + <item>Ve výchozím nastavení se po opuštění aplikace relace pozastaví; po návratu pokračuje, kde skončila.</item> + <item>Dotyk funguje jako touchpad: tažením posouváte kurzor, dotykem klikáte.</item> + <item>Váš ovladač se zobrazí jako ovladač Xbox 360. Jinak použijte ovládání na obrazovce.</item> + </string-array> + <string name="session_channel">Relace</string> + <string name="session_channel_description">Zobrazuje se, když je otevřená relace Steamu nebo Plochy</string> + <string name="session_notification">Steam běží</string> + <string name="session_notification_downloads">Probíhá stahování Steamu</string> + <string name="session_notification_paused">Relace pozastavena – klepnutím na Obnovit pokračujte</string> + <string name="resume_session">Obnovit</string> + <string name="suspend_now">Pozastavit teď</string> + <string name="stop_session">Ukončit relaci</string> + <string name="frame_gen_title">Generování snímků</string> + <string name="frame_gen_hint">Vyberte režim generování snímků</string> + <string name="frame_gen_off">Vypnuto</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG Adaptive %1$d</string> + <string name="lsfg_needs_steam">Vyžaduje Lossless Scaling na Steamu</string> + <string name="lsfg_needs_install">Nainstalujte Lossless Scaling ve Steamu nebo naimportujte Lossless.dll</string> + <string name="lsfg_import">Importovat Lossless.dll…</string> + <string name="lsfg_using_steam">Používá se kopie ze Steamu</string> + <string name="lsfg_using_import">Používá se importovaná kopie</string> + <string name="lsfg_pick_title">Vyberte Lossless.dll</string> + <string name="lsfg_imported">Lossless.dll byl importován</string> + <string name="lsfg_import_unchanged">Tento Lossless.dll se už používá</string> + <string name="lsfg_import_not_owned">Tento účet Steamu nevlastní Lossless Scaling</string> + <string name="lsfg_import_invalid">Tento soubor není Lossless.dll aplikace Lossless Scaling</string> + <string name="lsfg_import_failed">Z tohoto souboru Lossless.dll se nepodařilo připravit LSFG</string> + <string name="lsfg_problem_missing">LSFG vyžaduje Lossless Scaling ze Steamu nebo importovaný Lossless.dll</string> + <string name="lsfg_problem_failed">LSFG se nepodařilo připravit shadery z Lossless.dll</string> + <string name="hud_fg_unsupported">%1$s: nepodporováno</string> + <string name="hud_fg_failed">%1$s: chyba enginu</string> + <string name="hud_fg_needs_lossless">%1$s: vyžaduje Lossless Scaling</string> + <string name="hud_fg_starting">%1$s %2$s · spouští se</string> + <string name="second_library_import_hint">Přidá knihovnu Steamu a prohledá ji na hry pro Windows. Vlastněné hry použijí své položky ze Steamu; ostatní se stanou zástupci mimo Steam.</string> + <string name="game_env_title">Prostředí hry</string> + <string name="game_env_hint">Proměnné prostředí pro hry na Protonu.</string> + <string name="game_env_edit">Upravit</string> + <string name="game_env_shared">Všechny hry na Protonu</string> + <string name="game_env_scope">Profil</string> + <string name="game_env_app_id">ID aplikace Steam</string> + <string name="game_env_open">Otevřít</string> + <string name="game_env_profile">Hra %1$s</string> + <string name="game_env_add">Přidat proměnnou</string> + <string name="game_env_name">Název proměnné</string> + <string name="game_env_value">Hodnota</string> + <string name="game_env_save">Uložit</string> + <string name="game_env_remove">Odebrat</string> + <string name="game_env_restore">Znovu zdědit</string> + <string name="game_env_reset">Vymazat přepsání prostředí</string> + <string name="game_env_unset">Zrušit pro tento profil</string> + <string name="game_env_empty">Nejsou nastaveny žádné proměnné.</string> + <string name="game_env_inherited">Zděděno</string> + <string name="game_env_custom">Vlastní proměnná</string> + <string name="game_env_error">Nepodařilo se načíst ani uložit. Zkontrolujte volné úložiště a zkuste to znovu.</string> + <string name="game_env_invalid">Použijte platný název a hodnotu o délce až 8192 znaků.</string> + <string name="game_env_unused">Standardní běhové prostředí Protonu tuto proměnnou nečte.</string> + <string name="game_env_loading">Načítání…</string> + <string name="game_env_feature">Přepsání úrovně funkcí D3D12. 12_2 oznamuje schopnosti DirectX 12 Ultimate; nepodporované funkce mohou selhat. Nechte nenastavené pro automatickou detekci.</string> + <string name="game_env_shader">Přepsání shader modelu D3D12. 6_9 vyžaduje dostatečně novou verzi VKD3D a odpovídající funkce GPU. Nechte nenastavené pro automatickou detekci.</string> + <string name="game_env_cache">Ponechte zapnuté ukládání shaderů Mesa do mezipaměti, aby se omezilo opakované kompilování. false ukládání do mezipaměti zapíná.</string> + <string name="game_env_cache_size">Maximální velikost mezipaměti shaderů Mesa, například 1G. Větší mezipaměť zabírá více úložiště.</string> + <string name="game_env_vkd3d_config">Možnosti VKD3D pro D3D12. nodxr vypíná ray tracing; použijte jen pro hru, která to vyžaduje.</string> + <string name="game_env_proton_log">Nastavte na 1 pro diagnostický log Protonu. Protokolování může snížit výkon a zabírat úložiště.</string> + <string name="game_env_wined3d">Nastavte na 1 pro použití WineD3D založeného na OpenGL místo DXVK, pro testování kompatibility D3D9–11.</string> + <string name="game_env_no_ntsync">Nastavte na 1, aby tato hra nepoužívala ntsync (droiddeck-ntsync nebo ovladač jádra); použije droiddeck-esync, pokud existuje balíček, jinak droiddeck-fsync.</string> + <string name="game_env_no_esync">Nastavte na 1 pro vypnutí droiddeck-esync u této hry; místo něj použije droiddeck-fsync, nebo droiddeck-ntsync, pokud je vypnutý i fsync.</string> + <string name="game_env_no_fsync">Nastavte na 1, aby tato hra nepoužívala droiddeck-fsync; použije droiddeck-esync, pokud existuje balíček, jinak droiddeck-ntsync.</string> + <string name="game_env_fsync_first">Nastavte na 1 pro spuštění této hry na droiddeck-fsync, nebo na 0, aby měl droiddeck-esync přednost, pokud balíček odpovídá jejímu Protonu.</string> + <string name="game_env_xalia">Nastavte na 0 pro vypnutí pomocníka Protonu pro navigaci gamepadem u této hry.</string> + <string name="game_env_hud">Překryvná vrstva DXVK pro D3D9–11. fps zobrazuje snímkovou frekvenci; bez nastavení se skryje.</string> + <string name="game_env_dxvk_config">Nastavení DXVK oddělená středníky. Novější verze používají pro omezení snímků dxvk.maxFrameRate; pro běžné hraní dejte přednost omezovači snímků relace.</string> + <string name="game_env_vkd3d_limit">Omezení snímkové frekvence D3D12. 0 omezení vypíná.</string> + <string name="game_env_glthread">Vícevláknové OpenGL Mesa. true může pomoci hrám OpenGL omezeným CPU; vyzkoušejte u každé hry zvlášť.</string> + <string name="sync_backend_title">Synchronizace Wine</string> + <string name="sync_backend_hint">Jak hry synchronizují svá vlákna. Použije se od příští relace.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">Předvolba FEX</string> + <string name="fex_next_launch">Použije se při dalším spuštění hry.</string> + <string name="game_settings_title">Hry</string> + <string name="fex_default_label">Výchozí FEX</string> + <string name="fex_default_detail">Používá výchozí nastavení FEX.</string> + <string name="fex_stability_label">Stabilita</string> + <string name="fex_stability_detail">Nejbezpečnější, nejpomalejší nastavení.</string> + <string name="fex_compatibility_label">Kompatibilita</string> + <string name="fex_compatibility_detail">Plné TSO s multiblokem.</string> + <string name="fex_intermediate_label">Střední</string> + <string name="fex_intermediate_detail">Vyvážené TSO se sníženou přesností x87.</string> + <string name="fex_performance_label">Výkon</string> + <string name="fex_performance_detail">Veškeré TSO vypnuto; multiblok.</string> + <string name="fex_performance_tso_label">Výkon + TSO</string> + <string name="fex_performance_tso_detail">Nastavení výkonu se zapnutým hlavním TSO.</string> + <string name="fex_extreme_label">Extrémní</string> + <string name="fex_extreme_detail">Vypíná kontroly samo-modifikujícího se kódu. Hry s JIT a DRM mohou přestat fungovat.</string> + <string name="fex_extreme_tso_label">Extrémní + TSO</string> + <string name="fex_extreme_tso_detail">Extrémní nastavení se zapnutým hlavním TSO.</string> + <string name="fex_extreme_gn_label">Extrémní-gn</string> + <string name="fex_extreme_gn_detail">Výkon s nápovědami TSC a metadat. Zkuste před Extrémním.</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">Výkon s plnými kontrolami kódu a skrytým bitem hypervizoru.</string> + <string name="game_env_custom_value">Vlastní hodnota</string> + <string name="game_env_select_value">Vybrat hodnotu</string> + <string name="user_apps_section">Emulátory a aplikace</string> + <string name="user_apps_add">Přidat</string> + <string name="user_apps_add_detail">Vaše vlastní aplikace nebo hra</string> + <string name="user_apps_kind_script">Skript</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d %%</string> + <string name="user_apps_starting">Spouští se…</string> + <string name="user_apps_added">%1$s přidáno</string> + <string name="user_apps_removed">%1$s odebráno</string> + <string name="user_apps_back">Plocha</string> + <string name="user_apps_gone">Tato aplikace zmizela. Vyberte ji znovu z Plochy.</string> + <string name="user_apps_open">Otevřít %1$s</string> + <string name="user_apps_remove">Odebrat</string> + <string name="user_apps_remove_confirm">Pro odebrání stiskněte znovu</string> + <string name="user_apps_stop_session">Pro odebrání ukončete relaci</string> + <string name="user_apps_script_linked">Spouští %1$s ze své složky, propojené do relace tam, kde se nachází. Odebráním aplikace zůstane tato složka nedotčena.</string> + <string name="user_apps_script_copied">Spouští %1$s z kopie své složky uvnitř DroidDeck. Odebráním aplikace se smaže kopie, ne originál.</string> + <string name="user_apps_runtime_required">Nejprve nainstalujte běhové prostředí Linuxu</string> + <string name="user_apps_file_gone">Soubor zmizel</string> + <string name="user_apps_no_space">Nedostatek volného místa: zkopírování složky vyžaduje %1$s</string> + <string name="user_apps_copying">Kopíruje se %1$s</string> + <string name="user_apps_failed">Přidání aplikace se nezdařilo</string> + <string name="user_apps_github_checking">Hledá se vydání %1$s</string> + <string name="user_apps_github_unreachable">Nepodařilo se načíst vydání %1$s</string> + <string name="user_apps_github_no_appimage">Žádné vydání %1$s neobsahuje AppImage pro ARM64 ani x86_64</string> + <string name="user_apps_downloading">Stahuje se %1$s</string> + <string name="user_apps_download_failed">Stahování se nezdařilo</string> + <string name="user_apps_update_failed">Aktualizace aplikace se nezdařila</string> + <string name="user_apps_icon_failed">Tuto ikonu se nepodařilo načíst</string> + <string name="user_apps_saved">%1$s uloženo</string> + <string name="user_apps_edit">Upravit</string> + <string name="user_apps_check_updates">Zkontrolovat aktualizace</string> + <string name="user_apps_checking_updates">Kontrola aktualizací…</string> + <string name="user_apps_update_to">Aktualizovat na %1$s</string> + <string name="user_apps_up_to_date">Aktuální · %1$s</string> + <string name="user_apps_updated">%1$s aktualizováno</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">Přidat aplikaci</string> + <string name="add_app_tab_script">Skript</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">Soubor .sh. Spouští se ze své vlastní složky, na celou obrazovku pod gamescope.</string> + <string name="add_app_appimage_hint">AppImage pro ARM64 (aarch64), nebo pro x86_64, který běží přes FEX. Jednou se rozbalí do běhového prostředí Linuxu.</string> + <string name="add_app_github_hint">Nainstaluje AppImage pro ARM64 z nejnovějšího vydání repozitáře, nebo jeho verzi pro x86_64 přes FEX.</string> + <string name="app_fex_title">Použít FEX</string> + <string name="app_fex_auto">Automaticky</string> + <string name="app_fex_on">Zapnuto</string> + <string name="app_fex_off">Vypnuto</string> + <string name="app_fex_hint">FEX spouští programy pro x86 Linux na tomto zařízení ARM64. Automaticky ho použije, když je program sestaven pro x86.</string> + <string name="app_fex_x86">%1$s přes FEX</string> + <string name="app_fex_not_ready">Tento program je sestaven pro PC %1$s a běží přes FEX, který ještě není nastaven. Jednou spusťte přihlášený Steam: DroidDeck tím stáhne FEX a Steam Linux Runtime, který potřebuje, a pak se tento program otevře.</string> + <string name="session_end_program">Tento program</string> + <string name="session_end_fex_missing">%1$s je sestaven pro PC x86 a běží přes FEX, který ještě není nainstalován. Jednou spusťte přihlášený Steam a nechte ho stáhnout FEX a Steam Linux Runtime, pak to spusťte znovu.</string> + <string name="add_app_flatpak_hint">ID aplikace Flathub nebo její stránka na Flathub. Flatpak se nejprve nastaví, pokud ještě není.</string> + <string name="add_app_file">Soubor</string> + <string name="add_app_no_file">Nevybrán žádný soubor</string> + <string name="add_app_choose_file">Vybrat soubor</string> + <string name="add_app_change_file">Změnit</string> + <string name="add_app_pick_script">Vybrat skript</string> + <string name="add_app_pick_appimage">Vybrat AppImage</string> + <string name="add_app_pick_icon">Vybrat ikonu</string> + <string name="add_app_repository">Repozitář</string> + <string name="add_app_repository_placeholder">owner/repo nebo odkaz na github.com</string> + <string name="add_app_repository_invalid">Není to repozitář GitHubu</string> + <string name="add_app_flatpak_id">ID aplikace</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">Není to ID aplikace Flathub</string> + <string name="add_app_flatpak_bundle_hint">Nebo soubor .flatpak pro aplikaci, která není na Flathub.</string> + <string name="add_app_pick_flatpak">Vybrat soubor .flatpak</string> + <string name="add_app_source">Zdroj</string> + <string name="add_app_appearance">Na Ploše</string> + <string name="add_app_preview_name">Nová aplikace</string> + <string name="add_app_name">Název</string> + <string name="add_app_name_placeholder">Z aplikace</string> + <string name="add_app_icon">Ikona</string> + <string name="add_app_icon_default">Výchozí ikona</string> + <string name="add_app_choose_icon">Vybrat ikonu</string> + <string name="add_app_clear_icon">Vymazat</string> + <string name="add_app_icon_suggestions">Z repozitáře</string> + <string name="add_app_icon_searching">Vyhledávání ikon…</string> + <string name="add_app_icon_reset">Výchozí</string> + <string name="edit_app_title">Upravit %1$s</string> + <string name="edit_app_save">Uložit</string> + <string name="add_app_checking_folder">Kontrola složky…</string> + <string name="add_app_folder_linked">Její složka je propojena do relace: %1$s</string> + <string name="add_app_folder_copied">Její složka obsahuje programy pro Linux, které sdílené úložiště nemůže spustit, proto se zkopíruje do DroidDeck (%1$s).</string> + <string name="add_app_confirm">Přidat</string> + <string name="add_app_cancel">Zrušit</string> + <string name="add_app_runtime_required">Nejprve nainstalujte běhové prostředí Linuxu v Nastavení.</string> + <string name="add_app_busy">Počkejte, až %1$s skončí.</string> + <string name="steam_compat_label">Kompatibilní</string> + <string name="proton_compatible_note">Sestavení označená „%1$s“ v nabídce Kompatibilita Steamu mají opravy prefixu od DroidDeck a hry z nich zde spustíte. Výchozí je Proton Experimental ARM64 (%1$s), nebo Proton 11.0 ARM64 (%1$s), jakmile se nainstaluje tato novější verze. Vlastní položky Protonu od Valve bez tohoto označení nemohou na tomto zařízení spustit hru.</string> + <string name="proton_installed_compatible">Nainstalováno %1$s · %2$s</string> + <string name="common_cancel">Zrušit</string> + <string name="common_auto">Automaticky</string> + <string name="common_never">Nikdy</string> + <string name="common_always">Vždy</string> + <string name="common_uninstall">Odinstalovat</string> + <string name="common_check">Zkontrolovat</string> + <string name="common_update">Aktualizovat</string> + <string name="common_checking">Kontrola…</string> + <string name="common_working">Zpracovává se…</string> + <string name="common_forget">Zapomenout</string> + <string name="common_add_ellipsis">Přidat…</string> + <string name="common_use">Použít</string> + <string name="common_applies_next_session">Použije se od příští relace.</string> + <string name="mode_steam_title">Relace Steamu</string> + <string name="mode_desktop_title">Relace Plochy</string> + <string name="mode_display">Displej</string> + <string name="mode_resolution">Rozlišení</string> + <string name="mode_res_720">Až 720p</string> + <string name="mode_res_900">Až 900p</string> + <string name="mode_res_1080">Až 1080p</string> + <string name="mode_res_native">Nativní</string> + <string name="mode_res_default">%1$s (výchozí)</string> + <string name="mode_res_custom_value">Vlastní · %1$d×%2$d</string> + <string name="mode_res_custom">Vlastní…</string> + <string name="mode_res_note">720p může zrychlit odezvu nabídek.</string> + <string name="mode_ratio">Poměr stran obrazovky</string> + <string name="mode_ratio_custom">Určen vlastním rozlišením.</string> + <string name="mode_ratio_auto">Automaticky použije alespoň 16:9.</string> + <string name="mode_fps">Omezení snímků</string> + <string name="mode_fps_note">Omezuje celou relaci. Displej přejde na obnovovací frekvenci, kterou lze tímto limitem dělit beze zbytku.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">Výstup HDR10</string> + <string name="mode_hdr_unavailable">Není k dispozici: %1$s.</string> + <string name="mode_hdr_restart">Pro použití restartujte aplikaci.</string> + <string name="mode_drivers">Ovladače</string> + <string name="mode_gpu_drivers">Ovladače GPU</string> + <string name="mode_gpu_drivers_hint">Společné pro Steam a Plochu. Nastavuje se na stránce Komponenty.</string> + <string name="mode_touch_controls">Dotyk a ovládání</string> + <string name="mode_touch">Dotyk</string> + <string name="mode_touch_touchpad">Touchpad</string> + <string name="mode_touch_direct">Přímé</string> + <string name="mode_touch_note">Automaticky: touchpad na Ploše, přímo ve Steamu.</string> + <string name="mode_osc">Ovládání na obrazovce</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">Automaticky zobrazí ovládání, pokud není připojen žádný ovladač.</string> + <string name="osc_steam_touch">Dotykové ovládání Steamu (beta)</string> + <string name="osc_style_next_session">Dotykové ovládání Steamu se spustí od příští relace Steamu</string> + <string name="osc_steam_layout">Rozvržení dotykového ovládání Steamu</string> + <string name="osc_steam_layout_hint">Přesuňte, změňte velikost nebo skryjte dotykové ovládání této hry; uloží se do Steamu</string> + <string name="mode_controller">Ovladač</string> + <string name="mode_controller_hint">Jak Steam vidí váš ovladač. Použije se od příští relace.</string> + <string name="mode_controller_deck">Ovladač Steam Deck</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Ovladač Xbox 360</string> + <string name="mode_controller_note">Deck: tlačítko Quick Access a gyroskop. Steam Controller: totéž, navíc snímání úchopu na zadních tlačítkách. DualShock 4 a DualSense: gyroskop, trackpad slouží jako touchpad. Xbox 360: Quick Access otevřete kombinací Guide+A.</string> + <string name="mode_back">Zpět</string> + <string name="back_menu_then_qam">Menu → QAM</string> + <string name="back_qam_then_menu">QAM → Menu</string> + <string name="mode_session">Relace</string> + <string name="mode_suspend">Chování na pozadí</string> + <string name="mode_suspend_hint">Když je aplikace skrytá nebo se vypne obrazovka.</string> + <string name="mode_suspend_manual">Manuální</string> + <string name="mode_suspend_native">Nativní</string> + <string name="mode_suspend_steam_note">Automaticky pozastaví relaci a obnoví ji při zobrazení. Nativně nejprve připraví Steam na spánek. Manuálně čeká na Obnovit. Nikdy běh nezastavuje.</string> + <string name="mode_background_downloads">Nechat stahování běžet</string> + <string name="mode_background_downloads_hint">Oddálí pozastavení na pozadí, pokud Steam aktivně stahuje.</string> + <string name="mode_suspend_note">Automaticky pozastaví na pozadí a obnoví při zobrazení. Manuálně čeká na Obnovit. Nikdy běh nezastavuje.</string> + <string name="mode_startup">Spouštění</string> + <string name="mode_steam_startup">Spustit Steam s DroidDeck</string> + <string name="mode_steam_startup_hint">Otevře relaci Steamu při spuštění DroidDeck.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">Hledání vydání…</string> + <string name="mode_decky_ready">Připraveno k instalaci %1$s.</string> + <string name="mode_decky_none">Nenalezeno žádné kompatibilní sestavení. Zkuste to později.</string> + <string name="mode_decky_installed">Nainstalováno · %1$s</string> + <string name="mode_decky_update">Dostupná aktualizace · %1$s</string> + <string name="mode_decky_current">Aktuální · %1$s</string> + <string name="mode_decky_loader">Loader</string> + <string name="mode_decky_install_latest">Nainstalovat nejnovější</string> + <string name="mode_decky_on">Spouští se se Steamem. Když je zapnuto, jiné aplikace na tomto zařízení mohou mít možnost ovládat Steam.</string> + <string name="mode_decky_off">Vypnuto. Port pro vzdálené ladění Steamu zůstává zavřený.</string> + <string name="mode_decky_remove_title">Odinstalovat Decky Loader?</string> + <string name="mode_decky_remove_text">Odstraní loader. Vaše pluginy a nastavení zůstanou zachovány.</string> + <string name="mode_client">Klient</string> + <string name="mode_mangoapp">Výkonový overlay</string> + <string name="mode_mangoapp_hint">Vypněte, pokud hry v režimu Deck padají nebo zčernají. Použije se od příští relace.</string> + <string name="mode_steam_repair">Opravit klienta Steam</string> + <string name="mode_steam_repair_hint">Při dalším spuštění znovu stáhne klienta Steam. Použijte, pokud Steam zůstává na načítací obrazovce. Hry, přihlášení a nastavení zůstanou zachovány.</string> + <string name="mode_steam_repair_queued">Klient Steam se znovu stáhne při dalším spuštění Steamu.</string> + <string name="mode_steam_repair_button">Opravit</string> + <string name="mode_added_art">Grafika</string> + <string name="mode_sgdb_key">Klíč API SteamGridDB</string> + <string name="mode_sgdb_key_yours">Váš klíč</string> + <string name="mode_sgdb_edit">Upravit…</string> + <string name="mode_added_art_hint">Doplní chybějící grafiku ze Steamu, poté ze SteamGridDB. Vlastní soubory cover.jpg, hero.jpg, logo.png nebo icon.png ve složce hry mají přednost.</string> + <string name="summary_remove_game">Odebrat %1$s</string> + <string name="mode_added_remove_confirm">Odebrat?</string> + <string name="games_add">Přidat hru</string> + <string name="games_add_title">Přidat hru:</string> + <string name="games_pick_exe_title">Vyberte exe:</string> + <string name="games_add_unreachable">Toto umístění není v relaci dostupné</string> + <string name="mode_added_exe_check">Zkontrolovat tento exe</string> + <string name="wincomp_title">Komponenty Windows</string> + <string name="wincomp_pad_hint">LB / RB přeskakují mezi sekcemi.</string> + <string name="wincomp_card_none">Žádné</string> + <string name="wincomp_applies">DLL knihovny Windows, které Proton neobsahuje, ze seznamu komponent. Zapněte ty, které tato hra potřebuje: uloží se do vlastní složky Windows hry, nikdy do jejích souborů, a zůstanou bez ohledu na to, s jakým Protonem hra běží.</string> + <string name="wincomp_offline">Nepodařilo se načíst seznam komponent. Již stažené komponenty lze stále přepínat.</string> + <string name="wincomp_recommended">Doporučeno pro tuto hru</string> + <string name="wincomp_checking">Zjišťuje se, co tato hra potřebuje…</string> + <string name="wincomp_when_online">Dostupné online</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">Zapíná se %1$d komponenta…</item> + <item quantity="few">Zapínají se %1$d komponenty…</item> + <item quantity="many">Zapínají se %1$d komponenty…</item> + <item quantity="other">Zapíná se %1$d komponent…</item> + </plurals> + <string name="wincomp_auto_section">Zapnuto pro tuto hru</string> + <string name="wincomp_auto_steam">Steam instaluje %1$s s %2$s (aplikace %3$s)</string> + <string name="wincomp_auto_gog">Ze seznamu GOG pro tuto hru</string> + <string name="wincomp_auto_epic">Z požadavků Epic pro tuto hru</string> + <string name="wincomp_auto_amazon">Z instalačních kroků Amazon pro tuto hru</string> + <string name="wincomp_steam_by_name">Steam instaluje %1$s s aplikací %2$s, spárováno podle názvu</string> + <string name="wincomp_suggestions">Také uvedeno</string> + <string name="wincomp_suggestion_reason">Uvádí ho %1$s; zde to není komponenta</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">Hra obsahuje %1$s</string> + <string name="wincomp_found_shipped">Hra má %1$s (často funguje i bez úprav)</string> + <string name="wincomp_already_here">Již v této hře</string> + <string name="wincomp_no_recommendation">Nic ve složce hry neodkazuje na komponentu.</string> + <string name="wincomp_active">Nainstalováno pro tuto hru (%1$d)</string> + <string name="wincomp_all">Všechny komponenty (%1$d)</string> + <string name="wincomp_installers">Potřebují instalátor Windows (%1$d)</string> + <string name="wincomp_installers_note">Jde o vlastní instalační programy, uvnitř nemají balíček k rozebrání a zatím není zaznamenáno, co instalují. Většina z nich jsou staré runtimy, které už nahrazuje Mono uvnitř Protonu; záznamy se doplňují postupně.</string> + <string name="wincomp_show">Zobrazit</string> + <string name="wincomp_hide">Skrýt</string> + <string name="wincomp_needs_installer">Spouští vlastní instalační program (zatím bez záznamu)</string> + <string name="wincomp_unsupported">Zde ještě nepodporováno</string> + <string name="wincomp_loading">Načítání komponent…</string> + <string name="wincomp_failed">Nepodařilo se nainstalovat: %1$s</string> + <string name="wincomp_downloaded">Staženo</string> + <string name="wincomp_downloading">Stahuje se %1$s</string> + <string name="wincomp_installing">Instaluje se: %1$s</string> + <string name="wincomp_reverting">Vypínání…</string> + <string name="wincomp_download_size">%1$s ke stažení</string> + <string name="wincomp_next_launch">Použije se při dalším spuštění hry.</string> + <string name="mode_stretch169">Roztáhnout hry 16:9</string> + <string name="mode_stretch169_hint">Na obrazovkách bližších čtverci než 16:9 roztáhne hry na celou plochu místo pruhů. Použije se od příští relace.</string> + <string name="mode_audio">Zvuk</string> + <string name="mode_directaudio">DirectAudio pro hry</string> + <string name="mode_directaudio_hint">Obchází PulseAudio pro nižší latenci.</string> + <string name="mode_mic">Mikrofon</string> + <string name="mode_mic_hint">Pro hlasový chat.</string> + <string name="mode_network">Síť</string> + <string name="mode_wifi_discovery">Vyhledávání Wi-Fi</string> + <string name="mode_wifi_discovery_hint">Zobrazí názvy Wi-Fi a okolních sítí ve Steamu.</string> + <string name="mode_wifi_location_off">Služby určování polohy jsou vypnuté.</string> + <string name="mode_wifi_permission_denied">Oprávnění k poloze nebylo uděleno.</string> + <string name="mode_wifi_open_settings">Otevřít nastavení</string> + <string name="mode_wifi_continue">Pokračovat</string> + <string name="mode_wifi_explain_title">Povolit vyhledávání Wi-Fi?</string> + <string name="mode_wifi_explain_text">Android vyžaduje oprávnění k poloze pro čtení názvů Wi-Fi a vyhledávání okolních sítí. DroidDeck tento přístup využívá k zobrazení těchto sítí ve Steamu. Vaši polohu nesledujeme. Přístup k internetu funguje i bez tohoto oprávnění.</string> + <string name="mode_storage">Úložiště her</string> + <string name="mode_storage_auto">Automaticky – karta SD, pokud je k dispozici, a vaše složky Games</string> + <string name="mode_storage_auto_none">Automaticky – vaše složky Games a karta SD, pokud je k dispozici (nyní žádná)</string> + <string name="mode_storage_internal">Pouze interní</string> + <string name="mode_storage_folder">Složka: %1$s</string> + <string name="mode_second_library">Druhá knihovna</string> + <string name="mode_storage_note">Hry, které streamují data z karty SD nebo sdíleného úložiště, mohou trhat.</string> + <string name="mode_storage_diagnostics">Diagnostika úložiště</string> + <string name="mode_storage_diagnostics_hint">Zaznamenává výsledky alokace a vzorky časování úložiště do sdílených protokolů. Když je zapnuto, může zpomalit instalace. Použije se od příští relace.</string> + <string name="mode_choose_folder">Vybrat složku…</string> + <string name="mode_custom_res_title">Vlastní rozlišení</string> + <string name="mode_custom_res_text">Nahradí limit i poměr stran. Velikost, která neodpovídá obrazovce, získá pruhy.</string> + <string name="mode_width">Šířka</string> + <string name="mode_height">Výška</string> + <string name="mode_custom_res_range">Mezi 320×240 a 3840×2160.</string> + <plurals name="mode_added_count"> + <item quantity="one">%1$d hra</item> + <item quantity="few">%1$d hry</item> + <item quantity="many">%1$d hry</item> + <item quantity="other">%1$d her</item> + </plurals> + <string name="common_ok">OK</string> + <string name="common_delete">Smazat</string> + <string name="common_restore">Obnovit</string> + <string name="common_download">Stáhnout</string> + <string name="common_delete_named">Smazat %1$s</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Ovladač runtime</string> + <string name="comp_runtime_driver_hint">Používá Steam, hry i Plocha. Použije se od příští relace.</string> + <string name="comp_import_turnip">Importovat zip Turnip…</string> + <string name="comp_display_driver">Ovladač displeje</string> + <string name="comp_display_driver_hint">Vykresluje snímky na obrazovku v obou režimech. Pro použití restartujte aplikaci.</string> + <string name="comp_import_adrenotools">Importovat zip AdrenoTools…</string> + <string name="comp_never">nikdy</string> + <string name="comp_nightlies_checked">Nightlies zkontrolovány %1$s</string> + <string name="comp_title">Komponenty</string> + <string name="comp_about">O komponentách</string> + <string name="comp_check_drivers">Zkontrolovat nové ovladače</string> + <string name="comp_check_nightlies">Zkontrolovat Nightlies na nové balíčky</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">Proton vybraný zde spouští každou hru, která si ve Steamu nevybrala svůj vlastní, a změna provedená ve Steamu se zobrazí i tady. Steam při aktualizaci nahrazuje Proton od Valve; originály jsou zachovány pro každé sestavení.</string> + <string name="comp_scope">Konfigurace</string> + <string name="comp_profile_configure">Konfigurovat profil</string> + <string name="comp_profile">Profil</string> + <string name="comp_scope_general">Výchozí profil</string> + <string name="comp_scope_general_detail">Automaticky používán hrami bez vlastního přepsání.</string> + <string name="comp_profile_game_detail">Uložené nastavení se automaticky použije při spuštění %1$s.</string> + <string name="comp_game_hint">Vyberte jen nastavení, která tato hra potřebuje. Vše ostatní dál používá Výchozí profil.</string> + <string name="comp_game_fex">Předvolba FEX</string> + <string name="comp_game_fex_inherited">Používá se předvolba Výchozího profilu.</string> + <string name="comp_game_fex_override">Pro tuto hru je uložena vlastní předvolba.</string> + <string name="comp_game_fex_custom">Vlastní proměnné FEX</string> + <string name="comp_game_use_general">Použít Výchozí</string> + <string name="comp_game_compatibility">Kompatibilita hry</string> + <string name="comp_game_environment_hint">Možnosti prostředí Proton, DXVK, VKD3D a synchronizace pro tuto hru.</string> + <string name="comp_game_proton_inherited">Používá se Proton Výchozího profilu.</string> + <string name="comp_game_proton_override">Pro tuto hru je přes mapování kompatibility Steamu uložen vlastní Proton.</string> + <string name="comp_no_proton_short">Žádný Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Používá balíček Výchozího profilu z %1$s, pokud tato hra nevybere jiný balíček.</string> + <string name="comp_game_component_general">Používá balíček komponent Výchozího profilu.</string> + <string name="comp_game_component_override">Pro tuto hru používá jen %1$s.</string> + <string name="comp_game_runtime_options">Možnosti runtime</string> + <string name="comp_game_environment_other_hint">Další možnosti prostředí Proton, DXVK, VKD3D-Proton a synchronizace pro jednotlivé hry.</string> + <string name="comp_game_saved_next">Profil %1$s je uložen pro další spuštění hry.</string> + <string name="game_no_steam_title">Spustit bez Steamu</string> + <string name="game_no_steam_detail">Pro hru, která se hned zavře: spustí ji bez nastavení Steamu v Protonu. Funkce Steamu, jako je překryvná vrstva, jsou pro ni vypnuté.</string> + <string name="game_no_steam_on">Zapnuto</string> + <string name="comp_game_proton_live">Výběr Protonu pro %1$s je uložen a hned dostupný ve Steamu.</string> + <string name="comp_game_proton_next">Výběr Protonu pro %1$s je uložen. Před spuštěním hry restartujte Steam.</string> + <string name="comp_game_proton_not_runnable">%1$s nemůže na tomto zařízení použít vybraný Proton.</string> + <string name="comp_game_load_failed">Nepodařilo se načíst profil %1$s: %2$s</string> + <string name="comp_game_save_failed">Profil hry se nepodařilo uložit: %1$s</string> + <string name="comp_game_reset">Resetovat profil hry</string> + <string name="comp_game_reset_title">Resetovat profil %1$s?</string> + <string name="comp_game_reset_body">Odebrat Proton, FEX, balíček komponent, prostředí, filtrování textur a přepsání komponent Windows u této hry? Bude používat Výchozí profil.</string> + <string name="comp_game_reset_done">%1$s nyní používá Výchozí profil.</string> + <string name="comp_game_reset_failed">Profil hry se nepodařilo úplně resetovat: %1$s</string> + <string name="comp_default_live">%1$s nyní spouští každou hru, která si ve Steamu nevybrala svůj vlastní Proton.</string> + <string name="comp_default_next">%1$s bude od dalšího spuštění Steamu spouštět každou hru, která si nevybrala svůj vlastní Proton.</string> + <string name="comp_default_not_runnable">%1$s zde nemůže spouštět hry, takže nemůže být výchozí.</string> + <string name="comp_default_failed">Výchozí Proton se nepodařilo uložit: %1$s</string> + <string name="comp_swapped">%1$s · %2$d nahrazeno</string> + <string name="comp_all_original">%1$s · vše originální</string> + <string name="comp_game_running_suffix">%1$s · hra běží</string> + <string name="comp_esync_pack">%1$s · balíček droiddeck-esync</string> + <string name="comp_esync_wanted">%1$s · zatím bez balíčku droiddeck-esync</string> + <string name="comp_esync_builtin">%1$s · esync je vestavěný, balíček nepotřeba</string> + <string name="comp_gpu_drivers">Ovladače GPU</string> + <string name="comp_reapplied">%1$s · znovu použito při spuštění %2$s</string> + <string name="comp_next_queued">Další: %1$s · po zavření hry</string> + <string name="comp_in_use">%1$s v použití: %2$s</string> + <string name="comp_waits">Čeká na zavření hry</string> + <string name="comp_applies_next_game">Použije se při dalším spuštění hry</string> + <string name="comp_import_zip">Importovat .zip</string> + <string name="comp_reading">Čtení Protonů…</string> + <string name="comp_no_proton">Zatím není nainstalován žádný Proton. Jednou spusťte Steam a stáhněte jeho Proton pro ARM64, nebo přidejte GE-Proton, CachyOS nebo DroidDeck-Proton v Nastavení.</string> + <string name="comp_original">Originál · %1$s</string> + <string name="comp_this_build">%1$s · toto sestavení</string> + <string name="comp_earlier_build">%1$s · dřívější sestavení</string> + <string name="comp_installed_count">Nainstalováno · %1$d</string> + <string name="comp_nothing_stored">Pro %1$s zatím nic není uloženo. Vlastní soubory tohoto Protonu jsou uloženy jako jeho originál.</string> + <string name="comp_waits_long">Na tomto Protonu běží hra, takže se čeká na její zavření.</string> + <string name="comp_applies_long">Použije se při dalším spuštění hry.</string> + <string name="comp_swap_title">Nahradit %1$s?</string> + <string name="comp_swap_body">Vložit %1$s do %2$s? Jeho původní soubory zůstanou uložené. %3$s</string> + <string name="comp_restore_title">Obnovit %1$s?</string> + <string name="comp_restore_body">Vrátit originál %1$s zpět do %2$s? %3$s</string> + <string name="comp_delete_package_body">Odstraní balíček z aplikace. Můžete ho znovu stáhnout.</string> + <string name="comp_delete_original_title">Smazat tento originál?</string> + <string name="comp_delete_original_body">Tento originál %1$s je z dřívějšího sestavení %2$s. Originál aktuálního sestavení je vždy zachován.</string> + <string name="comp_nightlies_count">Na Nightlies · %1$d</string> + <string name="comp_refresh_to_list">Obnovte pro zobrazení balíčků na Nightlies.</string> + <string name="comp_nothing_new">Od poslední kontroly nic nového.</string> + <string name="comp_import_wcp">Importovat .wcp</string> + <string name="comp_restore_original">Obnovit originál tohoto Protonu</string> + <string name="comp_restore_original_body">Vrátit zpět originální %2$s od %1$s?</string> + <string name="comp_swap">Nahradit</string> + <string name="comp_about_text">FEX, DXVK a VKD3D-Proton pro každý Proton, se kterým Steam spouští hry.\n\nVyberte nainstalovaný řádek pro jeho nahrazení. Použije se při dalším spuštění hry, nebo po zavření běžící hry.\n\nVaše volby se před každým spuštěním znovu zkontrolují a vrátí zpět, pokud je něco změnilo. Původní soubory každého Protonu jsou zachovány, takže je aktualizace Steamu nikdy neztratí.</string> + <string name="comp_tag_original">Originál</string> + <string name="comp_tag_in_use">Používá se</string> + <string name="comp_tag_new">Nové</string> + <string name="comp_tag_stored">Uloženo</string> + <string name="perf_title">Výkon</string> + <string name="perf_lede">Jádra CPU, opravy relace a název hostitele. Použije se od příští relace.</string> + <string name="perf_client_cores_group">Jádra pro klienta Steam</string> + <string name="perf_override">Přepsat výběr jader Steamu</string> + <string name="perf_override_hint">Připne Steam, jeho pomocníka rozhraní a gamescope na jádra níže.</string> + <string name="perf_client_cores">Jádra klienta</string> + <string name="perf_client_cores_on">Jádra pro Steam.</string> + <string name="perf_client_cores_off">Pro výběr zapněte přepsání.</string> + <string name="perf_game_cores_group">Jádra pro hru</string> + <string name="perf_game_cores">Jádra pro hru</string> + <string name="perf_game_cores_hint">Jádra pro hry. Se všemi vybranými plánuje Android volně.</string> + <string name="perf_client_ui">Rozhraní klienta</string> + <string name="perf_glthread">Vícevláknové GL</string> + <string name="perf_glthread_hint">Může zrychlit odezvu nabídek Steamu.</string> + <string name="perf_zink">Zink: líné deskriptory</string> + <string name="perf_zink_hint">Doporučeno pro ovladače bez descriptor bufferů.</string> + <string name="perf_noglerror">Vynechat kontroly chyb GL</string> + <string name="perf_noglerror_hint">Vypne ověřování GL při každém volání.</string> + <string name="perf_gsrealtime">gamescope: fronta GPU v reálném čase</string> + <string name="perf_gsrealtime_hint">Dává prioritu práci GPU kompozitoru. Může vyhladit tempo snímků na úkor her.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">Udržet GPU na nejvyšších taktech</string> + <string name="perf_gpuclock_hint">Jen Adreno. Méně zadrhávání, více spotřeby baterie a tepla.</string> + <string name="perf_fixes">Opravy relace</string> + <string name="perf_sysmem">Turnip: vykreslování sysmem</string> + <string name="perf_sysmem_hint">Rychlejší pro Steam a většinu her. Vyžadováno na Adreno 710/720/722.</string> + <string name="perf_xalia">Vynechat Steamova pomocníka xalia</string> + <string name="perf_fastsync">droiddeck-ntsync (experimentální)</string> + <string name="perf_fastsync_hint">Uživatelská náhrada linuxového ovladače ntsync, pro testování. Když je zapnuto, Proton ho používá místo droiddeck-esync a droiddeck-fsync. Funguje s Proton Experimental ARM64, GE-Proton a proton-cachyos. Použije se od příští relace.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync jako první</string> + <string name="perf_fsync_hint">Spouští vlastní fsync Protonu na původních souborech Protonu, s futex_waitv dodaným v uživatelském prostoru, takže nepotřebuje balíček a funguje i hned po aktualizaci Protonu Steamem. Když je zapnuto, hry ho používají před droiddeck-esync, kromě těch pár, které Proton u fsync nechává vypnuté. Použije se od příští relace.</string> + <string name="perf_esync_hint">Vlákna Windows her na sebe čekají přes eventfd a sdílenou paměť místo zpráv přes wineserver. Používá balíček droiddeck-esync vytvořený přesně pro dané sestavení Protonu; balíčky se stahují automaticky. Dokud pro nové sestavení Protonu žádný neexistuje, zastupuje ho droiddeck-fsync. Použije se od příští relace.</string> + <string name="perf_xalia_hint">Vypne pomocníka Protonu pro navigaci gamepadem, který stojí výkon CPU v každé hře.</string> + <string name="perf_seccomp">Spustit proot bez seccomp</string> + <string name="perf_seccomp_hint">Může opravit chyby s chybějícími systémovými voláními na některých jádrech. Může snížit výkon.</string> + <string name="perf_fastpath">Rychlá cesta proot</string> + <string name="perf_fastpath_needs_seccomp">Vyžaduje filtr seccomp od proot.</string> + <string name="perf_fastpath_hint">Rychlejší vyhledávání souborů a méně zadrhávání. Vypněte, pokud program nemůže najít své soubory.</string> + <string name="perf_identity">Identita relace</string> + <string name="perf_hostname">Název hostitele</string> + <string name="perf_hostname_hint">Název, který Steam vidí pro toto zařízení. Prázdné obnoví %1$s.</string> + <string name="perf_hostname_invalid">Pouze písmena, číslice a vnitřní pomlčky, až 63 znaků.</string> + <string name="perf_phantom_warning">Android je nastaven tak, aby tuto relaci ukončil</string> + <string name="driver_installed">Nainstalováno</string> + <string name="driver_available">K dispozici ke stažení</string> + <string name="driver_nothing_new">Od poslední kontroly nic nového.</string> + <string name="driver_restore_builtin">Obnovit vestavěné ovladače</string> + <string name="driver_delete_builtin">Vestavěné v aplikaci: toto odstraní jeho rozbalené soubory a skryje ho. Obnovit vestavěné ovladače ho vrátí zpět.</string> + <string name="driver_delete_bundle">Polovina balíčku Android + Linux: odstraní se obě poloviny. Používaný ovladač se vrátí na výchozí.</string> + <string name="driver_delete_plain">Jeho soubory se odstraní z aplikace.</string> + <string name="driver_delete_in_use">Jeho soubory se odstraní. Používá se, takže převezme výchozí.</string> + <string name="drawer_page_display">Displej</string> + <string name="drawer_page_effects">Efekty</string> + <string name="drawer_page_controls">Ovládání</string> + <string name="drawer_page_components">Komponenty</string> + <string name="drawer_page_controller">Ovladač</string> + <string name="drawer_page_settings">Nastavení</string> + <string name="drawer_page_desc">Stránka %1$s</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Plocha</string> + <string name="drawer_steam_menu">Nabídka Steam</string> + <string name="drawer_quick_access">Rychlý přístup</string> + <string name="drawer_open_qam">Otevřít nabídku Rychlý přístup</string> + <string name="drawer_prev_page">Předchozí stránka</string> + <string name="drawer_next_page">Další stránka</string> + <string name="drawer_hud">HUD výkonu</string> + <string name="drawer_scaling">Režim škálování</string> + <string name="drawer_scaling_sharpness">Ostrost škálování</string> + <string name="drawer_effects">Efekty obrazovky</string> + <string name="drawer_effects_hint">Živě. Stojí trochu výkonu GPU, když je zapnutý jakýkoli efekt.</string> + <string name="drawer_look">Předvolby</string> + <string name="drawer_look_custom">Vlastní</string> + <string name="drawer_effects_advanced">Pokročilé</string> + <string name="widgets_expanded">Rozbaleno</string> + <string name="widgets_collapsed">Sbaleno</string> + <string name="drawer_cas">Zaostření (CAS)</string> + <string name="drawer_cas_level">Úroveň zaostření</string> + <string name="drawer_fake_hdr">Falešné HDR</string> + <string name="drawer_deband">Odstranění pruhování</string> + <string name="drawer_deband_strength">Síla odstranění pruhování</string> + <string name="drawer_brightness">Jas</string> + <string name="drawer_contrast">Kontrast</string> + <string name="drawer_gamma">Gama</string> + <string name="drawer_saturation">Sytost</string> + <string name="drawer_fxaa">FXAA (vyhlazení hran)</string> + <string name="drawer_toon">Kreslené obrysy</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Filtrování textur</string> + <string name="drawer_texture_hint">Hry na DirectX 9–11, od jejich dalšího spuštění.</string> + <string name="drawer_anisotropy">Anizotropní filtrování</string> + <string name="drawer_texture_sharpness">Ostrost textur</string> + <string name="drawer_texture_sharpness_note">Záporný mip bias: ostřejší textury, trochu mihotání. Automaticky odpovídá tomu, jak moc je relace zvětšena na obrazovku.</string> + <string name="drawer_touch_auto">Automaticky (%1$s)</string> + <string name="drawer_keyboard">Klávesnice</string> + <string name="drawer_pc_keyboard">Klávesnice PC</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">Druhá obrazovka</string> + <string name="drawer_second_screen_mode">Režim</string> + <string name="drawer_second_screen_shows">Zobrazuje</string> + <string name="drawer_android_apps">Aplikace Android</string> + <string name="drawer_launch_app">Spustit aplikaci</string> + <string name="common_hide">Skrýt</string> + <string name="common_show">Zobrazit</string> + <string name="drawer_no_apps">Žádné spustitelné aplikace</string> + <string name="drawer_session_behavior">Chování relace</string> + <string name="drawer_suspend_hint">Použije se nyní i pro budoucí relace v tomto režimu.</string> + <string name="drawer_next_session">Příští relace</string> + <string name="drawer_support">Podpora</string> + <string name="drawer_logs">Protokoly relace</string> + <string name="drawer_logs_hint">Přiložit k hlášení chyby</string> + <string name="drawer_share_logs">Sdílet protokoly</string> + <string name="drawer_background">Pozadí</string> + <string name="drawer_stop">Ukončit</string> + <string name="drawer_stop_title">Ukončit relaci?</string> + <string name="drawer_running_now">Právě běží</string> + <string name="drawer_no_game">Na Protonu neběží žádná hra</string> + <string name="drawer_selected_profile">Profil hry</string> + <string name="drawer_selected_profile_none">Podrobnosti profilu zatím nejsou k dispozici.</string> + <string name="drawer_profile_refresh_failed_initial">Nepodařilo se načíst profily her. Automaticky se to zkouší znovu.</string> + <string name="drawer_profile_refresh_failed_stale">Nepodařilo se obnovit profily her. Zobrazují se poslední dostupná data; automaticky se to zkouší znovu.</string> + <string name="drawer_profile_inspect_hint">Zobrazit a upravit uložené nastavení použité při dalším spuštění.</string> + <string name="drawer_profile_default">Používá se Výchozí profil</string> + <string name="drawer_profile_custom">Vlastní profil</string> + <string name="drawer_component_inherited">Používá se %1$s z Výchozího profilu.</string> + <string name="drawer_game_components_note">Změny se uloží do profilu %1$s a použijí se při dalším spuštění hry.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d přepsání hry</item> + <item quantity="few">%1$d přepsání hry</item> + <item quantity="many">%1$d přepsání hry</item> + <item quantity="other">%1$d přepsání hry</item> + </plurals> + <string name="drawer_no_proton">Zatím není nainstalován žádný Proton.</string> + <string name="drawer_game_running">hra běží</string> + <string name="drawer_reapplied">znovu použito při spuštění %1$s</string> + <string name="drawer_original">Originál</string> + <string name="drawer_components_note">Import a mazání na stránce Komponenty.</string> + <string name="setup_check_done">Hotovo</string> + <string name="setup_check_warn">Vyžaduje pozornost</string> + <string name="setup_check_busy">Zpracovává se</string> + <string name="setup_limit_adb_failed">Zkontrolujte IP adresu a port pro Bezdrátové ladění, nebo zařízení znovu spárujte, pokud ho Android odstranil.</string> + <string name="setup_install">Instalovat</string> + <string name="setup_manage">Správa</string> + <string name="setup_tab_overview">Přehled</string> + <string name="setup_tab_controller">Ovladač</string> + <string name="setup_tab_session">Relace</string> + <string name="setup_tab_launcher">Spouštěč</string> + <string name="setup_title">Příprava</string> + <string name="setup_all_set">● Vše připraveno</string> + <string name="setup_n_ready">%1$d z %2$d připraveno</string> + <string name="setup_system_check">Kontrola systému</string> + <string name="setup_gpu_supported">Zařízení podporováno</string> + <string name="setup_gpu_untested">Neověřené GPU</string> + <string name="setup_gpu_unsupported">GPU nepodporováno</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">Steam vyžaduje ovladač Adreno; %1$s může zobrazit černou obrazovku</string> + <string name="setup_runtime">Běhové prostředí Linuxu</string> + <string name="setup_runtime_progress">%1$s · %2$d %%</string> + <string name="setup_runtime_missing">Nenainstalováno · přibližně 3 GB, instaluje se při prvním Play</string> + <string name="setup_installed">Nainstalováno</string> + <string name="setup_runtime_update">%1$s · dostupná aktualizace</string> + <string name="setup_runtime_current">%1$s · aktuální</string> + <string name="setup_limit">Limit podprocesů</string> + <string name="setup_limit_on">Zapnuto · Android může Steam zavřít. Vypnutí trvá minutu.</string> + <string name="setup_limit_unset">Nenastaveno · Android může Steam zavřít. Vypnutí trvá minutu.</string> + <string name="setup_limit_unknown">Nelze zkontrolovat · vypnutí trvá minutu.</string> + <string name="setup_fix_it">Opravit</string> + <string name="setup_details">Podrobnosti</string> + <string name="setup_dev_options">Možnosti pro vývojáře</string> + <string name="setup_use_wireless">Použít Bezdrátové ladění</string> + <string name="setup_check_again">Zkontrolovat znovu</string> + <string name="setup_limit_turn_on">Zapnout limit</string> + <string name="setup_account">Účet Steam</string> + <string name="setup_signed_in_offline">Přihlášen jako %1$s · režim offline</string> + <string name="setup_signed_in">Přihlášen jako %1$s</string> + <string name="setup_sign_in">Stiskněte Play a přihlaste se ke Steamu</string> + <string name="setup_tools">Nástroje</string> + <string name="setup_controller">Ovladač</string> + <string name="setup_controller_unavailable">Nastavení ovladače není k dispozici.</string> + <string name="setup_session">Relace</string> + <string name="setup_logs">Protokoly relace</string> + <string name="setup_logs_hint">Uloženo po každé relaci</string> + <string name="setup_agent_commands">Nástroje pro ladění</string> + <string name="setup_latest_logs">Protokoly poslední relace</string> + <string name="setup_saved_logs">Uložené protokoly relací</string> + <string name="setup_saved_logs_hint">Uchovává nejnovějších %1$d</string> + <string name="setup_clear_logs">Vymazat protokoly</string> + <string name="setup_offline">Režim offline</string> + <string name="setup_sign_in_first">Nejprve se přihlaste ke Steamu</string> + <string name="setup_launcher">Spouštěč</string> + <string name="setup_language">Jazyk</string> + <string name="setup_language_hint">Použije ho i Steam, od dalšího spuštění.</string> + <string name="setup_language_system">Výchozí systémový (%1$s)</string> + <string name="setup_language_system_short">Výchozí systémový</string> + <string name="setup_app_scale">Měřítko aplikace</string> + <string name="setup_app_scale_hint">Text a ovládání DroidDeck</string> + <string name="setup_app_scale_default">%1$d %% (výchozí)</string> + <string name="setup_theme">Téma</string> + <string name="setup_theme_hint">Vzhled spouštěče</string> + <string name="setup_home">Použít jako domovskou obrazovku</string> + <string name="setup_home_on">DroidDeck může být domovská aplikace</string> + <string name="setup_home_off">DroidDeck se nikdy nenabízí jako domovská aplikace</string> + <string name="setup_orientation">Orientace</string> + <string name="setup_orientation_auto">Automaticky na šířku</string> + <string name="setup_orientation_landscape">Na šířku</string> + <string name="setup_orientation_reverse">Na šířku obráceně</string> + <string name="setup_fullscreen">Celá obrazovka</string> + <string name="setup_fullscreen_on">Skryje stavový a navigační panel Androidu</string> + <string name="setup_fullscreen_off">Zobrazí stavový a navigační panel Androidu</string> + <string name="setup_default_home">Výchozí domovská aplikace</string> + <string name="setup_choose_home">Vybrat domovskou aplikaci</string> + <string name="setup_choose">Vybrat</string> + <string name="setup_linux_apps">Aplikace Linuxu (beta)</string> + <string name="setup_tool_files">Soubory</string> + <string name="setup_tool_files_hint">Procházení a správa souborů</string> + <string name="setup_tool_protons">Verze Protonu</string> + <string name="setup_tool_protons_hint">Instalace sestavení Protonu pro ARM64</string> + <string name="setup_tool_performance">Výkon</string> + <string name="setup_tool_performance_hint">Přiřazení jader CPU</string> + <string name="setup_tool_roms">Složka ROM</string> + <string name="setup_tool_roms_hint">Kde jsou uloženy hry pro emulátory</string> + <string name="setup_card_components">Komponenty</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Ovládání</string> + <string name="setup_card_controls_hint">Mapování tlačítek</string> + <string name="store_tab_discover">Objevit</string> + <string name="store_tab_search">Hledat</string> + <string name="store_tab_installed">Nainstalováno</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Obchod</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak připraven</string> + <string name="store_needs_runtime">Nejprve nainstalujte běhové prostředí Linuxu v Přípravě.</string> + <string name="store_setup_title">Nastavit Flatpak</string> + <string name="store_setup_text">Přidá Flatpak a připojí ho k Flathub (přibližně 20 MB). První aplikace si také stáhne svůj runtime.</string> + <string name="store_setting_up">Nastavování…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Aktualizace</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d %%</string> + <string name="store_starting">Spouští se…</string> + <string name="store_categories">Kategorie</string> + <string name="store_unreachable">Nepodařilo se spojit s Flathub. Zkontrolujte připojení a zkuste to znovu.</string> + <string name="store_try_again">Zkusit znovu</string> + <string name="store_search_label">Hledat na Flathub</string> + <string name="store_search_placeholder">Firefox, hry, emulátory…</string> + <string name="store_search">Hledat</string> + <string name="store_all">Vše</string> + <string name="store_searching">Hledání…</string> + <string name="store_search_hint">Hledejte podle názvu nebo vyberte kategorii. Uvedeny jsou jen aplikace pro ARM64.</string> + <string name="store_no_results">Tomu na Flathub neodpovídá žádná aplikace pro ARM64.</string> + <string name="store_results">Výsledky</string> + <string name="store_check_updates">Zkontrolovat aktualizace</string> + <string name="store_update_all">Aktualizovat vše (%1$d)</string> + <string name="store_all_current">● Vše je aktuální</string> + <string name="store_no_apps">Zatím žádné aplikace. Najděte nějaké v Objevit nebo Hledat.</string> + <string name="store_setup_first_note">Pro instalaci aplikací nastavte Flatpak.</string> + <string name="store_update_available_prefix">Dostupná aktualizace · %1$s</string> + <string name="store_open">Otevřít</string> + <string name="store_details">Podrobnosti</string> + <string name="store_remove">Odebrat</string> + <string name="store_press_again">Pro odebrání stiskněte znovu</string> + <string name="store_installing">Instaluje se…</string> + <string name="store_install">Instalovat</string> + <string name="store_setup_first">Nejprve nastavte Flatpak</string> + <string name="store_download_size">%1$s ke stažení</string> + <string name="store_installed_chip">● Nainstalováno</string> + <string name="store_details_failed">Nepodařilo se načíst stránku této aplikace na Flathub.</string> + <string name="store_loading">Načítání z Flathub…</string> + <string name="store_no_arm">Na Flathub není sestavení pro ARM64, takže zde nelze nainstalovat.</string> + <string name="store_screenshots">Snímky obrazovky</string> + <string name="store_about">O aplikaci</string> + <string name="store_app_id">ID aplikace</string> + <string name="store_license">Licence</string> + <string name="store_runtime">Runtime</string> + <string name="store_installed_size">Velikost po instalaci</string> + <string name="store_installed_size_value">%1$s (plus sdílený runtime)</string> + <string name="store_website">Web</string> + <string name="store_verified">Ověřeno</string> + <string name="upd_title">Aktualizace</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Zkontrolováno %1$s</string> + <string name="upd_not_checked">Zatím nezkontrolováno</string> + <string name="upd_perm_title">Povolit DroidDecku aktualizovat se sám</string> + <string name="upd_perm_text">Na další obrazovce zapněte Povolit z tohoto zdroje a vraťte se zpět. Aktualizace pak pokračuje sama.</string> + <string name="upd_open_settings">Otevřít nastavení</string> + <string name="upd_not_now">Teď ne</string> + <string name="upd_signed_status">Jinak podepsáno</string> + <string name="upd_signed_headline">Nelze aktualizovat na místě</string> + <string name="upd_signed_detail">Tato kopie není podepsána vydávacím klíčem DroidDecku, proto se přes ni aktualizace nedají nainstalovat. Odinstalujte ji, abyste mohli přejít.</string> + <string name="upd_available">Aktualizace k dispozici</string> + <string name="upd_blocked_status">Nelze instalovat</string> + <string name="upd_switch">Připraveno k přechodu</string> + <string name="upd_ahead_status">Před Stable</string> + <string name="upd_ahead_headline">Jste před verzí Stable</string> + <string name="upd_ahead_detail">Tato sestava je novější než aktuální Stable. Na Stable přejdete s jejím dalším vydáním.</string> + <string name="upd_gone_status">Test skončil</string> + <string name="upd_gone_headline">Tento test skončil</string> + <string name="upd_gone_detail">Jeho oprava byla sloučena nebo zrušena. Nejnovější opravy sledujte v Preview.</string> + <string name="upd_current_status">Aktuální</string> + <string name="upd_current_headline">Máte nejnovější %1$s</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Aktualizovat%1$s</string> + <string name="upd_install_button">Instalovat%1$s</string> + <string name="upd_install_anyway">Přesto instalovat Stable %1$s</string> + <string name="upd_follow_preview">Sledovat Preview</string> + <string name="upd_stop_session">Pro aktualizaci zastavte běžící relaci.</string> + <string name="upd_less">Méně</string> + <string name="upd_more">Více</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Nová sestava Preview</string> + <string name="upd_new_test">Testovací sestava PR #%1$d</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">AKTUALIZAČNÍ KANÁL</string> + <string name="upd_stable">Stable</string> + <string name="upd_stable_hint">Testovaná vydání, pro většinu lidí</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Preview</string> + <string name="upd_preview_hint">Nejnovější opravy, dříve než ve Stable</string> + <string name="upd_preview_current">Aktuální</string> + <string name="upd_tests">Testovací sestavy</string> + <string name="upd_tests_hint">Vyzkoušejte opravu před jejím vydáním</string> + <string name="upd_tests_none">Momentálně žádné</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Pro tuto kopii DroidDecku není sestavena</string> + <string name="upd_updated">Aktualizováno %1$s</string> + <string name="upd_test_name">test PR #%1$d</string> + <string name="upd_while_ago">před chvílí</string> + <string name="upd_just_now">právě teď</string> + <plurals name="upd_tests_count"> + <item quantity="one">%1$d k vyzkoušení</item> + <item quantity="few">%1$d k vyzkoušení</item> + <item quantity="many">%1$d k vyzkoušení</item> + <item quantity="other">%1$d k vyzkoušení</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="one">Změny od vaší sestavy · o %1$d sestavu pozadu</item> + <item quantity="few">Změny od vaší sestavy · o %1$d sestavy pozadu</item> + <item quantity="many">Změny od vaší sestavy · o %1$d sestav pozadu</item> + <item quantity="other">Změny od vaší sestavy · o %1$d sestav pozadu</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="one">Nedávné změny · %1$d sestava</item> + <item quantity="few">Nedávné změny · %1$d sestavy</item> + <item quantity="many">Nedávné změny · %1$d sestav</item> + <item quantity="other">Nedávné změny · %1$d sestav</item> + </plurals> + <string name="gate_title">Je třeba změnit jedno nastavení Androidu</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Android zastavuje aplikace, které spouštějí mnoho procesů na pozadí, a Steam jich spouští desítky. Dokud se to nezmění, hry se zavírají bez chybové zprávy.</string> + <string name="gate_fix_for_me">Oprav to za mě</string> + <string name="gate_toggle_steps">Zapněte „Disable child process restrictions“ a vraťte se zpět. Nechte Možnosti pro vývojáře zapnuté, jinak se přepínač znovu vypne.</string> + <string name="gate_open_dev">Otevřít Možnosti pro vývojáře</string> + <string name="gate_no_toggle">Tato verze Androidu pro to nemá přepínač, proto to DroidDeck změní přes Bezdrátové ladění. Zabere to asi minutu, počítač není potřeba.</string> + <string name="gate_needs_wifi">Bezdrátové ladění potřebuje Wi-Fi. Funguje jakákoli síť, internet není nutný.</string> + <string name="gate_needs_notifications">Párovací kód se zobrazí v oznámení. Povolte oznámení pro DroidDeck, nebo použijte Jiné způsoby.</string> + <string name="gate_allow_notifications">Povolit oznámení</string> + <string name="gate_step1">Otevřete Nastavení → Možnosti pro vývojáře → Bezdrátové ladění a zapněte ho</string> + <string name="gate_step2">Klepněte na „Spárovat zařízení pomocí párovacího kódu“</string> + <string name="gate_step3">Zadejte kód do oznámení DroidDecku</string> + <string name="gate_waiting">Čekání na párovací okno…</string> + <string name="gate_code_needed">Okno nalezeno. Zadejte jeho kód do oznámení.</string> + <string name="gate_status">Stav</string> + <string name="gate_polling">Kontroluje se každé 2 sekundy. Tato stránka se zavře, až bude vypnuto.</string> + <string name="gate_other_ways">Jiné způsoby</string> + <string name="gate_other_toggle">Bezdrátové ladění, ruční adresa, ADB z počítače</string> + <string name="gate_other_no_toggle">Ruční adresa, ADB z počítače, Možnosti pro vývojáře</string> + <string name="gate_use_wireless_instead">Použít místo toho Bezdrátové ladění</string> + <string name="gate_some_roms">Některé ROM přepínač přidávají i tak. Hledejte v Možnostech pro vývojáře „child process“.</string> + <string name="gate_manual_hint">Pokud oznámení nefunguje, zadejte adresu a kód z okna sami.</string> + <string name="gate_enter_manually">Zadat adresu ručně</string> + <string name="gate_from_computer">Z počítače s ADB:</string> + <string name="gate_copy_command">Kopírovat příkaz</string> + <string name="gate_choose_display">Vyberte, na kterém displeji se má otevřít Nastavení Androidu.</string> + <string name="gate_main_screen">Hlavní obrazovka</string> + <string name="wadb_finding_port">Spárováno. Hledání portu Bezdrátového ladění…</string> + <string name="wadb_enter_port">Spárováno. Zadejte port z Nastavení.</string> + <string name="wadb_applying">Zařízení nalezeno. Používá se nastavení…</string> + <string name="wadb_apply_failed">Nepodařilo se použít automaticky. Zkontrolujte adresu níže a zkuste to znovu. %1$s</string> + <string name="wadb_title_wireless">Bezdrátové ladění</string> + <string name="wadb_title_done">Omezení pro dětské procesy aktualizováno</string> + <string name="wadb_title">Změnit omezení pro dětské procesy</string> + <string name="wadb_lede_compact0">Pokud volba chybí, spárujte přes Bezdrátové ladění.</string> + <string name="wadb_lede_compact1">Spárováno. Zkontrolujte IP adresu a port v Bezdrátovém ladění.</string> + <string name="wadb_lede_done">Omezení pro dětské procesy bylo aktualizováno.</string> + <string name="wadb_lede0">Krok 2 použijte, pokud Možnosti pro vývojáře nemají nastavení pro dětské procesy. Otevřete Možnosti pro vývojáře → Bezdrátové ladění → Spárovat zařízení pomocí párovacího kódu.</string> + <string name="wadb_lede1">Spárováno. Použijte adresu níže, nebo ji aktualizujte z Bezdrátové ladění → IP adresa a port.</string> + <string name="wadb_lede_done_long">Omezení pro dětské procesy bylo aktualizováno přes Bezdrátové ladění.</string> + <string name="wadb_confirmed_on">Android potvrdil, že omezení je zapnuto.</string> + <string name="wadb_confirmed_off">Android potvrdil, že omezení je vypnuto.</string> + <string name="wadb_pair_group">2 · Spárovat Bezdrátové ladění</string> + <string name="wadb_pair_address">IP adresa a port párovacího okna</string> + <string name="wadb_pin">Párovací kód (PIN)</string> + <string name="wadb_pin_placeholder">6 číslic</string> + <string name="wadb_pair">Spárovat</string> + <string name="wadb_apply_group">2 · Použít přes Bezdrátové ladění</string> + <string name="wadb_connect_address">IP adresa a port pro připojení</string> + <string name="wadb_pair_again">Spárovat znovu</string> + <string name="wadb_turn_off">Vypnout omezení</string> + <string name="wadb_other_ways">Jiné způsoby změny</string> + <string name="wadb_dev_try_first">1 · Možnosti pro vývojáře (zkusit nejdřív)</string> + <string name="wadb_restrict_on">Zapněte „Restrict child processes“.</string> + <string name="wadb_restrict_off">Vypněte „Restrict child processes“.</string> + <string name="wadb_computer_last">3 · ADB z počítače (poslední možnost)</string> + <string name="wadb_only_if">Jen pokud Bezdrátové ladění není k dispozici.</string> + <string name="wadb_computer_group">3 · ADB z počítače (poslední možnost)</string> + <string name="ctrl_osc_hint">Kdy se dotykové ovládání zobrazí v relaci</string> + <string name="ctrl_color">Barva</string> + <string name="ctrl_color_hint">Odstín tlačítek na obrazovce</string> + <string name="ctrl_custom">Vlastní</string> + <string name="ctrl_opacity">Průhlednost</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">Velikost tlačítek</string> + <string name="ctrl_size_hint" formatted="false">100 % je standardní velikost</string> + <string name="ctrl_stick_click">Kliknutí páčkou</string> + <string name="ctrl_stick_click_hint">Dvojitým dotykem a podržením páčky aktivujete L3 nebo R3</string> + <string name="ctrl_adaptive">Adaptivní páčky</string> + <string name="ctrl_rumble">Vibrace ve hře</string> + <string name="ctrl_steam_button">Tlačítko Steam</string> + <string name="ctrl_qam_button">Tlačítko QAM</string> + <string name="ctrl_keyboard_button">Tlačítko klávesnice</string> + <string name="ctrl_select_steam">Otevřít nabídku Steam stisknutím Select</string> + <string name="ctrl_select_steam_hint">Místo View/Select.</string> + <string name="setup_animations">Animace</string> + <string name="ctrl_adaptive_hint">Páčky se zobrazí tam, kde se dotknete, a skryjí se po uvolnění</string> + <string name="ctrl_layout">Rozložení</string> + <string name="ctrl_layout_custom">Uloženy vlastní pozice</string> + <string name="ctrl_layout_default">Přizpůsobeno této obrazovce</string> + <string name="ctrl_edit">Upravit</string> + <string name="ctrl_reset">Obnovit</string> + <string name="ctrl_mapping">Mapování tlačítek</string> + <string name="ctrl_mapping_default">Každé tlačítko odesílá svůj vlastní vstup</string> + <string name="ctrl_mapping_count">%1$d z %2$d tlačítek přemapováno</string> + <string name="ctrl_configure">Konfigurovat</string> + <string name="ctrl_reset_all">Obnovit ovladač</string> + <string name="ctrl_reset_all_hint">Obnoví všechna nastavení ovladače a rozložení</string> + <string name="ctrl_mapping_lede">Co každé tlačítko na obrazovce odesílá hře. Skryté tlačítko odstraní.</string> + <string name="ctrl_onscreen_buttons">Tlačítka na obrazovce</string> + <string name="ctrl_defaults">Výchozí</string> + <string name="ctrl_reset_mapping">Obnovit mapování</string> + <string name="content_android_apps">Aplikace Android</string> + <string name="content_apps_count">%1$d aplikací</string> + <string name="content_no_android_apps">Nebyly nalezeny žádné spustitelné aplikace Android.</string> + <string name="content_desktop_installed">● Desktop nainstalován</string> + <string name="content_desktop_first_open">Nainstaluje se při prvním otevření</string> + <string name="content_available">K instalaci</string> + <string name="content_not_installed">Nenainstalováno.</string> + <string name="content_not_installed_suffix">%1$s · nenainstalováno</string> + <string name="content_open_named">Otevřít %1$s</string> + <string name="content_games">Hry</string> + <string name="content_choose_roms">Vyberte složku ROMs.</string> + <string name="content_retroarch_roms">Přejděte v RetroArchu na /root/ROMs.</string> + <string name="content_add_roms">Přidejte hry %1$s do ROMs/%2$s.</string> + <string name="content_install_named">Instalovat %1$s</string> + <string name="content_stop_to_install">Instalace vyžaduje zastavení relace</string> + <string name="content_runtime_required">Vyžadován runtime</string> + <string name="content_loading_details">Načítání podrobností instalace…</string> + <string name="content_details_unavailable">Podrobnosti instalace nejsou k dispozici. Zkuste to později.</string> + <string name="content_needs_runtime">Nejprve nainstalujte Linuxový runtime z Přípravy.</string> + <string name="content_stop_first">Pro instalaci desktopových aplikací zastavte běžící relaci.</string> + <string name="content_install_into">Instaluje %1$s do Linuxového desktopu.</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">Skrytím se odstraní z Desktopu. Soubory zůstanou v Linuxovém runtime.</string> + <string name="content_rom_gone">Tato hra už není ve složce ROMs.</string> + <string name="content_desktop_system">Desktop · %1$s</string> + <string name="content_launch_in">Spustit v %1$s</string> + <string name="content_also_in">Také v %1$s</string> + <string name="content_page_gone">Tato stránka už neexistuje. Vyberte sekci vlevo.</string> + <string name="content_open_desktop">Otevřít desktop</string> + <string name="content_install_open_desktop">Instalovat a otevřít desktop</string> + <string name="desktop_remove_title">Odinstalovat desktop?</string> + <string name="desktop_remove_text">Odstraní desktop a jeho vestavěné aplikace. Hry, přidané aplikace, nastavení a sdílené knihovny zůstanou.</string> + <string name="desktop_removing">Odstraňování desktopu</string> + <string name="desktop_removed">Desktop odstraněn</string> + <string name="desktop_cleaning_legacy">Odstraňování starých souborů LXQt</string> + <string name="desktop_removal_unknown">Před odstraněním této verze desktopu aktualizujte DroidDeck.</string> + <string name="content_linux_desktop">Linuxový desktop</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox a vaše emulátory</string> + <string name="games_empty">Hry, které nainstalujete ve Steamu, se zde zobrazí po relaci.</string> + <string name="games_play_steam">Hrát ve Steamu</string> + <string name="games_launch_settings">Nastavení spuštění</string> + <string name="games_launch">Spustit</string> + <string name="games_files">Soubory hry</string> + <string name="games_prefix">Prefix Protonu</string> + <string name="games_saves_looking">Hledání uložených pozic…</string> + <string name="games_saves_none">Podle názvu nebyla nalezena žádná složka s uloženými pozicemi. Export zabere celou uživatelskou složku.</string> + <string name="games_saves_folders">%1$d složek s uloženými pozicemi</string> + <string name="games_saves_summary">%1$s · %2$d souborů · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">Správa uložených pozic</string> + <string name="games_saves">Uložené pozice hry</string> + <string name="games_import_saves">Importovat uložené pozice…</string> + <string name="games_export_gamehub">Exportovat jako GameHub zip…</string> + <string name="games_export_winlator">Exportovat jako Winlator zip…</string> + <string name="games_open_save_folder">Otevřít složku s uloženými pozicemi</string> + <string name="games_never_played">Nikdy nehráno</string> + <string name="games_runtime_busy_percent">Runtime zaneprázdněn · %1$d%%</string> + <string name="games_runtime_busy">Runtime zaneprázdněn</string> + <string name="games_runtime_first_play">Runtime se nainstaluje při prvním spuštění hry</string> + <string name="games_runtime_update">K dispozici aktualizace runtime</string> + <string name="games_runtime_ready">● Runtime připraven</string> + <string name="games_last_played">Poslední hraní %1$s</string> + <string name="games_internal">Interní úložiště</string> + <string name="games_added">Přidaná hra</string> + <string name="games_custom">Vlastní hra</string> + <string name="games_store_storage">Úložiště her</string> + <string name="gpu_auto_hint">Nejlepší dvojice pro tento GPU, udržovaná aktuální</string> + <string name="gpu_manual_hint">Vyberte dvojici sami</string> + <string name="gpu_checking">Hledání ovladačů…</string> + <string name="gpu_none_listed">Zatím žádné ovladače. Obnovte, aby se vyhledaly.</string> + <string name="gpu_hide_other">Skrýt ovladače pro jiné GPU</string> + <string name="gpu_show_other">Zobrazit ovladače pro jiné GPU</string> + <string name="gpu_advanced">Pokročilé</string> + <string name="gpu_set_with_display">Nastaveno společně s ovladačem displeje: %1$s obsahuje oba.</string> + <string name="gpu_set_with_runtime">Nastaveno společně s ovladačem runtime: %1$s obsahuje oba.</string> + <string name="gpu_runtime_hint">Používají hry a desktop.</string> + <string name="gpu_runtime_default">Výchozí runtime</string> + <string name="gpu_display_hint">Vykresluje snímky na obrazovku. Použije se po restartu DroidDecku.</string> + <string name="gpu_auto_by_gpu">Automaticky - podle GPU</string> + <string name="gpu_manage">Stáhnout, importovat nebo smazat…</string> + <string name="gpu_in_use_caps">POUŽÍVÁ SE</string> + <string name="gpu_builtin">Ovladače vestavěné v DroidDecku</string> + <string name="gpu_matched_pair">Ovladače runtime a displeje nastaveny společně jako sladěná dvojice.</string> + <string name="gpu_recommended_for">Doporučeno pro tento GPU: %1$s.</string> + <string name="gpu_refresh_hint">Obnovte, aby se vyhledaly ovladače pro tento GPU.</string> + <string name="gpu_recommended_caps">DOPORUČENO</string> + <string name="gpu_unavailable">Není k dispozici</string> + <string name="load_runtime">Linuxový runtime</string> + <string name="load_runtime_detail">Příprava Linuxového runtime</string> + <string name="load_session">Relace</string> + <string name="load_session_detail">Spouštění obrazu a zvuku</string> + <string name="load_steam">Klient Steam</string> + <string name="load_steam_detail">Kontrola Steamu a jeho nástrojů kompatibility</string> + <string name="load_opening_steam">Otevírání Steamu</string> + <string name="load_opening_steam_detail">Čekání na první snímek Steamu</string> + <string name="load_desktop">Desktop</string> + <string name="load_desktop_detail">Příprava desktopu</string> + <string name="load_opening">Otevírání</string> + <string name="load_opening_detail">Čekání na první snímek</string> + <string name="load_starting_steam">Spouštění Steamu</string> + <string name="load_ended">Relace skončila</string> + <string name="load_step">Krok %1$d z %2$d</string> + <string name="load_steam_paused">Steam je na pauze</string> + <string name="deck_tab_grips">Rukojeti</string> + <string name="deck_tab_left">Levý panel</string> + <string name="deck_tab_right">Pravý panel</string> + <string name="deck_tab_both">Oba</string> + <string name="deck_tab_gamepad">Gamepad</string> + <string name="deck_surface_stick">Páčka</string> + <string name="deck_surface_trackpad">Trackpad</string> + <string name="deck_surface_switch">Přepnout mezi páčkou a trackpadem</string> + <string name="deck_more_controls">Další ovládání Deck</string> + <string name="deck_panels">PANELY</string> + <string name="deck_close">Zavřít ovládání druhé obrazovky</string> + <string name="deck_open_steam_menu">Otevřít nabídku Steamu</string> + <string name="deck_left_upper">Levý · horní</string> + <string name="deck_left_lower">Levý · dolní</string> + <string name="deck_right_upper">Pravý · horní</string> + <string name="deck_right_lower">Pravý · dolní</string> + <string name="deck_click">Kliknutí</string> + <string name="deck_right_trackpad">Pravý trackpad</string> + <string name="deck_left_trackpad">Levý trackpad</string> + <string name="second_trackpad">Trackpad</string> + <string name="second_show_keyboard">Zobrazit klávesnici</string> + <string name="second_hide_keyboard">Skrýt klávesnici</string> + <string name="second_terminal">Terminál Linuxu</string> + <string name="second_terminal_not_ready">Relace Linuxu ještě není připravena na terminál.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Hry</string> + <string name="rail_desktop">Plocha</string> + <string name="rail_store">Obchod</string> + <string name="rail_components">Komponenty</string> + <string name="rail_apps">Aplikace</string> + <string name="rail_setup">Příprava</string> + <string name="rail_updates">Aktualizace</string> + <string name="rail_percent">%1$d%%</string> + <string name="rail_resume_named">Obnovit %1$s</string> + <string name="roms_text">V každé relaci se zobrazí jako /root/ROMs. Vnitřní úložiště je v /root/Storage.</string> + <string name="roms_none">Nevybrána žádná složka</string> + <string name="roms_applies">Použije se v další relaci. SD karty fungují.</string> + <string name="roms_choose">Vybrat složku</string> + <string name="roms_change">Změnit složku</string> + <string name="proton_lede">Nainstalujte sestavení Protonu pro ARM64 a pak ho vyberte pro konkrétní hru v Steamu → Vlastnosti → Kompatibilita.</string> + <string name="proton_stop_first">Pro instalaci nebo odstranění nástroje kompatibility zastavte běžící relaci.</string> + <string name="proton_available">Dostupná sestavení</string> + <string name="proton_installed">Nainstalováno %1$s</string> + <string name="proton_queued">Čeká z dřívějšího požadavku</string> + <string name="proton_not_installed">Není nainstalováno</string> + <string name="proton_update_available">Dostupná aktualizace → %1$s</string> + <string name="proton_remove_older">Odstranit staré verze %1$s</string> + <string name="proton_remove_older_body">Odstraní %1$s. Hry nastavené na tyto verze je nutné přepnout na jiný Proton.</string> + <string name="proton_remove_body">Hry nastavené na tuto verzi je nutné přepnout na jiný Proton.</string> + <string name="proton_install_now">Instalovat nyní</string> + <string name="proton_cancel_queued">Zrušit čekající</string> + <string name="pckb_android">Klávesnice Androidu</string> + <string name="pckb_hide">Skrýt ✕</string> + <string name="main_launch_named">Spustit %1$s</string> + <string name="main_choose_screen">Vyberte obrazovku. Druhá: %1$s.</string> + <string name="main_secondary_gone">Druhá obrazovka už není dostupná.</string> + <string name="main_primary">Hlavní obrazovka</string> + <string name="main_secondary">Druhá obrazovka</string> + <string name="widgets_prev_tab">Předchozí karta</string> + <string name="widgets_next_tab">Další karta</string> + <string name="widgets_on">Zapnuto</string> + <string name="widgets_all_n">Vše %1$d</string> + <string name="widgets_none">Žádné</string> + <string name="widgets_n_of_m">%1$d z %2$d</string> + <string name="widgets_off">Vypnuto</string> + <string name="screen_bottom">Spodní obrazovka</string> + <string name="game_env_applies">Použije se při dalším spuštění hry. Profil hry má přednost před společným.</string> + <string name="game_env_other">Jiná hra podle ID aplikace…</string> + <string name="game_env_done">Hotovo</string> + <string name="game_env_default">Výchozí</string> + <string name="game_env_off">Vypnuto</string> + <string name="game_env_change">Změnit…</string> + <string name="game_env_change_title">Změnit proměnnou</string> + <string name="game_env_restore_default">Zpět na výchozí</string> + <string name="game_env_eyebrow">Hry přes Proton</string> + <string name="pip_title">Obraz v obraze</string> + <string name="pip_auto">Automaticky přejít do PiP</string> + <string name="unfolded_controls">Ovládání na rozložené obrazovce</string> + <string name="unfolded_controls_hint">Po rozložení je hra zobrazena nad trackpady a zadními rukojeťmi Steam Decku. Po složení je hra na celou obrazovku.</string> + <string name="pip_suspended">Pozastaveno</string> + <string name="pip_not_ready">Obraz v obraze bude dostupný, jakmile bude relace připravena.</string> + <string name="pip_permission">Povolte obraz v obraze pro DroidDeck v nastavení Androidu.</string> + <string name="pip_unavailable">Android nemohl otevřít obraz v obraze.</string> + <string name="game_shortcut">Zkratka</string> + <string name="game_shortcut_add">Přidat na domovskou obrazovku</string> + <string name="game_shortcut_choose">Vybrat hru</string> + <string name="game_shortcut_scanning">Vyhledávání nainstalovaných her…</string> + <string name="game_link_copy">Kopírovat odkaz pro spuštění</string> + <string name="game_link_copied">Odkaz pro spuštění zkopírován</string> + <string name="game_shortcut_unsupported">Tento spouštěč neumí přidávat zkratky. Použijte místo toho Kopírovat odkaz pro spuštění.</string> + <string name="game_link_invalid">Neplatný odkaz pro spuštění hry.</string> + <string name="game_link_missing">Tato hra už není ve vaší knihovně DroidDeck.</string> + <string name="game_link_session_busy">Před spuštěním hry ze Steamu zastavte aktuální relaci.</string> + <string name="game_link_failed">Hru se nepodařilo odeslat do Steamu. Zkuste ji spustit znovu.</string> + <string name="setup_retry_removal">Zkusit odstranění znovu</string> + <string name="runtime_removing">Odstraňování</string> + <string name="runtime_removal_incomplete">Odstranění nedokončeno</string> + <string name="game_file_export">Exportovat soubor hry</string> + <string name="game_file_sync">Synchronizovat všechny soubory her do složky</string> + <string name="game_file_stop_sync">Zastavit synchronizaci souborů her</string> + <string name="game_file_folder">Vybrat složku pro soubory her frontendu</string> + <string name="game_file_exported">Soubory hry uloženy do %1$s</string> + <string name="game_frontend_files">Soubory frontendu</string> + + <string name="main_wait_runtime">Počkejte, až skončí operace s runtime</string> + <string name="main_wait_proton_install">Počkejte, až skončí instalace nástroje kompatibility</string> + <string name="main_wait_desktop_install">Počkejte, až skončí instalace plochy</string> + <string name="main_wait_install_game">Před spuštěním hry počkejte, až skončí instalace</string> + <string name="main_saves_close_session">Nejprve zavřete relaci Steamu, aby hra svým uložením nepřepsala import.</string> + <string name="main_saves_importing">Importování do %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="one">Importován %1$d soubor z %2$s do %3$s</item> + <item quantity="few">Importovány %1$d soubory z %2$s do %3$s</item> + <item quantity="many">Importováno %1$d souborů z %2$s do %3$s</item> + <item quantity="other">Importováno %1$d souborů z %2$s do %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">Importován %1$d soubor z %2$s do %3$s. Staré uložené pozice byly zálohovány do Download/DroidDeck/Saves/backups</item> + <item quantity="few">Importovány %1$d soubory z %2$s do %3$s. Staré uložené pozice byly zálohovány do Download/DroidDeck/Saves/backups</item> + <item quantity="many">Importováno %1$d souborů z %2$s do %3$s. Staré uložené pozice byly zálohovány do Download/DroidDeck/Saves/backups</item> + <item quantity="other">Importováno %1$d souborů z %2$s do %3$s. Staré uložené pozice byly zálohovány do Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Vyberte ZIP s uloženou pozicí pro %1$s</string> + <string name="main_saves_exporting">Exportování %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="one">Exportován %1$d soubor jako %2$s: %3$s</item> + <item quantity="few">Exportovány %1$d soubory jako %2$s: %3$s</item> + <item quantity="many">Exportováno %1$d souborů jako %2$s: %3$s</item> + <item quantity="other">Exportováno %1$d souborů jako %2$s: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Vyberte, kam uložit %1$s (%2$s)</string> + <string name="main_action_failed">%1$s se nezdařilo: %2$s</string> + <string name="main_no_logs_yet">Zatím žádné logy relace: nejprve spusťte relaci.</string> + <string name="main_no_logs_to_clear">Žádné logy relace k vymazání.</string> + <plurals name="main_logs_cleared"> + <item quantity="one">Vyčištěna %1$d složka s logy relace.</item> + <item quantity="few">Vyčištěny %1$d složky s logy relace.</item> + <item quantity="many">Vyčištěno %1$d složek s logy relace.</item> + <item quantity="other">Vyčištěno %1$d složek s logy relace.</item> + </plurals> + <string name="main_wadb_pair_failed">Párování bezdrátového ladění se nezdařilo</string> + <string name="main_wadb_command_failed">Příkaz bezdrátového ladění se nezdařil</string> + <string name="main_wadb_unavailable">Bezdrátové ladění není k dispozici</string> + <string name="main_adb_copied">Příkaz ADB zkopírován</string> + <string name="main_pick_roms">Vybrat složku ROMs</string> + <string name="main_non_adreno_title">Není GPU Adreno</string> + <string name="main_non_adreno_text">Turnip podporuje GPU Adreno. Na %1$s může Steam zobrazit černou obrazovku. Stažení: %2$.0f MB.</string> + <string name="main_install_anyway">Přesto instalovat</string> + <string name="main_remove_runtime_title">Odstranit runtime Linuxu</string> + <string name="main_remove_runtime_text">Tím se odstraní runtime, klient Steam v něm a všechny hry v něm nainstalované.</string> + <string name="main_applied">Použito: %1$s</string> + <string name="android_app_open_failed_primary">Nepodařilo se otevřít %1$s na hlavní obrazovce</string> + <string name="android_app_open_failed_on">Nepodařilo se otevřít %1$s na %2$s</string> + <string name="android_app_open_failed_display">Nepodařilo se otevřít %1$s na obrazovce %2$d</string> + <string name="comp_busy_importing">Importování</string> + <string name="comp_busy_swapping">Přepínání</string> + <string name="comp_busy_restoring">Obnovování</string> + <string name="comp_busy_cancelling">Rušení</string> + <string name="comp_busy_deleting">Mazání</string> + <string name="comp_cannot_read">soubor nelze přečíst</string> + <string name="comp_imported">Importováno %1$s</string> + <string name="comp_swap_cancelled">Čekající přepnutí bylo zrušeno.</string> + <string name="comp_pick_wcp">Vybrat balíček komponenty (-linux .wcp)</string> + <string name="comp_nightlies_unreachable">Nepodařilo se spojit s Nightlies</string> + <string name="comp_stored">Uloženo %1$s</string> + <string name="comp_download_failed">Stažení se nezdařilo: %1$s</string> + <string name="drivers_pick_linux">Vybrat ovladač runtime Linuxu (ZIP -Linux nebo balíček Android + Linux)</string> + <string name="drivers_pick_android">Vybrat ovladač zobrazení (ZIP AdrenoTools nebo balíček Android + Linux)</string> + <string name="drivers_pick_any">Vybrat ZIP s ovladačem nebo balíček Android + Linux</string> + <string name="main_pkg_problem">%1$s: %2$s</string> + <string name="main_removing_named">Odstraňování %1$s…</string> + <string name="main_forgetting_named">Zapomínání %1$s…</string> + <string name="main_pick_game_storage">Vybrat složku úložiště her</string> + <string name="main_pick_decky_zip">Vybrat ZIP s pluginem Decky</string> + <string name="main_storage_not_usable">Nepoužitelné: %1$s</string> + <string name="main_running_program">Program</string> + <string name="main_removing_runtime">Odstraňování runtime Linuxu</string> + <string name="main_settings_main_screen">Nepodařilo se otevřít Nastavení na spodní obrazovce; otevřelo se na hlavní obrazovce.</string> + <string name="main_open_dev_options">Otevřít Možnosti pro vývojáře v Nastavení</string> + + <string name="upd_unknown_apps_failed">Nepodařilo se otevřít nastavení Androidu „Instalace neznámých aplikací“</string> + <string name="upd_couldnt_check">Nepodařilo se zkontrolovat: %1$s</string> + <string name="upd_no_download">Toto sestavení nemá stažení pro tuto kopii DroidDecku</string> + <string name="upd_stop_first">Nejprve zastavte běžící relaci, pak aktualizujte</string> + <string name="upd_stage_downloading">Stahování</string> + <string name="upd_stage_installing">Instalování</string> + <string name="upd_stage_confirm">Čeká na vaše potvrzení</string> + <string name="upd_failed">Aktualizace se nezdařila</string> + <string name="upd_not_offered">Android nenabídl instalaci aktualizace</string> + + <string name="decky_install_failed">Instalace Decky se nezdařila: %1$s</string> + <string name="decky_plugin_starting">Spouštění importu pluginu…</string> + <string name="decky_plugin_failed">Import pluginu se nezdařil: %1$s</string> + <string name="decky_plugin_installed">Plugin nainstalován. Restartujte Steam, aby se načetl.</string> + + <string name="drivers_not_checked">Zatím nezkontrolováno - klepnutím na obnovit vyhledáte nové ovladače</string> + <string name="drivers_bundle_sets_display">Android + Linux: zároveň nastaví ovladač zobrazení</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: zároveň nastaví ovladač runtime</string> + <string name="drivers_runtime_builtin">Turnip vestavěný v runtime</string> + <string name="drivers_system_vulkan">systémový Vulkan: pro toto GPU není přibalené sestavení</string> + <string name="drivers_bundled">%1$s (přibalený)</string> + <string name="drivers_manual">Manuální</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">Právě je zveřejněna jen jedna polovina</string> + <string name="drivers_pair_size">%1$.0f MB za oba</string> + <string name="drivers_none_to_set">Žádné ovladače k nastavení: %1$s</string> + <string name="drivers_checking_latest">Vyhledávání nejnovějších ovladačů…</string> + <string name="drivers_up_to_date_check_failed">Aktuální (nepodařilo se zkontrolovat: %1$s)</string> + <string name="drivers_recommended_unpublished">Doporučené ovladače nejsou právě zveřejněné</string> + <string name="drivers_downloading_pair">Stahování %1$s %2$s…</string> + <string name="drivers_updated_to">Aktualizováno na %1$s %2$s.</string> + <string name="drivers_updated_to_restart">Aktualizováno na %1$s %2$s. Ovladač zobrazení se použije po restartu DroidDecku.</string> + <string name="drivers_switched_to">Přepnuto na %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Přepnuto na %1$s %2$s. Ovladač zobrazení se použije po restartu DroidDecku.</string> + <string name="drivers_using">Používá se %1$s %2$s.</string> + <string name="drivers_using_restart">Používá se %1$s %2$s. Ovladač zobrazení se použije po restartu DroidDecku.</string> + <string name="drivers_not_a_driver">Toto není ZIP s ovladačem: ani ovladač AdrenoTools, ani Turnip -Linux, ani balíček Android + Linux</string> + <string name="drivers_import_failed">Import se nezdařil: %1$s</string> + <string name="drivers_imported_unnamed">Importovaný ovladač</string> + <string name="drivers_using_bundle">%1$s se používá jako ovladač runtime i zobrazení.</string> + <string name="drivers_using_bundle_restart">%1$s se používá jako ovladač runtime i zobrazení. Ovladač zobrazení se použije po restartu DroidDecku.</string> + <string name="drivers_deleted_bundle">Odstraněno %1$s (oba ovladače)</string> + <string name="drivers_deleted">Odstraněno %1$s</string> + <string name="drivers_release_status">Nejnovější: %1$s · zkontrolováno %2$s</string> + <string name="drivers_release_status_failed">Nejnovější: %1$s · %2$s nedostupné · zkontrolováno %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">před %1$d min</item> + <item quantity="few">před %1$d min</item> + <item quantity="many">před %1$d min</item> + <item quantity="other">před %1$d min</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">před %1$d h</item> + <item quantity="few">před %1$d h</item> + <item quantity="many">před %1$d h</item> + <item quantity="other">před %1$d h</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">před %1$d dnem</item> + <item quantity="few">před %1$d dny</item> + <item quantity="many">před %1$d dny</item> + <item quantity="other">před %1$d dny</item> + </plurals> + <string name="drivers_checking_sources">Kontrola Banners-Turnip, WinNative a DroidDeck…</string> + <string name="drivers_check_failed">kontrola se nezdařila</string> + <string name="drivers_installed_pick_either">Nainstalováno %1$s - vyberte ho v kterémkoli seznamu pro nastavení obou ovladačů</string> + <string name="drivers_installed_pick">Nainstalováno %1$s - vyberte ho v nabídce</string> + + <string name="session_wait_removal">Počkejte, až skončí odstranění runtime</string> + <string name="session_title_game">Hra</string> + <string name="session_touch_auto_touchpad">touchpad</string> + <string name="session_touch_auto_direct">přímý</string> + <string name="session_swap_failed">Přepnutí se nezdařilo: %1$s</string> + <string name="session_profile_save_failed">Uložení profilu se nezdařilo: %1$s</string> + <string name="session_desktop_paused">Plocha je na pauze</string> + <string name="session_named_paused">%1$s je na pauze</string> + <string name="session_program_paused">Program je na pauze</string> + <string name="session_starting_desktop">Spouštění plochy</string> + <string name="session_starting_named">Spouštění %1$s</string> + <string name="session_starting_program">Spouštění programu</string> + <string name="session_no_logs">Pro tuto relaci nejsou žádné logy.</string> + <string name="session_logs_archive_failed">Nepodařilo se vytvořit archiv logů relace.</string> + <string name="session_logs_share_failed">Nepodařilo se sdílet logy relace.</string> + <string name="session_starting_session">Spouštění relace…</string> + <string name="session_runtime_downloading">stahování runtime Linuxu</string> + <string name="session_runtime_downloading_mb">stahování runtime Linuxu · %1$d z %2$d MB</string> + <string name="session_runtime_checking">kontrola runtime Linuxu</string> + <string name="session_runtime_unpacking">rozbalování runtime Linuxu</string> + <string name="session_desktop_downloading">stahování plochy</string> + <string name="session_desktop_downloading_mb">stahování plochy · %1$d z %2$d MB</string> + <string name="session_desktop_checking">kontrola plochy</string> + <string name="session_desktop_unpacking">rozbalování plochy</string> + <string name="session_proton_downloading">stahování Proton Experimental (ARM64)</string> + <string name="session_proton_downloading_mb">stahování Proton Experimental (ARM64) · %1$d z %2$d MB</string> + <string name="session_proton_checking">kontrola Proton Experimental (ARM64)</string> + <string name="session_proton_unpacking">rozbalování Proton Experimental (ARM64)</string> + <string name="session_runtime_catalog_unreachable">Nepodařilo se spojit s katalogem runtime. Zkontrolujte připojení a znovu stiskněte Hrát.</string> + <string name="session_runtime_install_failed">Runtime Linuxu se nenainstaloval. Zkontrolujte runtime v Přípravě a znovu stiskněte Hrát.</string> + <string name="session_desktop_install_failed">Plocha se nenainstalovala (%1$s). Zkontrolujte připojení a znovu stiskněte Plocha.</string> + <string name="session_ended_named">Došlo k neočekávanému zastavení %1$s. Přiložte logy k hlášení chyby, nebo zkuste znovu.</string> + <string name="session_ended_program">Program neočekávaně skončil. Přiložte logy k hlášení chyby, nebo zkuste znovu.</string> + <string name="session_ended_desktop">Plocha neočekávaně skončila. Přiložte logy k hlášení chyby, nebo zkuste znovu.</string> + <string name="session_ended_detail">Návratový kód %1$d · %2$s</string> + <string name="session_ended_no_log">žádný log</string> + <plurals name="session_hint_enosys"> + <item quantity="one">V logu je %1$d chyba „Function not implemented“: akcelerace seccomp v proot zde selhává u pomocné funkce na tomto zařízení. Zkuste %2$s → „%3$s“ (nebo „%4$s“) a spusťte to znovu.</item> + <item quantity="few">V logu jsou %1$d chyby „Function not implemented“: akcelerace seccomp v proot zde selhává u pomocné funkce na tomto zařízení. Zkuste %2$s → „%3$s“ (nebo „%4$s“) a spusťte to znovu.</item> + <item quantity="many">V logu je %1$d chyb „Function not implemented“: akcelerace seccomp v proot zde selhává u pomocné funkce na tomto zařízení. Zkuste %2$s → „%3$s“ (nebo „%4$s“) a spusťte to znovu.</item> + <item quantity="other">V logu je %1$d chyb „Function not implemented“: akcelerace seccomp v proot zde selhává u pomocné funkce na tomto zařízení. Zkuste %2$s → „%3$s“ (nebo „%4$s“) a spusťte to znovu.</item> + </plurals> + + <string name="mode_decky_plugins">Pluginy</string> + <string name="mode_decky_plugins_hint">Nainstalujte ZIP s pluginem pro další relaci Steamu.</string> + <string name="mode_decky_plugin_downloading">Stahování…</string> + <string name="mode_decky_plugin_install_zip">Instalovat ze ZIP</string> + <string name="store_cat_games">Hry</string> + <string name="store_cat_audio_video">Zvuk a video</string> + <string name="store_cat_graphics">Grafika</string> + <string name="store_cat_internet">Internet</string> + <string name="store_cat_office">Kancelář</string> + <string name="store_cat_developer">Vývoj</string> + <string name="store_cat_education">Vzdělávání</string> + <string name="store_cat_science">Věda</string> + <string name="store_cat_system">Systém</string> + <string name="store_cat_utilities">Nástroje</string> + <string name="store_section_popular">Populární</string> + <string name="store_section_trending">Trendy</string> + <string name="store_section_new">Novinky</string> + <string name="store_section_updated">Aktualizováno</string> + <string name="store_section_loading">načítání…</string> + <string name="store_flatpak_setup">Nastavení Flatpaku</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Nepodařilo se zkontrolovat aktualizace na Flathubu</string> + <string name="content_rom_installed">nainstalováno</string> + <string name="content_rom_folder">složka</string> + <string name="theme_graphite">Grafit</string> + <string name="theme_graphite_detail">Grafitové povrchy, s modrou barvou kuličky z loga pro vše vybrané, zaměřené nebo stisknuté.</string> + <string name="theme_paper">Papír na černém</string> + <string name="theme_paper_detail">Hlavní je bílá, modrá z ikony značí linky, kroužky a body.</string> + <string name="theme_icon_blue">Modrá z ikony</string> + <string name="theme_icon_blue_detail">Černé pozadí, bílé písmo, modrá pro vše vybrané, zaměřené nebo stisknuté.</string> + <string name="theme_electric">Elektrická modř</string> + <string name="theme_electric_detail">Jasnější modrá na povrchách, které jsou spíše tmavomodré než šedé.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Space</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <string name="game_sync_err_runtime">Nejprve nainstalujte runtime</string> + <string name="game_sync_err_steam_library">Knihovna Steamu není dostupná</string> + <string name="game_sync_err_previous_sd">Předchozí knihovna na SD kartě není dostupná</string> + <string name="game_sync_err_game_storage">Úložiště her není dostupné</string> + <string name="game_sync_err_added_games">Složka přidaných her není dostupná</string> + <string name="game_sync_err_manifests">Manifesty Steamu nejsou dostupné</string> + <string name="content_system_many">Mnoho systémů</string> + <string name="fm_drive_internal">Interní</string> + <string name="fm_drive_linux">Systém Linuxu</string> + <string name="fm_drive_sd">SD karta</string> + <string name="fm_drive_storage">Úložiště</string> + <string name="fm_select">Vybrat</string> + <string name="fm_properties">Vlastnosti</string> + <string name="fm_favorite_add">Přidat do oblíbených</string> + <string name="fm_favorite_remove">Odebrat z oblíbených</string> + <string name="fm_rename">Přejmenovat</string> + <string name="fm_copy">Kopírovat</string> + <string name="fm_cut">Vyjmout</string> + <string name="fm_actions">Akce</string> + <string name="fm_favorites">Oblíbené</string> + <string name="fm_pin_current">Připnout aktuální složku</string> + <string name="fm_no_favorites">Zatím žádné oblíbené - připněte složku pomocí jejího menu ⋮ a rychle se sem vrátíte.</string> + <string name="fm_unpin_favorite">Odebrat z oblíbených</string> + <string name="fm_size_zero">0 bajtů</string> + <string name="fm_unit_bytes">bajtů</string> + <string name="fm_prop_location">Umístění</string> + <string name="fm_prop_type">Typ</string> + <string name="fm_prop_size">Velikost</string> + <string name="fm_prop_modified">Změněno</string> + <string name="fm_type_folder">Složka</string> + <string name="fm_type_file">Soubor</string> + <string name="fm_type_ext_file">Soubor %1$s</string> + <string name="fm_read_only">Jen pro čtení</string> + <string name="fm_read_only_hint">Zabrání hrám (a Wine) přepsat nebo smazat tento soubor.</string> + <string name="fm_read_only_failed">Nepodařilo se změnit Jen pro čtení</string> + <string name="fm_hidden">Skryté</string> + <string name="fm_hidden_unsupported">Toto úložiště neumí uložit příznak skrytí.</string> + <string name="fm_hidden_hint">Označí soubor jako skrytý ve Windows (atribut skrytí Wine).</string> + <string name="fm_hidden_failed">Nepodařilo se změnit Skryté na tomto úložišti</string> + <string name="fm_done">Hotovo</string> + <string name="fm_op_deleting">Mazání...</string> + <string name="fm_delete_failed">Smazání se nezdařilo</string> + <string name="fm_op_moving">Přesouvání...</string> + <string name="fm_op_copying">Kopírování...</string> + <string name="fm_op_moving_item">Přesouvání - %1$s</string> + <string name="fm_op_copying_item">Kopírování - %1$s</string> + <string name="fm_op_moving_batch">Přesouvání %1$d/%2$d - %3$s</string> + <string name="fm_op_copying_batch">Kopírování %1$d/%2$d - %3$s</string> + <string name="fm_paste_failed">%1$d hotovo, %2$d se nezdařilo</string> + <string name="fm_paste_skipped">%1$d hotovo, %2$d vynecháno</string> + <plurals name="fm_paste_moved"> + <item quantity="one">Přesunuta %1$d položka</item> + <item quantity="few">Přesunuty %1$d položky</item> + <item quantity="many">Přesunuto %1$d položek</item> + <item quantity="other">Přesunuto %1$d položek</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">Zkopírována %1$d položka</item> + <item quantity="few">Zkopírovány %1$d položky</item> + <item quantity="many">Zkopírováno %1$d položek</item> + <item quantity="other">Zkopírováno %1$d položek</item> + </plurals> + <string name="fm_already_exists">„%1$s“ už existuje</string> + <string name="fm_op_renaming">Přejmenovávání...</string> + <string name="fm_rename_failed">Přejmenování se nezdařilo</string> + <string name="fm_op_creating_folder">Vytváření složky...</string> + <string name="fm_create_folder_failed">Nepodařilo se vytvořit složku</string> + <string name="fm_new_folder">Nová složka</string> + <string name="fm_folder_name">Název složky</string> + <string name="fm_create">Vytvořit</string> + <string name="fm_new_name">Nový název</string> + <string name="fm_delete_title">Smazat?</string> + <string name="fm_delete_text">Trvale smazat „%1$s“?</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">Smazat %1$d položku?</item> + <item quantity="few">Smazat %1$d položky?</item> + <item quantity="many">Smazat %1$d položek?</item> + <item quantity="other">Smazat %1$d položek?</item> + </plurals> + <string name="fm_cannot_undo">Tuto akci nelze vzít zpět.</string> + <plurals name="fm_and_more"> + <item quantity="one">…a %1$d další</item> + <item quantity="few">…a %1$d další</item> + <item quantity="many">…a %1$d dalších</item> + <item quantity="other">…a %1$d dalších</item> + </plurals> + <string name="fm_op_deleting_batch">Mazání %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">Nepodařilo se smazat %1$d položku</item> + <item quantity="few">Nepodařilo se smazat %1$d položky</item> + <item quantity="many">Nepodařilo se smazat %1$d položek</item> + <item quantity="other">Nepodařilo se smazat %1$d položek</item> + </plurals> + <string name="fm_conflict_merge_hint">Sloučení přidá a nahradí soubory uvnitř stávající složky.</string> + <string name="fm_conflict_overwrite_hint">Přepsání nahradí existující soubor.</string> + <string name="fm_conflict_apply_all">Použít pro všechny konflikty</string> + <string name="fm_conflict_merge">Sloučit</string> + <string name="fm_conflict_overwrite">Přepsat</string> + <string name="fm_conflict_keep_both">Zachovat obojí</string> + <string name="fm_conflict_skip">Přeskočit</string> + <string name="fm_select_this_folder">Vybrat tuto složku</string> + <string name="fm_back">Zpět</string> + <string name="fm_drive_not_readable">%1$s je připojeno, ale momentálně není čitelné</string> + <string name="fm_list_view">Zobrazení seznamu</string> + <string name="fm_grid_view">Zobrazení mřížky</string> + <string name="fm_search">Hledat</string> + <string name="fm_sort">Seřadit</string> + <string name="fm_sort_name">Název</string> + <string name="fm_sort_date">Datum změny</string> + <string name="fm_rows_comfortable">Pohodlné řádky</string> + <string name="fm_rows_compact">Kompaktní řádky</string> + <string name="fm_hide_hidden">Skrýt skryté soubory</string> + <string name="fm_show_hidden">Zobrazit skryté soubory</string> + <string name="fm_hide_favorites">Skrýt oblíbené</string> + <string name="fm_show_favorites">Zobrazit oblíbené</string> + <string name="fm_filter_folder">Filtrovat tuto složku</string> + <string name="fm_free_space">Volné %1$s</string> + <string name="fm_selected_count">%1$d vybráno</string> + <string name="fm_select_none">Žádné</string> + <string name="fm_select_all">Vše</string> + <string name="fm_paste">Vložit</string> + <plurals name="fm_items"> + <item quantity="one">%1$d položka</item> + <item quantity="few">%1$d položky</item> + <item quantity="many">%1$d položek</item> + <item quantity="other">%1$d položek</item> + </plurals> + <string name="fm_paste_here">Vložit %1$s sem</string> + <string name="fm_paste_move_here">Vložit %1$s (přesunout) sem</string> + <string name="fm_rail_title">Soubory</string> + <string name="fm_rail_storage">ÚLOŽIŠTĚ</string> + <string name="fm_rail_quick">RYCHLÉ</string> + <string name="fm_rail_favorites">OBLÍBENÉ</string> + <string name="fm_rail_downloads">Stažené</string> + <string name="fm_rail_downloads_short">Stažené</string> + <string name="fm_rail_roms">ROMs</string> + <string name="fm_rail_session_logs">Logy relace</string> + <string name="fm_rail_pictures">Obrázky</string> + <string name="fm_favorite_added">„%1$s“ přidáno do oblíbených</string> + <string name="fm_favorite_removed">„%1$s“ odebráno z oblíbených</string> + <string name="fm_empty_directory">Prázdný adresář</string> + <string name="fm_up_one_level">O úroveň výš</string> + <string name="fm_choose_folder">Vybrat složku</string> + <string name="fm_choose_file">Vybrat soubor</string> + <string name="ctrl_tint_steam_blue">Steam modrá</string> + <string name="ctrl_tint_sky">Nebeská</string> + <string name="ctrl_tint_white">Bílá</string> + <string name="ctrl_tint_violet">Fialová</string> + <string name="ctrl_tint_green">Zelená</string> + <string name="ctrl_tint_amber">Ambrová</string> + <string name="ctrl_tint_red">Červená</string> + <string name="ctrl_tint_pink">Růžová</string> + <string name="ctrl_button_a">Tlačítko A</string> + <string name="ctrl_button_b">Tlačítko B</string> + <string name="ctrl_button_x">Tlačítko X</string> + <string name="ctrl_button_y">Tlačítko Y</string> + <string name="ctrl_button_lb">Levý bumper</string> + <string name="ctrl_button_rb">Pravý bumper</string> + <string name="ctrl_button_lt">Levý trigger</string> + <string name="ctrl_button_rt">Pravý trigger</string> + <string name="ctrl_button_view">Tlačítko View</string> + <string name="ctrl_button_menu">Tlačítko Menu</string> + <string name="ctrl_target_view">View</string> + <string name="ctrl_target_menu">Menu</string> + <string name="ctrl_target_up">D-pad nahoru</string> + <string name="ctrl_target_down">D-pad dolů</string> + <string name="ctrl_target_left">D-pad vlevo</string> + <string name="ctrl_target_right">D-pad vpravo</string> + <string name="ctrl_target_hidden">Skryté</string> + <string name="ctrl_editor_drag">Přetažením přesunete</string> + <string name="ctrl_editor_snap">Přichytit</string> + <string name="ctrl_editor_save">Uložit</string> + <string name="second_mode_none">Žádný</string> + <string name="second_mode_keyboard_trackpad">Klávesnice + trackpad</string> + <string name="second_mode_terminal">Terminál</string> + <string name="second_mode_deck_controls">Rukojeti + trackpady Deck</string> + + <string name="rtinst_removing">Odstraňování runtime Linuxu</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">Odstraňování runtime Linuxu · %1$d položka</item> + <item quantity="few">Odstraňování runtime Linuxu · %1$d položky</item> + <item quantity="many">Odstraňování runtime Linuxu · %1$d položek</item> + <item quantity="other">Odstraňování runtime Linuxu · %1$d položek</item> + </plurals> + <string name="rtinst_removed">Runtime Linuxu odstraněn</string> + <string name="rtinst_removal_failed">Nepodařilo se dokončit odstranění runtime Linuxu. Zkuste odstranění znovu v Přípravě.</string> + <string name="rtinst_removing_leftovers">Odstraňování zbytků runtime</string> + <string name="rtinst_downloading">Stahování</string> + <string name="rtinst_verifying">Ověřování</string> + <string name="rtinst_extracting">Rozbalování</string> + <string name="rtinst_channel_description">Zobrazuje se při instalaci nebo odstraňování runtime Linuxu</string> + <string name="rtinst_title_installing">Instalace runtime Linuxu</string> + <string name="rtinst_title_removing">Odstraňování runtime Linuxu</string> + + <string name="deskpkg_runtime_missing">Runtime Linuxu není nainstalován</string> + <string name="deskpkg_no_checksum">Katalog neobsahuje kontrolní součet pro %1$s</string> + <string name="deskpkg_checksum_mismatch">Neshoda kontrolního součtu - nic se nezměnilo</string> + <string name="deskpkg_installing">Instalování %1$s</string> + <string name="deskpkg_place_failed">Nepodařilo se umístit AppImage</string> + <string name="deskpkg_extract_failed">Rozbalení se nezdařilo</string> + <string name="deskpkg_install_failed">Instalace se nezdařila</string> + + <string name="flatpakmgr_setup_failed">Nastavení Flatpaku se nezdařilo (návratový kód %1$d)</string> + <string name="flatpakmgr_setup_unfinished">Nastavení Flatpaku se nedokončilo</string> + <string name="flatpakmgr_busy">Běží jiná úloha obchodu</string> + <string name="flatpakmgr_not_ready">Nejprve nastavte Flatpak</string> + <string name="flatpakmgr_starting">Spouštění</string> + <string name="flatpakmgr_exit_status">Flatpak skončil s návratovým kódem %1$d</string> + <string name="flatpakmgr_installing">Instalování %1$s</string> + <string name="flatpakmgr_updating">Aktualizování %1$s</string> + <string name="flatpakmgr_removing">Odstraňování %1$s</string> + <string name="flatpakmgr_installing_step">Instalování %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_updating_step">Aktualizování %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_removing_step">Odstraňování %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_ref_translations">překlady</string> + <string name="flatpakmgr_ref_graphics_drivers">grafické ovladače</string> + <string name="flatpakmgr_ref_media_codecs">multimediální kodeky</string> + <string name="flatpakmgr_ref_runtime">runtime %1$s</string> + <string name="flatpakmgr_ref_runtime_branch">runtime %1$s %2$s</string> + + <string name="appimg_too_small">Soubor je příliš malý na to, aby byl AppImage</string> + <string name="appimg_unreadable">Soubor nelze přečíst: %1$s</string> + <string name="appimg_not_linux">Toto není AppImage (nejde o program pro Linux)</string> + <string name="appimg_wrong_arch">Tento AppImage není sestaven ani pro ARM64 (aarch64), ani pro PC s x86</string> + <string name="appimg_not_appimage">Toto je program pro Linux, ale ne AppImage</string> + <string name="appimg_wrong_type">Importovat lze jen AppImage typu 2 (tento je typu %1$d)</string> + <string name="appimg_no_space_import">Nedostatek volného místa: import potřebuje přibližně %1$s</string> + <string name="appimg_no_space_update">Nedostatek volného místa: aktualizace potřebuje přibližně %1$s</string> + <string name="appimg_import_failed">Import se nezdařil</string> + <string name="appimg_gone">Tento AppImage už neexistuje</string> + <string name="appimg_update_failed">Aktualizace se nezdařila</string> + <string name="appimg_extracting">Rozbalování %1$s</string> + <string name="appimg_extract_failed">AppImage se nepodařilo rozbalit</string> + <string name="appimg_extract_failed_detail">AppImage se nepodařilo rozbalit: %1$s</string> + + <string name="deckymgr_stop_session_loader">Před instalací Decky Loader zastavte aktivní relaci</string> + <string name="deckymgr_download_failed">Stažení Decky Loader se nezdařilo</string> + <string name="deckymgr_size_mismatch">Velikost Decky Loader neodpovídala metadatům vydání</string> + <string name="deckymgr_checksum_download_failed">Nepodařilo se stáhnout kontrolní součet vydání</string> + <string name="deckymgr_checksum_missing">Chybí kontrolní součet vydání</string> + <string name="deckymgr_checksum_invalid">Kontrolní součet vydání je neplatný</string> + <string name="deckymgr_checksum_mismatch">Neshoda kontrolního součtu Decky Loader</string> + <string name="deckymgr_not_elf">Soubor vydání není kompatibilní 64bitový spustitelný soubor pro Linux</string> + <string name="deckymgr_session_started_loader">Během stahování se spustila relace; před instalací Decky Loader ji zastavte</string> + <string name="deckymgr_loader_not_executable">Nepodařilo se označit PluginLoader jako spustitelný</string> + <string name="deckymgr_cef_failed">Nepodařilo se povolit vzdálené ladění Steam CEF</string> + <string name="deckymgr_stage_failed">Nepodařilo se připravit PluginLoader</string> + <string name="deckymgr_loader_preserve_failed">Nepodařilo se zachovat stávající PluginLoader</string> + <string name="deckymgr_loader_activate_failed">Nepodařilo se aktivovat nový PluginLoader</string> + <string name="deckymgr_stop_session_plugin">Před instalací pluginu zastavte aktivní relaci</string> + <string name="deckymgr_loader_required">Před importem pluginu nainstalujte Decky Loader</string> + <string name="deckymgr_zip_unreadable">Nepodařilo se přečíst vybraný soubor ZIP</string> + <string name="deckymgr_plugins_dir_failed">Nepodařilo se vytvořit adresář pluginů Decky</string> + <string name="deckymgr_extracting_zip">Rozbalování ZIP s pluginem</string> + <string name="deckymgr_zip_one_folder">ZIP s pluginem musí obsahovat jednu složku pluginu na nejvyšší úrovni</string> + <string name="deckymgr_zip_missing_files">V ZIP s pluginem chybí plugin.json, package.json nebo dist/index.js</string> + <string name="deckymgr_plugin_name_missing">Chybí název pluginu</string> + <string name="deckymgr_session_started_plugin">Během instalace se spustila relace; před instalací pluginu ji zastavte</string> + <string name="deckymgr_plugin_installed">Plugin nainstalován · restartujte Steam, aby se načetl</string> + <string name="deckymgr_plugin_install_failed">Instalace ZIP s pluginem se nezdařila</string> + <string name="deckymgr_zip_too_many">ZIP s pluginem obsahuje příliš mnoho položek</string> + <string name="deckymgr_zip_invalid_path">ZIP s pluginem obsahuje neplatnou cestu</string> + <string name="deckymgr_zip_duplicate_paths">ZIP s pluginem obsahuje duplicitní cesty</string> + <string name="deckymgr_zip_unsafe_path">ZIP s pluginem obsahuje nebezpečnou cestu</string> + <string name="deckymgr_folder_failed">Nepodařilo se vytvořit složku pluginu</string> + <string name="deckymgr_zip_too_large">ZIP s pluginem po rozbalení přesáhne limit 1 GiB</string> + <string name="deckymgr_bundled_not_executable">Nepodařilo se označit přibalený binární soubor pluginu jako spustitelný</string> + <string name="deckymgr_remote_entry_invalid">Neplatná položka remote_binary</string> + <string name="deckymgr_remote_name_invalid">Neplatný název vzdáleného binárního souboru</string> + <string name="deckymgr_remote_url_https">URL vzdáleného binárního souboru musí používat HTTPS</string> + <string name="deckymgr_remote_hash_invalid">Vzdálený binární soubor %1$s má neplatný hash SHA-256</string> + <string name="deckymgr_bin_dir_failed">Nepodařilo se vytvořit adresář bin pluginu</string> + <string name="deckymgr_downloading_binary">Stahování binárního souboru pluginu · %1$s</string> + <string name="deckymgr_binary_download_failed">Nepodařilo se stáhnout binární soubor pluginu %1$s</string> + <string name="deckymgr_binary_checksum_mismatch">Neshoda kontrolního součtu binárního souboru pluginu · %1$s</string> + <string name="deckymgr_binary_not_executable">Nepodařilo se označit binární soubor pluginu jako spustitelný · %1$s</string> + <string name="deckymgr_binary_replace_failed">Nepodařilo se nahradit binární soubor pluginu · %1$s</string> + <string name="deckymgr_binary_install_failed">Nepodařilo se nainstalovat binární soubor pluginu · %1$s</string> + <string name="deckymgr_folder_taken">Složka pluginu patří jinému pluginu</string> + <string name="deckymgr_preserve_failed">Nepodařilo se zachovat stávající plugin</string> + <string name="deckymgr_activate_failed">Nepodařilo se aktivovat importovaný plugin</string> + <string name="deckymgr_settings_failed">Nepodařilo se aktualizovat nastavení pluginu Decky</string> + <string name="deckymgr_restore_failed">Nepodařilo se obnovit stávající plugin</string> + + <string name="adbfix_save_failed">Nepodařilo se uložit identitu bezdrátového ladění</string> + <string name="adbfix_command_failed">Příkaz ADB se nezdařil (%1$d)</string> + <string name="adbfix_not_confirmed">Android nepotvrdil změnu limitu procesů potomků: %1$s</string> + <string name="adbfix_no_pairing">Není uloženo žádné párování bezdrátového ladění</string> + <string name="adbfix_enter_address">Zadejte aktuální IP adresu a port bezdrátového ladění</string> + + <string name="adbpair_timed_out">Čekání na vyskakovací okno pro párování vypršelo. Zkuste to znovu z DroidDecku.</string> + <string name="adbpair_discovery_failed_code">Nepodařilo se vyhledat vyskakovací okno pro párování (%1$d). Zkontrolujte, že je zapnutá Wi-Fi.</string> + <string name="adbpair_discovery_failed">Nepodařilo se vyhledat vyskakovací okno pro párování: %1$s</string> + <string name="adbpair_six_digits">Zadejte všech 6 číslic kódu</string> + <string name="adbpair_pairing">Párování…</string> + <string name="adbpair_code_rejected">Tento kód nefungoval. Otevřete znovu vyskakovací okno pro párování a zadejte jeho nový kód.</string> + <string name="adbpair_connecting">Zpárováno. Připojování…</string> + <string name="adbpair_port_not_found">Zpárováno, ale port bezdrátového ladění nebyl nalezen. Ponechte bezdrátové ladění zapnuté a zkuste to znovu.</string> + <string name="adbpair_applying">Použití nastavení…</string> + <string name="adbpair_command_failed">Příkaz bezdrátového ladění se nezdařil</string> + <string name="adbpair_channel_name">Nastavení bezdrátového ladění</string> + <string name="adbpair_channel_description">Vyžádá kód pro párování bezdrátového ladění, dokud je otevřené Nastavení</string> + <string name="adbpair_title_pair">Zpárovat bezdrátové ladění</string> + <string name="adbpair_waiting_text">Zapněte bezdrátové ladění a pak klepněte na „Párování zařízení pomocí párovacího kódu“.</string> + <string name="adbpair_waiting_long">Zapněte bezdrátové ladění a pak klepněte na „Párování zařízení pomocí párovacího kódu“. Zůstaňte v Nastavení: kód se zobrazí v tomto oznámení.</string> + <string name="adbpair_code_label">6místný párovací kód</string> + <string name="adbpair_code_hint">Klepněte na Zadat kód a napište 6 číslic zobrazených v Nastavení.</string> + <string name="adbpair_title_code">Zadejte párovací kód Wi-Fi</string> + <string name="adbpair_title_retry">Zkusit kód znovu</string> + <string name="adbpair_enter_code">Zadat kód</string> + <string name="adbpair_title_working">Nastavování Steamu</string> + <string name="adbpair_title_done">Steam je připraven</string> + <string name="adbpair_done_text">Klepnutím se vrátíte do DroidDecku. Bezdrátové ladění teď můžete vypnout.</string> + <string name="adbpair_title_failed">Nastavení bezdrátového ladění bylo zastaveno</string> + + <string name="selfinst_http">Stahování odpovědělo HTTP %1$d</string> + <string name="selfinst_size_mismatch">Velikost stahování neodpovídala zveřejněnému APK a bylo zahozeno - zkuste to znovu</string> + <string name="selfinst_checksum_mismatch">Stahování neodpovídalo svému kontrolnímu součtu a bylo zahozeno - zkuste to znovu</string> + <string name="selfinst_wrong_package">Aktualizace je pro balíček Androidu %1$s, ne %2$s</string> + <string name="selfinst_metadata_signer">Metadata aktualizace neuvádějí podpisový klíč vydání DroidDecku</string> + <string name="selfinst_unreadable">Android nemohl přečíst stažené APK</string> + <string name="selfinst_apk_wrong_package">Stažené APK je pro %1$s, ne %2$s</string> + <string name="selfinst_version_mismatch">Stažené APK uvádí versionCode %1$d, ne zveřejněný %2$d</string> + <string name="selfinst_downgrade">Android nenainstaluje versionCode %1$d přes nainstalovaný %2$d</string> + <string name="selfinst_no_signing">Stažené APK nemá žádné informace o podpisu</string> + <string name="selfinst_wrong_signer">Stažené APK není podepsáno vydávacím klíčem DroidDecku</string> + <string name="appupd_http">Katalog aktualizací odpověděl HTTP %1$d</string> + + <string name="loadstate_starting">Spouštění relace…</string> + <string name="loadstate_elapsed">%1$d:%2$02d uplynulo · stále pracuje</string> + <string name="loadstate_steam_update">Stahování aktualizace klienta Steam · %1$d%%</string> + <string name="loadstate_steam_client">Stahování klienta Steam · %1$s</string> + + <string name="pextra_busy">Instalace nástroje kompatibility už běží</string> + <string name="pextra_stop_session">Před instalací nástrojů kompatibility zastavte aktivní relaci</string> + <string name="pextra_no_release">Nepodařilo se najít vydání ARM64 pro %1$s</string> + <string name="pextra_no_space">Pro instalaci %1$s je potřeba alespoň 4 GB volného místa navíc k velikosti stahování</string> + <string name="pextra_downloading">Stahování %1$s %2$s</string> + <string name="pextra_retrying">Opakování stahování · pokus %1$d z %2$d</string> + <string name="pextra_interrupted">Stahování přerušeno</string> + <string name="pextra_download_failed">Stahování se nezdařilo; částečný soubor je zachován, aby mohl další pokus navázat</string> + <string name="pextra_too_big">Stahování překročilo velikost vydání a bylo odstraněno</string> + <string name="pextra_incomplete">Stahování nebylo dokončeno; částečný soubor je zachován pro navazující opakování</string> + <string name="pextra_verifying">Ověřování stahování</string> + <string name="pextra_checksum_unreadable">Nepodařilo se přečíst kontrolní součet vydání; zkuste to znovu</string> + <string name="pextra_no_checksum">%1$s %2$s nezveřejňuje kontrolní součet; nic se nenainstalovalo</string> + <string name="pextra_checksum_mismatch">Neshoda kontrolního součtu; stahování bylo odstraněno</string> + <string name="pextra_session_started">Během stahování se spustila relace; před instalací nástrojů kompatibility ji zastavte</string> + <string name="pextra_installing">Instalování %1$s</string> + <string name="pextra_install_failed">Instalace %1$s se nezdařila (návratový kód %2$d)</string> + <string name="pextra_could_not_install">Nepodařilo se nainstalovat %1$s</string> + <string name="esync_fetching">Stahování balíčku droiddeck-esync</string> + + <string name="cmgr_fex_linux_helpers">s pomocnými nástroji pro Linux</string> + <string name="cmgr_fex_dlls_only">Jen DLL</string> + <string name="cmgr_original_proton_updated">Původní (Proton byl od posledního přepnutí aktualizován)</string> + <string name="cmgr_changed_outside">Změněno mimo Komponenty</string> + <string name="cmgr_not_present">není přítomno</string> + <string name="cmgr_original_version">Původní %1$s</string> + <string name="cmgr_original_from">Původní z %1$s</string> + <string name="cmgr_swap_queued">%1$s %2$s se přenese do %3$s, jakmile se zavře hra, která na něm běží.</string> + <string name="cmgr_swap_done">%1$s %2$s je teď v %3$s. Použije se při dalším spuštění hry.</string> + <string name="cmgr_original_missing">Tento původní balíček není uložen</string> + <string name="cmgr_restore_queued">%1$s z %2$s se vrátí na původní verzi, jakmile se zavře hra, která na něm běží.</string> + <string name="cmgr_restore_done">%1$s z %2$s bylo obnoveno z jeho původní verze %3$s.</string> + <string name="cmgr_package_in_use">%1$s se používá v %2$s. Nejprve ten Proton přepněte na něco jiného.</string> + <string name="cmgr_package_not_stored">%1$s není uloženo</string> + <string name="cmgr_deleted">Odstraněno %1$s.</string> + <string name="cmgr_original_kept">Původní verze nainstalovaného sestavení je zachována: je to cesta zpět.</string> + <string name="cmgr_not_stored">Není uloženo</string> + <string name="cmgr_deleted_original">Odstraněna původní verze %2$s z %1$s.</string> + <string name="cmgr_nightlies_only">Stahování pochází jen z vydání Nightlies</string> + <string name="cmgr_list_predates_checksums">Tento seznam balíčků předchází kontrolním součtům - obnovte ho a zkuste to znovu</string> + <string name="cmgr_checksum_mismatch">Neshoda kontrolního součtu</string> + + <string name="sprefs_upscaler_linear">Lineární</string> + <string name="sprefs_upscaler_nearest">Nejbližší</string> + <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (kvalita)</string> + <string name="sprefs_upscaler_sharpen">Jen zaostření</string> + <string name="sprefs_lod_bias_auto">Automaticky (podle škálování)</string> + + <string name="look_off_desc">Nic není použito.</string> + <string name="look_game_clarity">Herní jasnost</string> + <string name="look_game_clarity_desc">Zaostření s trochou kontrastu - ten každodenní.</string> + <string name="look_vivid">Sytý</string> + <string name="look_vivid_desc">Barvy vyhnané na maximum. Deband zapnutý, aby nebe nepruhovalo.</string> + <string name="look_cinematic">Kinematografický</string> + <string name="look_cinematic_desc">Hlubší kontrast, chladnější barvy, vyhlazené hrany.</string> + <string name="look_competitive">Soutěžní</string> + <string name="look_competitive_desc">Zvednuté stíny a silné zaostření - všimněte si lidí první.</string> + <string name="look_adaptive_sharpen">Adaptivní zaostření</string> + <string name="look_adaptive_sharpen_desc">Jen zaostření, barvy nedotčené.</string> + <string name="look_filmic">Filmový</string> + <string name="look_filmic_desc">Křivka připomínající film, lehce ztlumená.</string> + <string name="look_arcade">Arkáda</string> + <string name="look_arcade_desc">Křiklavé a výrazné.</string> + <string name="look_retro_crt">Retro CRT</string> + <string name="look_retro_crt_desc">Efekt CRT trubice a analogového signálu zároveň.</string> + <string name="look_upscale_sharp">Ostré zvětšení</string> + <string name="look_upscale_sharp_desc">Pro běh v rozlišení nižším než má displej.</string> + <string name="look_pixel_clean">Čisté pixely</string> + <string name="look_pixel_clean_desc">Ostré pixely pro 2D a staré hry.</string> + <string name="look_anime_edge">Anime hrany</string> + <string name="look_anime_edge_desc">Kreslené obrysy plus zaostření.</string> + + <string name="gpuinfo_not_adreno">Není GPU Adreno</string> + <string name="gpuinfo_supported">Podporováno</string> + <string name="gpuinfo_experimental">Experimentální: jeho ovladače jsou testovací sestavení</string> + <string name="gpuinfo_below_tested">Pod testovaným hardwarem (Adreno 725 a novější): nemusí fungovat</string> + <string name="gpuinfo_unsupported">Není podporováno: DroidDeck potřebuje GPU Adreno (Snapdragon)</string> + + <string name="turnip_rate_limited">Byl dosažen limit požadavků GitHubu - zkuste to později</string> + <string name="turnip_github_http">GitHub odpověděl HTTP %1$d</string> + <string name="turnip_download_http">stahování odpovědělo HTTP %1$d</string> + <string name="turnip_list_predates_checksums">Tento seznam ovladačů předchází kontrolním součtům - obnovte ho a zkuste to znovu</string> + <string name="turnip_checksum_mismatch">Neshoda kontrolního součtu - stahování bylo zahozeno</string> + <string name="turnip_not_adrenotools">Není ovladač AdrenoTools: %1$s</string> + <string name="turnip_reject_no_meta">chybí meta.json</string> + <string name="turnip_reject_meta_unreadable">meta.json nelze přečíst (%1$s)</string> + <string name="turnip_reject_linux_driver">toto je ovladač runtime Linuxu (importujte ho v „Ovladač runtime Linuxu“)</string> + <string name="turnip_reject_wayland">toto je herní ovladač pro Wayland, který tato aplikace nepotřebuje</string> + <string name="turnip_reject_no_library_name">meta.json neuvádí libraryName, takže nic nemohlo být předáno AdrenoTools</string> + <string name="turnip_reject_library_missing">meta.json uvádí %1$s, který není v zipu</string> + <string name="turnip_reject_not_aarch64">%1$s není 64bitová sdílená knihovna ELF pro AArch64</string> + <string name="turnip_reject_glibc">%1$s odkazuje na glibc - jde o Turnip „-Linux“, který patří do „Ovladač runtime Linuxu“</string> + <string name="lvd_no_library">V tomto zipu není žádný libvulkan_freedreno*.so. ZIP Android (AdrenoTools) nebo -Wayland Turnip není ovladač runtime Linuxu.</string> + <string name="lvd_not_aarch64">%1$s není 64bitová sdílená knihovna ELF pro AArch64.</string> + <string name="lvd_not_glibc">%1$s není ovladač glibc - odkazuje na libc Androidu. Runtime Linuxu potřebuje zip „-Linux“; běžný nebo „-Wayland“ Turnip nemůže klient Steam vůbec načíst.</string> + <string name="bundle_no_manifest">Není balíček ovladačů: chybí manifest.json s názvy ovladače pro Android a Linux</string> + <string name="bundle_entry_outside">Není balíček ovladačů: %1$s odkazuje mimo něj</string> + <string name="bundle_too_large">Není balíček ovladačů: po rozbalení přesáhne %1$d MB</string> + + <string name="gstore_sd_card">SD karta</string> + <string name="gstore_folder">Složka</string> + + <string name="logshare_chooser">Sdílet logy relace</string> + + <string name="hdr_needs_android_10">vyžaduje Android 10 nebo novější (vrstvy displeje s barevným prostorem)</string> + <string name="hdr_none">tento displej neuvádí podporu HDR</string> + <string name="hdr_not_hdr10">tento displej podporuje %1$s, ne HDR10</string> + + <string name="gpu_this_gpu">toto GPU</string> + <string name="phantom_title_on">Limit procesů potomků je zapnutý</string> + <string name="phantom_title_unset">Limit procesů potomků není nastavený</string> + <string name="phantom_title_unreadable">Limit procesů potomků se nepodařilo zkontrolovat</string> + <string name="phantom_title_off">Limit procesů potomků je vypnutý</string> + <string name="phantom_title_not_required">Limit procesů potomků není vyžadován</string> + <string name="phantom_instructions_fix">%1$s Pokud to není možné, připojte toto zařízení k počítači s ADB a spusťte příkaz níže. Vraťte se sem a zkontrolujte to znovu.</string> + <string name="phantom_instructions_off">Relace Steamu se mohou spouštět.</string> + <string name="phantom_instructions_not_applicable">Tato verze Androidu tento limit nepoužívá.</string> + <string name="phantom_fix_toggle">Zapněte „Zakázat omezení procesů potomků“ v Možnostech pro vývojáře a pak je nechte zapnuté.</string> + <string name="phantom_fix_no_toggle">Tato verze Androidu pro to nemá přepínač. Použijte bezdrátové ladění a změňte to z tohoto zařízení.</string> + <string name="upd_block_no_apk">Toto sestavení nemá stažení pro tuto kopii DroidDecku.</string> + <string name="upd_block_version_code">Android to nenainstaluje přes toto sestavení, protože jeho versionCode %1$d je nižší než nainstalovaný %2$d.</string> + <string name="upd_fail_downgrade">Android nenainstaluje starší verzi přes novější. Zůstaňte na tomto sestavení, dokud kanál nedohoní.</string> + <string name="upd_fail_signature">Tato kopie DroidDecku je podepsána jinak (lokální sestavení?), takže ji Android nemůže aktualizovat na místě.</string> + <string name="upd_fail_storage">Na aktualizaci není dostatek volného místa.</string> + <string name="upd_fail_aborted">Aktualizace byla zrušena.</string> + <string name="upd_fail_with_message">Aktualizace se nenainstalovala: %1$s</string> + <string name="upd_fail_generic">Aktualizace se nenainstalovala.</string> + <string name="saves_layout_zip">%1$s zip</string> + <string name="saves_zip">zip</string> + <string name="saves_not_launched">%1$s ještě nebylo spuštěno</string> + <string name="saves_none_found">Pro %1$s nebyly nalezeny žádné soubory uložených pozic</string> + <string name="saves_not_save_zip">%1$s neobsahuje složku drive_c, takže to není zip s uloženou pozicí z GameHub nebo Winlator</string> + <string name="gstore_cannot_create">nelze vytvářet složky v %1$s</string> + <string name="gstore_cannot_write">nelze zapisovat do %1$s</string> + <string name="gstore_cannot_write_detail">nelze zapisovat do %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">Složka není dostupná</string> + <string name="gfiles_name_taken">Jiný soubor už používá %1$s</string> + <string name="gfiles_art_unavailable">Složka s obrázky není dostupná</string> + <string name="gfiles_unrelated_inventory">Složka obsahuje nesouvisející evidenci synchronizace</string> + <string name="gfiles_remove_failed">Nepodařilo se odstranit %1$s</string> + <string name="gfiles_outside_folder">Soubor opouští exportní složku</string> + <string name="cmgr_fex_unnamed">FEX (sestavení bez názvu verze)</string> + <string name="cmgr_no_profile">chybí profile.json v %1$s</string> + <string name="cmgr_not_component_package">%1$s není balíček komponenty pro Linux (typ %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s volných</string> + <string name="mode_storage_auto_short">Automaticky</string> + <string name="appupd_catalog_rejected">Katalog aktualizací neprošel kontrolou (%1$s)</string> + <string name="main_adb_clip_label">Nastavení procesů potomků DroidDecku</string> + <string name="setup_signed_in_offline_unnamed">Přihlášeno · offline režim</string> + <string name="setup_signed_in_unnamed">Přihlášeno</string> + <string name="driver_tag_bundled">PŘIBALENÝ</string> + <string name="driver_tag_downloaded">STAŽENÝ</string> + <string name="driver_tag_imported">IMPORTOVANÝ</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Obrazovka relace</string> + <string name="display_resolution">Rozlišení</string> + <string name="display_game_fit">Přizpůsobení hry 16:9</string> + <string name="display_game_fit_hint">Jak se hry s poměrem 16:9 přizpůsobí této obrazovce. Příští relace.</string> + <string name="display_game_fit_unavailable">Vyberte Podle obrazovky nebo vlastní velikost užší než 16:9.</string> + <string name="display_preserve">Zachovat proporce</string> + <string name="display_stretch">Roztáhnout na obrazovku</string> + <string name="display_image_scaling">Škálování obrazu</string> + <string name="display_filter">Filtr škálování</string> + <string name="display_filter_hint">Sdíleno Steamem a Plochou.</string> + <string name="display_filter_live">Použije se okamžitě. Sdíleno Steamem a Plochou.</string> + <string name="display_filter_note">FSR, GSR a NIS se použijí při zvětšování obrazu relace. Jen zaostření funguje i bez zvětšení.</string> + <string name="display_sharpness">Ostrost filtru</string> + <string name="display_frame_rate">Snímková frekvence</string> + <string name="display_look_filter">%1$s Nastaví škálování na %2$s.</string> + <string name="display_match_screen">Podle obrazovky (%1$d×%2$d)</string> + <string name="display_follow_screen">Sledovat obrazovku (změní velikost při složení nebo rozložení)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Zobrazit podrobnosti o zařízení</string> + <string name="device_info_hide">Skrýt podrobnosti o zařízení</string> + <string name="device_info_loading">Čtení zařízení…</string> + <string name="device_info_copy">Kopírovat</string> + <string name="device_info_copy_hint">Vlož tyto údaje do hlášení o chybě.</string> + <string name="device_info_copied">Zkopírováno.</string> + <string name="force_ssbs_title">Vynutit SSBS</string> + <string name="force_ssbs_hint">Udržuje zapnuté nastavení rychlosti CPU, které Wine stále vypíná. Může zrychlit Windows hry na Snapdragonu 8 Elite; na starších čipech je rozdíl malý. Použije se při příštím spuštění hry.</string> + + <string name="rail_stores">Obchody</string> + <string name="source_steam">Steam</string> + <string name="source_added">Vlastní</string> + <string name="setup_stores">Obchody</string> + <string name="setup_stores_open_on">Otevřít obchod na</string> + <string name="setup_stores_speed">Úroveň rychlosti stahování</string> + <string name="setup_stores_engine">Jádro obchodů</string> + <string name="setup_stores_engine_chip_ready">● Jádro je připraveno</string> + <string name="setup_stores_engine_chip_missing">Není sestaveno</string> + <string name="stores_tier_balanced">Vyvážené</string> + <string name="stores_tier_fast">Rychlé</string> + <string name="stores_tier_max">Maximální</string> + <string name="stores_title">Obchody</string> + <string name="stores_settings">Nastavení obchodů</string> + <string name="stores_settings_eyebrow">Nastavení</string> + <string name="stores_chip_signed_out">(nepřihlášen)</string> + <string name="stores_tab_store">Obchod</string> + <string name="stores_tab_installed">Nainstalované</string> + <string name="stores_tab_library">Knihovna</string> + <string name="stores_tab_all">Vše</string> + <string name="stores_tab_downloads">Stahování</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Přihlásit se</string> + <string name="stores_signout_named">%1$s · odhlásit</string> + <string name="stores_signout_title">Odhlásit se z %1$s?</string> + <string name="stores_signout_note">Nainstalované hry zůstanou. Knihovna se znovu stáhne po opětovném přihlášení.</string> + <string name="stores_signout_stay">Zůstat přihlášen</string> + <string name="stores_signout_go">Odhlásit se</string> + <string name="stores_search_catalog">Hledat v katalogu %1$s…</string> + <string name="stores_search_installed">Hledat v nainstalovaných hrách…</string> + <string name="stores_search_library">Hledat ve vaší knihovně…</string> + <string name="stores_search_open">Hledat</string> + <string name="stores_installed_empty">Nic není nainstalováno</string> + <string name="stores_library_loading">Načítání</string> + <string name="stores_nothing_matches">Žádné hry</string> + <string name="stores_shelves_loading">Načítání obchodu…</string> + <string name="stores_amazon_no_catalog">Žádný katalog</string> + <string name="stores_shelf_new">Novinky</string> + <string name="stores_shelf_deals">Nabídky</string> + <string name="stores_shelf_free">Hry zdarma</string> + <string name="stores_shelf_trending">Populární</string> + <string name="stores_shelf_library">Vaše knihovna</string> + <string name="stores_play">Hrát</string> + <string name="stores_install">Instalovat</string> + <string name="stores_resume_install">Pokračovat v instalaci</string> + <string name="stores_dl_interrupted">Přerušeno</string> + <string name="stores_install_size">Instalace · %1$s</string> + <string name="stores_get_free">Získat zdarma</string> + <string name="stores_view_on">Zobrazit na %1$s</string> + <string name="stores_buy_price">Koupit · %1$s</string> + <string name="stores_installed_chip">Nainstalováno</string> + <string name="stores_in_library">V knihovně</string> + <string name="stores_free">Zdarma</string> + <string name="stores_resume">Pokračovat</string> + <string name="stores_refresh">Obnovit</string> + <string name="stores_show_mature">Zobrazovat obsah pro dospělé</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Offline</string> + <string name="epic_card_none">Vypnuto</string> + <string name="epic_eos">Přihlášení do Epic (EOS)</string> + <string name="epic_offline">Spustit offline</string> + <string name="epic_resolve">Vyřešit přihlášení do Epic</string> + <string name="store_cloud_saves">Cloudové uložené pozice</string> + <string name="cloud_upload">Odeslat do cloudu</string> + <string name="cloud_download">Stáhnout z cloudu</string> + <string name="cloud_none">Žádné uložené pozice v cloudu</string> + <string name="cloud_conflict">Konflikt v cloudu</string> + <string name="cloud_conflict_files">Konflikt v cloudu · %1$d souborů</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Zachovat cloud</string> + <string name="cloud_keep_local">Zachovat lokální</string> + <string name="cloud_checking">Kontrola cloudu…</string> + <string name="cloud_last_sync">Poslední synchronizace %1$s</string> + <string name="cloud_never">Ještě nesynchronizováno</string> + <string name="cloud_done">%1$d souborů</string> + <string name="stores_eyebrow_installed">nainstalováno</string> + <string name="stores_eyebrow_library">ve vaší knihovně</string> + <string name="stores_eyebrow_store">obchod</string> + <string name="achievements_title">Achievementy</string> + <string name="achievements_count">%1$d z %2$d odemčeno</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Odemčeno · %1$d</string> + <string name="achievements_group_locked">Zamčeno · %1$d</string> + <string name="achievements_group_hidden">Skryto · %1$d</string> + <string name="achievements_hidden">Skrytý achievement</string> + <string name="achievements_hidden_detail">Podrobnosti se zobrazí po odemčení.</string> + <string name="achievements_all">Všechny achievementy odemčeny</string> + <string name="achievements_reading">Načítání…</string> + <string name="achievements_signed_out">Přihlaste se k %1$s a zobrazte si své achievementy.</string> + <string name="achievements_unreachable">Nepodařilo se spojit s %1$s. Zkontrolujte to znovu, až budete online.</string> + <string name="achievements_none">%1$s neuvádí pro tuto hru žádné achievementy.</string> + <string name="achievements_show_all">Zobrazit vše (%1$d)</string> + <string name="achievements_dialog_title">Achievementy · %1$s</string> + <string name="stores_in_steam">✓ Ve Steamu</string> + <string name="stores_uninstall">Odinstalovat</string> + <string name="stores_uninstalling">Odinstalování…</string> + <string name="stores_uninstall_title">Odinstalovat %1$s?</string> + <string name="stores_uninstall_body_size">Uvolní %1$s a odstraní ji ze Steamu.</string> + <string name="stores_uninstall_body">Odstraní její soubory a zástupce ve Steamu.</string> + <string name="stores_launch_not_registered">Steam hru zachytí při dalším spuštění; spustit ji odtamtud, nebo otevřít Steam hned.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Volba Steamu pro každou hru</string> + <string name="stores_card_audio">Zvuk</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Komponenty</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Jádro</string> + <string name="stores_card_engine_value">%1$s · nativní</string> + <string name="stores_card_engine_java">%1$s · vestavěné</string> + <string name="stores_how_it_runs">Jak se spouští</string> + <string name="stores_kv_fetched">Staženo z</string> + <string name="stores_kv_fetched_value">%1$s, s vaším účtem</string> + <string name="stores_kv_installed_to">Nainstalováno do</string> + <string name="stores_kv_installed_to_value">Úložiště her (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Spouští</string> + <string name="stores_kv_launched_by_value">Steam a Proton, stejnou cestou jako hry ze Steamu</string> + <string name="stores_kv_launched_by_epic">Steam a Proton · přihlášení do Epic při každém spuštění, překryvná vrstva EOS vypnuta</string> + <string name="stores_kv_launched_by_amazon">Steam a Proton · nastaveny proměnné Amazon Games Services</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">přidáno do Steamu při instalaci (Deck UI + Steam Input)</string> + <string name="stores_downloads_empty">Žádná stahování</string> + <string name="stores_dl_paused">Pozastaveno</string> + <string name="stores_dl_queued">V čekání</string> + <string name="stores_dl_queued_at">V čekání · #%1$d</string> + <string name="stores_dl_cancelled">Zrušeno</string> + <string name="stores_dl_failed">Nezdařilo se</string> + <string name="stores_dl_failed_reason">Nezdařilo se · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Pozastavit</string> + <string name="stores_dl_resume">Pokračovat</string> + <string name="stores_dl_cancel">Zrušit</string> + <string name="stores_dl_retry">Zkusit znovu</string> + <string name="stores_dl_clear">Vymazat</string> + <string name="stores_dl_clear_all">Vymazat dokončené</string> + <string name="stores_stage_manifest">Manifest</string> + <string name="stores_stage_download">Stažení</string> + <string name="stores_stage_verify">Ověření</string> + <string name="stores_stage_install">Instalace</string> + <string name="stores_stage_done">Hotovo</string> + <string name="stores_stage_checking">Kontrola</string> + <string name="stores_stage_downloading">Stahování</string> + <string name="stores_dl_cancel_confirm">Odstranit stažení?</string> + <string name="stores_dl_failed_kept">Nezdařilo se · %1$s zachováno</string> + <string name="stores_stage_verifying">Ověřování</string> + <string name="stores_stage_installing">Instalování</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d souborů</string> + <string name="stores_download_manager">Správce stahování</string> + <string name="stores_parallel">Souběžných stahování</string> + <string name="stores_install_where">Instalovat do</string> + <string name="stores_install_title">Instalace %1$s:</string> + <string name="stores_target_internal">Interní úložiště</string> + <string name="stores_target_free">%1$s volných</string> + <string name="stores_notification_channel">Stahování z obchodů</string> + <string name="stores_notification_channel_hint">Zobrazuje průběh stahování her a udržuje stahování aktivní na pozadí</string> + <string name="stores_notification_one">Stahování</string> + <string name="stores_notification_many">%1$d stahování</string> + <string name="stores_notification_waiting">Příprava…</string> + <string name="stores_notification_done_channel">Dokončená stahování z obchodů</string> + <string name="stores_notification_installed">%1$s nainstalována</string> + <string name="stores_notification_failed">Stažení %1$s se nezdařilo</string> + <string name="stores_notification_delete">Odstranit</string> + <string name="stores_notification_keep">Zachovat</string> + <string name="stores_login_title">Přihlásit se k %1$s</string> + <string name="stores_login_provider_title">Přihlásit se přes poskytovatele</string> + <string name="stores_login_reload">Znovu načíst</string> + <string name="stores_login_close">Zavřít</string> + <string name="stores_login_retry">Zkusit znovu</string> + <string name="stores_login_finishing">Dokončování přihlášení…</string> + <string name="stores_login_error_network">Přihlašovací stránku se nepodařilo načíst: %1$s</string> + <string name="stores_login_error_idp_blocked">Tento poskytovatel odmítá přihlášení ze vložené stránky. Přihlaste se místo toho heslem k obchodu.</string> + <string name="stores_login_error_http">Přihlašovací stránka odpověděla chybou (HTTP %1$d).</string> + <string name="stores_login_error_ssl">Spojení s přihlašovací stránkou není zabezpečené.</string> + <string name="stores_login_error_verification">Přihlášení se nepodařilo ověřit. Zkuste to znovu.</string> + <string name="stores_login_error_generic">Přihlášení se nezdařilo. Zkuste to znovu.</string> + <string name="common_close">Zavřít</string> + <string name="common_back">Zpět</string> + <string name="common_edit">Upravit</string> + <string name="games_add_all">Přidat všechny hry z této složky</string> + <string name="games_added_none">V této složce nic nového</string> + <string name="edit_name">Název</string> + <string name="edit_target">Cíl</string> + <string name="edit_start_in">Spustit v</string> + <string name="edit_launch_options">Možnosti spuštění</string> + <string name="edit_artwork">Grafika</string> + <string name="edit_other_exe">Jiný exe…</string> + <string name="edit_remove">Odstranit</string> + <string name="art_cover">Obálka</string> + <string name="art_background">Pozadí</string> + <string name="art_logo">Logo</string> + <string name="art_icon">Ikona</string> + <string name="art_src_folder">Složka hry</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">Váš soubor</string> + <string name="art_src_exe">Z exe souboru</string> + <string name="art_src_auto">Automaticky</string> + <string name="art_pick_image">Vybrat obrázek…</string> + <string name="art_pick_title">Vyberte obrázek: %1$s</string> + <string name="art_reset">Obnovit na automatické</string> + <plurals name="games_added_n"> + <item quantity="one">%1$d hra přidána</item> + <item quantity="few">%1$d hry přidány</item> + <item quantity="many">%1$d her přidáno</item> + <item quantity="other">%1$d her přidáno</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="one">%1$d už je v Hrách</item> + <item quantity="few">%1$d už jsou v Hrách</item> + <item quantity="many">%1$d už je v Hrách</item> + <item quantity="other">%1$d už je v Hrách</item> + </plurals> + <string name="hop_back">B · Zpět na %1$s</string> + <string name="hop_from_games">hru</string> + <string name="hop_from_play">Nastavení spuštění</string> + <string name="sheet_graphics_drivers">Grafické ovladače</string> + <string name="comp_this_proton">Tento Proton</string> + <string name="comp_every_proton">Každý Proton</string> + <string name="comp_for_every_proton">Pro každý Proton</string> + <string name="comp_available_count">K dispozici · %1$d</string> + <string name="comp_checked">Zkontrolováno %1$s</string> + <string name="comp_check_all">Zkontrolovat nové komponenty a ovladače</string> + <string name="comp_get">Získat</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton a FEX níže patří tomuto Protonu</string> + <string name="comp_device_tested">%1$s · testováno</string> + <string name="comp_device_untested">%1$s · netestováno</string> + <string name="comp_device_unsupported">%1$s · nepodporováno</string> + <string name="gpu_auto_keeps">Udržuje doporučenou dvojici aktuální</string> + <string name="gpu_bar_auto">Automaticky · %1$s</string> + <string name="gpu_separately">Ovladače runtime a displeje samostatně</string> + <string name="gpu_use_title">Použít %1$s?</string> + <string name="proton_remove_title">Odstranit %1$s?</string> + <string name="setup_tab_diagnostics">Diagnostika</string> + <string name="setup_ready">Připraveno ke hraní</string> + <string name="setup_not_ready">Ještě není připraveno ke hraní</string> + <string name="setup_ready_runtime">runtime %1$s</string> + <string name="setup_ready_limit_off">omezení procesů vypnuto</string> + <string name="setup_ready_signed_in">přihlášen jako %1$s</string> + <string name="setup_needs_you">Když je potřeba váš zásah</string> + <string name="setup_group_look">Vzhled</string> + <string name="setup_group_device">Toto zařízení</string> + <string name="setup_group_art">Grafika knihovny</string> + <string name="flathub_eyebrow">LINUXOVÉ APLIKACE</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Desktopové aplikace pro Linuxový desktop, otevírané z Desktopu.</string> + <string name="flathub_setup">Nastavit Flathub</string> + <string name="flathub_remove_title">Odstranit %1$s?</string> + <string name="flathub_remove_note">Nastavení a soubory zůstanou zachovány.</string> + <string name="flathub_keep">Zachovat</string> + <string name="desktop_size">Velikost na disku</string> + <string name="desktop_uninstall_title">Odinstalovat desktop?</string> + <string name="desktop_all_systems">Vše</string> + <string name="gog_achievements">Achievementy GOG</string> + <string name="gog_achievements_hint">Synchronizuje achievementy u podporovaných her GOG. Použije se při dalším spuštění.</string> + <string name="gog_achievements_off">Achievementy vypnuty</string> + <string name="gog_achievements_on">Achievementy zapnuty</string> + <string name="gog_achievements_signin">Přihlásit se ke GOG znovu</string> + <string name="gog_achievements_uploaded">Achievementy nahrány</string> + <string name="gog_achievements_pending">Nahrání achievementu čeká</string> + <string name="gog_achievements_enable_store">Zapněte achievementy GOG v nastavení obchodů</string> + <string name="gog_achievements_unavailable">Achievementy nejsou k dispozici; zkontrolujte protokoly relace</string> + <string name="achievement_unlocked">Achievement odemčen</string> + <string name="touch_paste">Vložit</string> + <string name="touch_dpad">Křížový ovladač</string> + <string name="touch_left_stick">Levá páčka</string> + <string name="touch_right_stick">Pravá páčka</string> + <string name="touch_menu">Nabídka</string> + <string name="touch_keyboard">Klávesnice</string> + <string name="touch_dpad_up">Křížový ovladač nahoru</string> + <string name="touch_dpad_down">Křížový ovladač dolů</string> + <string name="touch_dpad_left">Křížový ovladač vlevo</string> + <string name="touch_dpad_right">Křížový ovladač vpravo</string> + <string name="touch_title">Dotykové ovládání</string> + <string name="touch_controller">Dotykový ovladač</string> + <string name="touch_mouse">Myš</string> + <string name="touch_on">Zapnuto</string> + <string name="touch_off">Vypnuto</string> + <string name="touch_speed_slow">Pomalá</string> + <string name="touch_speed_normal">Normální</string> + <string name="touch_speed_fast">Rychlá</string> + <string name="touch_edit_layout">Upravit rozložení</string> + <string name="touch_droiddeck_menu">Nabídka DroidDeck</string> + <string name="touch_drag_hint">Přetáhněte prvky ovládání na obrazovku</string> + <string name="touch_arrange">Uspořádat</string> + <string name="touch_discard">Zahodit</string> + <string name="touch_saving">Ukládání…</string> + <string name="touch_icon">Ikona</string> + <string name="touch_narrower">Š−</string> + <string name="touch_wider">Š+</string> + <string name="touch_shorter">V−</string> + <string name="touch_taller">V+</string> + <string name="touch_hide">Skrýt</string> + <string name="touch_color_opacity">Barva a průhlednost…</string> + <string name="touch_copy_layout">Kopírovat toto rozložení</string> + <string name="touch_layout_copied">Rozložení zkopírováno; vložte ho do dotykového rozložení jakékoli hry</string> + <string name="touch_paste_layout">Vložit zkopírované rozložení</string> + <string name="touch_reset_layout">Obnovit výchozí rozložení Steamu</string> + <string name="touch_no_binding">Tento prvek nemá ve Steamu žádnou vazbu, které by se dala přiřadit ikona</string> + <string name="touch_icon_for">Ikona pro %1$s</string> + <string name="touch_layout_color">Barva dotykového rozložení</string> + <string name="touch_done">Hotovo</string> + <string name="touch_save_failed">Dotykové rozložení se nepodařilo uložit</string> + <string name="touch_saved">Dotykové rozložení uloženo ve Steamu</string> + <string name="touch_app_id">Aplikace %1$d</string> + <string name="touch_label_hint">Popisek (nepovinné)</string> + <string name="touch_icons_missing">Ikony Steamu nebyly nalezeny</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d ikona ze Steamu</item> + <item quantity="few">%1$d ikony ze Steamu</item> + <item quantity="many">%1$d ikony ze Steamu</item> + <item quantity="other">%1$d ikon ze Steamu</item> + </plurals> + <string name="touch_icon_color">Barva ikony</string> + <string name="touch_button_color">Barva tlačítka</string> + <string name="touch_no_icon">Bez ikony</string> + + <string name="stores_sync_sign_in">Ověřování přihlášení…</string> + <string name="stores_sync_library">Načítání vaší knihovny…</string> + <string name="stores_sync_game_list">Načítání seznamu her…</string> + <string name="stores_sync_details">Načítání podrobností o hrách… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">Synchronizace %1$d hry…</item> + <item quantity="few">Synchronizace %1$d her…</item> + <item quantity="many">Synchronizace %1$d hry…</item> + <item quantity="other">Synchronizace %1$d her…</item> + </plurals> + <string name="stores_sync_progress">Synchronizace… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Načítání obálek…</string> + <string name="stores_sync_failed">Synchronizace se nezdařila</string> + <string name="stores_sync_no_games">Ve vaší knihovně %1$s nebyly nalezeny žádné hry</string> + <string name="stores_sync_amazon_no_answer">Amazon Games nereagovala na vaši knihovnu: obnovte stránku nebo se přihlaste znovu</string> + <string name="stores_sync_gog_unreachable">Nepodařilo se spojit s GOG</string> + <string name="stores_sync_gog_unreadable">Vaši knihovnu GOG se nepodařilo načíst</string> + <string name="stores_registering">Registrace ve Steamu…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Režim Cardboard</string> + <string name="cardboard_mode_hint">Obraz pro levé a pravé oko pro VR brýle pro telefon.</string> + <string name="cardboard_source">Obraz</string> + <string name="cardboard_source_screen">Virtuální obrazovka</string> + <string name="cardboard_source_sbs">3D vedle sebe</string> + <string name="cardboard_viewer">Brýle</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Bez čočky</string> + <string name="cardboard_ipd">Vzdálenost očí</string> + <string name="cardboard_screen_width">Šířka obrazovky</string> + <string name="cardboard_screen_distance">Vzdálenost obrazovky</string> + <string name="cardboard_head_tracking">Sledování hlavy</string> + <string name="cardboard_no_sensor">Toto zařízení nemá senzor otáčení.</string> + <string name="cardboard_recenter">Vycentrovat pohled</string> + <string name="cardboard_recenter_button">Vycentrovat</string> + <string name="cardboard_steamvr">SteamVR (experimentální)</string> + <string name="cardboard_steamvr_install">Instalovat</string> + <string name="cardboard_steamvr_install_hint">Stáhne SteamVR pro ARM64 od Valve (571 MB).</string> + <string name="cardboard_steamvr_installing">Stahování a rozbalování SteamVR…</string> + <string name="cardboard_steamvr_start">Spustit</string> + <string name="cardboard_steamvr_ready_hint">Spustí SteamVR s vaší hlavou jako headsetem; obraz se přepne na 3D vedle sebe.</string> + <string name="cardboard_steamvr_stop">Zastavit</string> + <string name="cardboard_steamvr_running_hint">SteamVR běží.</string> + <string name="cardboard_steamvr_unavailable">V této relaci není k dispozici.</string> + <string name="cardboard_steamvr_failed">SteamVR neodpovídá; zkontrolujte protokol relace.</string> + <string name="drawer_component_downloads">Stáhnout komponenty</string> + <string name="drawer_component_downloads_hint">Stažené soubory se sdílejí se stránkou Komponenty. Výběrem stažené verze výše ji použijete při dalším spuštění hry.</string> + <string name="drawer_component_choose_download">Vyberte verzi ke stažení</string> + <string name="stores_fact_developer">Vývojář</string> + <string name="stores_fact_publisher">Vydavatel</string> + <string name="stores_fact_released">Vydáno</string> + <string name="stores_hltb_main">Hlavní</string> + <string name="stores_hltb_extras">+ Extra</string> + <string name="stores_hltb_completionist">Kompletní</string> + <string name="stores_hltb_hours">%1$s h</string> + <string name="stores_hltb_minutes">%1$d min</string> +</resources> diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml new file mode 100644 index 000000000..1a874c1cd --- /dev/null +++ b/app/src/main/res/values-da/strings.xml @@ -0,0 +1,2235 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>Den første start downloader Steam. Det kan tage nogle minutter.</item> + <item>Som standard sætter Steam på pause, når du forlader appen, og fortsætter, hvor du slap, når du kommer tilbage.</item> + <item>Din controller vises som en Xbox 360-controller. Brug ellers kontrollerne på skærmen.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>Den første start sætter skrivebordet op. Det kan tage nogle minutter.</item> + <item>Som standard sætter sessionen på pause, når du forlader appen, og fortsætter, hvor du slap, når du kommer tilbage.</item> + <item>Berøring fungerer som et touchpad: Træk for at flytte markøren, tryk for at klikke.</item> + <item>Din controller vises som en Xbox 360-controller. Brug ellers kontrollerne på skærmen.</item> + </string-array> + <string name="session_channel">Session</string> + <string name="session_channel_description">Vises, når en Steam- eller skrivebordssession er åben</string> + <string name="session_notification">Steam kører</string> + <string name="session_notification_downloads">Steam-download i gang</string> + <string name="session_notification_paused">Session på pause – tryk på Genoptag, når du er klar</string> + <string name="resume_session">Genoptag</string> + <string name="suspend_now">Sæt på pause nu</string> + <string name="stop_session">Afslut session</string> + <string name="frame_gen_title">Billedgenerering</string> + <string name="frame_gen_hint">Vælg metode til billedgenerering</string> + <string name="frame_gen_off">Fra</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG Adaptiv %1$d</string> + <string name="lsfg_needs_steam">Kræver Lossless Scaling på Steam</string> + <string name="lsfg_needs_install">Installer Lossless Scaling via Steam, eller importer Lossless.dll</string> + <string name="lsfg_import">Importer Lossless.dll…</string> + <string name="lsfg_using_steam">Bruger kopien fra Steam</string> + <string name="lsfg_using_import">Bruger en importeret kopi</string> + <string name="lsfg_pick_title">Vælg Lossless.dll</string> + <string name="lsfg_imported">Lossless.dll importeret</string> + <string name="lsfg_import_unchanged">Denne Lossless.dll er allerede i brug</string> + <string name="lsfg_import_not_owned">Lossless Scaling ejes ikke på denne Steam-konto</string> + <string name="lsfg_import_invalid">Denne fil er ikke Lossless Scalings Lossless.dll</string> + <string name="lsfg_import_failed">Kunne ikke forberede LSFG fra denne Lossless.dll</string> + <string name="lsfg_problem_missing">LSFG kræver Lossless Scaling fra Steam eller en importeret Lossless.dll</string> + <string name="lsfg_problem_failed">LSFG kunne ikke forberede sine shadere fra Lossless.dll</string> + <string name="hud_fg_unsupported">%1$s: ikke understøttet</string> + <string name="hud_fg_failed">%1$s: motoren fejlede</string> + <string name="hud_fg_needs_lossless">%1$s: kræver Lossless Scaling</string> + <string name="hud_fg_starting">%1$s %2$s · starter</string> + <string name="second_library_import_hint">Tilføjer et Steam-bibliotek og scanner det for Windows-spil. Ejede spil bruger deres Steam-poster; resten bliver genveje til spil, der ikke er fra Steam.</string> + <string name="game_env_title">Spilmiljø</string> + <string name="game_env_hint">Miljøvariabler for Proton-spil.</string> + <string name="game_env_edit">Rediger</string> + <string name="game_env_shared">Alle Proton-spil</string> + <string name="game_env_scope">Profil</string> + <string name="game_env_app_id">Steam-app-id</string> + <string name="game_env_open">Åbn</string> + <string name="game_env_profile">Spil %1$s</string> + <string name="game_env_add">Tilføj variabel</string> + <string name="game_env_name">Variabelnavn</string> + <string name="game_env_value">Værdi</string> + <string name="game_env_save">Gem</string> + <string name="game_env_remove">Fjern</string> + <string name="game_env_restore">Nedarv igen</string> + <string name="game_env_reset">Ryd miljøtilsidesættelser</string> + <string name="game_env_unset">Fjern for denne profil</string> + <string name="game_env_empty">Ingen variabler angivet.</string> + <string name="game_env_inherited">Nedarvet</string> + <string name="game_env_custom">Brugerdefineret variabel</string> + <string name="game_env_error">Kunne ikke indlæse eller gemme. Kontroller ledig lagerplads, og prøv igen.</string> + <string name="game_env_invalid">Brug et gyldigt navn og en værdi på op til 8192 tegn.</string> + <string name="game_env_unused">Den almindelige Proton-runtime læser ikke denne variabel.</string> + <string name="game_env_loading">Indlæser…</string> + <string name="game_env_feature">Tilsidesættelse af D3D12-funktionsniveau. 12_2 rapporterer DirectX 12 Ultimate-funktioner; ikke-understøttede funktioner kan fejle. Lad stå tomt for automatisk registrering.</string> + <string name="game_env_shader">Tilsidesættelse af D3D12-shadermodel. 6_9 kræver en tilstrækkeligt ny VKD3D og matchende GPU-funktioner. Lad stå tomt for automatisk registrering.</string> + <string name="game_env_cache">Behold Mesa-shader-cache aktiveret for at reducere gentagen kompilering. false aktiverer caching.</string> + <string name="game_env_cache_size">Maksimal størrelse for Mesa-shader-cache, f.eks. 1G. Større cache bruger mere lagerplads.</string> + <string name="game_env_vkd3d_config">VKD3D-indstillinger for D3D12. nodxr deaktiverer ray tracing; brug kun til et spil, der har brug for det.</string> + <string name="game_env_proton_log">Sæt til 1 for en diagnostisk Proton-log. Logning kan reducere ydelsen og bruge lagerplads.</string> + <string name="game_env_wined3d">Sæt til 1 for at bruge OpenGL-baseret WineD3D i stedet for DXVK til D3D9–11-kompatibilitetstest.</string> + <string name="game_env_no_ntsync">Sæt til 1 for at holde dette spil væk fra ntsync (droiddeck-ntsync eller en kerneldriver); det bruger droiddeck-esync, hvor en pack findes, ellers droiddeck-fsync.</string> + <string name="game_env_no_esync">Sæt til 1 for at slå droiddeck-esync fra for dette spil; det bruger droiddeck-fsync i stedet, eller droiddeck-ntsync hvis fsync også er fra.</string> + <string name="game_env_no_fsync">Sæt til 1 for at holde dette spil væk fra droiddeck-fsync; det bruger droiddeck-esync, hvor en pack findes, ellers droiddeck-ntsync.</string> + <string name="game_env_fsync_first">Sæt til 1 for at køre dette spil på droiddeck-fsync, eller 0 for at sætte droiddeck-esync først, hvor en pack passer til dets Proton.</string> + <string name="game_env_xalia">Sæt til 0 for at deaktivere Protons gamepad-navigationshjælper for dette spil.</string> + <string name="game_env_hud">DXVK-overlay til D3D9–11. fps viser billedhastigheden; lad stå tomt for at skjule den.</string> + <string name="game_env_dxvk_config">DXVK-indstillinger adskilt af semikolon. Nyere builds bruger dxvk.maxFrameRate til at begrænse billedhastigheden; foretræk sessionens billedgrænse til almindeligt spil.</string> + <string name="game_env_vkd3d_limit">Grænse for D3D12-billedhastighed. 0 deaktiverer grænsen.</string> + <string name="game_env_glthread">Mesa OpenGL-threading. true kan hjælpe CPU-begrænsede OpenGL-spil; test for hvert spil.</string> + <string name="sync_backend_title">Wine-synkronisering</string> + <string name="sync_backend_hint">Hvordan spil holder deres tråde i trit. Gælder fra næste session.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">FEX-forudindstilling</string> + <string name="fex_next_launch">Gælder fra næste spilstart.</string> + <string name="game_settings_title">Spil</string> + <string name="fex_default_label">FEX-standard</string> + <string name="fex_default_detail">Bruger FEX-standardindstillinger.</string> + <string name="fex_stability_label">Stabilitet</string> + <string name="fex_stability_detail">De sikreste, langsomste indstillinger.</string> + <string name="fex_compatibility_label">Kompatibilitet</string> + <string name="fex_compatibility_detail">Fuld TSO med multiblock.</string> + <string name="fex_intermediate_label">Mellemniveau</string> + <string name="fex_intermediate_detail">Afbalanceret TSO med reduceret x87-præcision.</string> + <string name="fex_performance_label">Ydelse</string> + <string name="fex_performance_detail">Al TSO deaktiveret; multiblock.</string> + <string name="fex_performance_tso_label">Ydelse + TSO</string> + <string name="fex_performance_tso_detail">Ydelsesindstillinger med primær TSO aktiveret.</string> + <string name="fex_extreme_label">Ekstrem</string> + <string name="fex_extreme_detail">Deaktiverer kontrol af selvmodificerende kode. JIT-spil og DRM kan gå i stykker.</string> + <string name="fex_extreme_tso_label">Ekstrem + TSO</string> + <string name="fex_extreme_tso_detail">Ekstreme indstillinger med primær TSO aktiveret.</string> + <string name="fex_extreme_gn_label">Ekstrem-gn</string> + <string name="fex_extreme_gn_detail">Ydelse med TSC og metadata-hints. Prøv før Ekstrem.</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">Ydelse med fuld kodekontrol og hypervisor-bit skjult.</string> + <string name="game_env_custom_value">Brugerdefineret værdi</string> + <string name="game_env_select_value">Vælg værdi</string> + <string name="user_apps_section">Emulatorer & apps</string> + <string name="user_apps_add">Tilføj</string> + <string name="user_apps_add_detail">Din egen app eller spil</string> + <string name="user_apps_kind_script">Script</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d%%</string> + <string name="user_apps_starting">Starter…</string> + <string name="user_apps_added">%1$s tilføjet</string> + <string name="user_apps_removed">%1$s fjernet</string> + <string name="user_apps_back">Skrivebord</string> + <string name="user_apps_gone">Den app findes ikke længere. Vælg den igen fra Skrivebord.</string> + <string name="user_apps_open">Åbn %1$s</string> + <string name="user_apps_remove">Fjern</string> + <string name="user_apps_remove_confirm">Tryk igen for at fjerne</string> + <string name="user_apps_stop_session">Afslut sessionen for at fjerne</string> + <string name="user_apps_script_linked">Kører %1$s fra dens egen mappe, linket ind i sessionen, hvor den ligger. Fjernes appen, lades mappen urørt.</string> + <string name="user_apps_script_copied">Kører %1$s fra en kopi af dens mappe inde i DroidDeck. Fjernes appen, slettes kopien, ikke originalen.</string> + <string name="user_apps_runtime_required">Installer Linux-runtimen først</string> + <string name="user_apps_file_gone">Filen findes ikke længere</string> + <string name="user_apps_no_space">Ikke nok ledig plads: kopiering af mappen kræver %1$s</string> + <string name="user_apps_copying">Kopierer %1$s</string> + <string name="user_apps_failed">Det mislykkedes at tilføje appen</string> + <string name="user_apps_github_checking">Søger efter en udgivelse af %1$s</string> + <string name="user_apps_github_unreachable">Kunne ikke læse udgivelserne for %1$s</string> + <string name="user_apps_github_no_appimage">Ingen udgivelse af %1$s har en ARM64- eller x86_64-AppImage</string> + <string name="user_apps_downloading">Downloader %1$s</string> + <string name="user_apps_download_failed">Download mislykkedes</string> + <string name="user_apps_update_failed">Det mislykkedes at opdatere appen</string> + <string name="user_apps_icon_failed">Det ikon kunne ikke læses</string> + <string name="user_apps_saved">%1$s gemt</string> + <string name="user_apps_edit">Rediger</string> + <string name="user_apps_check_updates">Søg efter opdateringer</string> + <string name="user_apps_checking_updates">Søger efter opdateringer…</string> + <string name="user_apps_update_to">Opdater til %1$s</string> + <string name="user_apps_up_to_date">Opdateret · %1$s</string> + <string name="user_apps_updated">%1$s opdateret</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">Tilføj en app</string> + <string name="add_app_tab_script">Script</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">En .sh-fil. Den køres fra sin egen mappe i fuld skærm under gamescope.</string> + <string name="add_app_appimage_hint">En ARM64 (aarch64) AppImage, eller en x86_64-udgave, der køres via FEX. Den udpakkes i Linux-runtimen én gang.</string> + <string name="add_app_github_hint">Installerer den nyeste udgivelse af ARM64-AppImage fra repositoryet, eller x86_64-udgaven via FEX.</string> + <string name="app_fex_title">Brug FEX</string> + <string name="app_fex_auto">Auto</string> + <string name="app_fex_on">Til</string> + <string name="app_fex_off">Fra</string> + <string name="app_fex_hint">FEX kører x86-Linux-programmer på denne ARM64-enhed. Auto bruger det, når programmet er bygget til x86.</string> + <string name="app_fex_x86">%1$s via FEX</string> + <string name="app_fex_not_ready">Dette program er bygget til %1$s-pc’er og køres via FEX, som ikke er sat op endnu. Åbn Steam én gang, mens du er logget ind: DroidDeck downloader så FEX og den Steam Linux Runtime, det kræver, hvorefter dette åbner.</string> + <string name="session_end_program">Dette program</string> + <string name="session_end_fex_missing">%1$s er bygget til x86-pc’er og køres via FEX, som ikke er installeret endnu. Start Steam én gang, mens du er logget ind, og lad det downloade FEX og Steam Linux Runtime, og start så dette igen.</string> + <string name="add_app_flatpak_hint">Et Flathub-app-id eller dens Flathub-side. Flatpak sættes op først, hvis det ikke allerede er.</string> + <string name="add_app_file">Fil</string> + <string name="add_app_no_file">Ingen fil valgt</string> + <string name="add_app_choose_file">Vælg fil</string> + <string name="add_app_change_file">Skift</string> + <string name="add_app_pick_script">Vælg et script</string> + <string name="add_app_pick_appimage">Vælg en AppImage</string> + <string name="add_app_pick_icon">Vælg et ikon</string> + <string name="add_app_repository">Repository</string> + <string name="add_app_repository_placeholder">ejer/repo eller et github.com-link</string> + <string name="add_app_repository_invalid">Ikke et GitHub-repository</string> + <string name="add_app_flatpak_id">App-id</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">Ikke et Flathub-app-id</string> + <string name="add_app_flatpak_bundle_hint">Eller en .flatpak-fil til en app, der ikke findes på Flathub.</string> + <string name="add_app_pick_flatpak">Vælg en .flatpak-fil</string> + <string name="add_app_source">Kilde</string> + <string name="add_app_appearance">På dit skrivebord</string> + <string name="add_app_preview_name">Ny app</string> + <string name="add_app_name">Navn</string> + <string name="add_app_name_placeholder">Fra appen</string> + <string name="add_app_icon">Ikon</string> + <string name="add_app_icon_default">Standardikon</string> + <string name="add_app_choose_icon">Vælg ikon</string> + <string name="add_app_clear_icon">Ryd</string> + <string name="add_app_icon_suggestions">Fra repositoryet</string> + <string name="add_app_icon_searching">Søger efter ikoner…</string> + <string name="add_app_icon_reset">Standard</string> + <string name="edit_app_title">Rediger %1$s</string> + <string name="edit_app_save">Gem</string> + <string name="add_app_checking_folder">Kontrollerer mappen…</string> + <string name="add_app_folder_linked">Dens mappe er linket ind i sessionen: %1$s</string> + <string name="add_app_folder_copied">Dens mappe indeholder Linux-programmer, som delt lager ikke kan køre, så den kopieres ind i DroidDeck (%1$s).</string> + <string name="add_app_confirm">Tilføj</string> + <string name="add_app_cancel">Annuller</string> + <string name="add_app_runtime_required">Installer Linux-runtimen fra Opsætning først.</string> + <string name="add_app_busy">Vent på, at %1$s er færdig.</string> + <string name="steam_compat_label">Kompatibel</string> + <string name="proton_compatible_note">Builds markeret med “%1$s” i Steams kompatibilitetsmenu har DroidDecks prefix-fixes og kan starte spil på denne enhed. Standarden er Proton Experimental ARM64 (%1$s), eller Proton 11.0 ARM64 (%1$s), når den nyere build er installeret. Valves egne Proton-poster uden mærket kan ikke starte et spil på denne enhed.</string> + <string name="proton_installed_compatible">Installeret %1$s · %2$s</string> + <string name="common_cancel">Annuller</string> + <string name="common_auto">Auto</string> + <string name="common_never">Aldrig</string> + <string name="common_always">Altid</string> + <string name="common_uninstall">Afinstaller</string> + <string name="common_check">Kontroller</string> + <string name="common_update">Opdater</string> + <string name="common_checking">Kontrollerer…</string> + <string name="common_working">Arbejder…</string> + <string name="common_forget">Glem</string> + <string name="common_add_ellipsis">Tilføj…</string> + <string name="common_use">Brug</string> + <string name="common_applies_next_session">Gælder fra næste session.</string> + <string name="mode_steam_title">Steam-session</string> + <string name="mode_desktop_title">Skrivebordssession</string> + <string name="mode_display">Skærm</string> + <string name="mode_resolution">Opløsning</string> + <string name="mode_res_720">Op til 720p</string> + <string name="mode_res_900">Op til 900p</string> + <string name="mode_res_1080">Op til 1080p</string> + <string name="mode_res_native">Native</string> + <string name="mode_res_default">%1$s (standard)</string> + <string name="mode_res_custom_value">Brugerdefineret · %1$d×%2$d</string> + <string name="mode_res_custom">Brugerdefineret…</string> + <string name="mode_res_note">720p kan gøre menuer mere lydhøre.</string> + <string name="mode_ratio">Billedformat</string> + <string name="mode_ratio_custom">Angivet af den brugerdefinerede opløsning.</string> + <string name="mode_ratio_auto">Auto bruger mindst 16:9.</string> + <string name="mode_fps">Billedgrænse</string> + <string name="mode_fps_note">Begrænser hele sessionen. Skærmen skifter til en opdateringshastighed, grænsen går lige op med.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">HDR10-output</string> + <string name="mode_hdr_unavailable">Ikke tilgængelig: %1$s.</string> + <string name="mode_hdr_restart">Genstart appen for at anvende ændringen.</string> + <string name="mode_drivers">Drivere</string> + <string name="mode_gpu_drivers">GPU-drivere</string> + <string name="mode_gpu_drivers_hint">Delt mellem Steam og Skrivebord. Indstilles på siden Komponenter.</string> + <string name="mode_touch_controls">Berøring & controller</string> + <string name="mode_touch">Berøring</string> + <string name="mode_touch_touchpad">Touchpad</string> + <string name="mode_touch_direct">Direkte</string> + <string name="mode_touch_note">Auto: touchpad på Skrivebord, direkte i Steam.</string> + <string name="mode_osc">Kontroller på skærmen</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">Auto viser kontrollerne, når ingen controller er tilsluttet.</string> + <string name="osc_steam_touch">Steam-berøring (beta)</string> + <string name="osc_style_next_session">Steams berøringskontroller starter med næste Steam-session</string> + <string name="osc_steam_layout">Steam-berøringslayout</string> + <string name="osc_steam_layout_hint">Flyt, tilpas størrelsen på eller skjul dette spils berøringskontroller; gemmes til Steam</string> + <string name="mode_controller">Controller</string> + <string name="mode_controller_hint">Hvordan Steam ser din controller. Gælder fra næste session.</string> + <string name="mode_controller_deck">Steam Deck-controller</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Xbox 360-controller</string> + <string name="mode_controller_note">Deck: Hurtig adgang-knap og gyro. Steam Controller: det samme, med gribefølsomme bagknapper. DualShock 4 og DualSense: gyro, og touchpaddene som deres touchpad. Xbox 360: åbn Hurtig adgang med Guide+A.</string> + <string name="mode_back">Tilbage</string> + <string name="back_menu_then_qam">Menu → QAM</string> + <string name="back_qam_then_menu">QAM → Menu</string> + <string name="mode_session">Session</string> + <string name="mode_suspend">Baggrundsopførsel</string> + <string name="mode_suspend_hint">Når appen er skjult, eller skærmen slukkes.</string> + <string name="mode_suspend_manual">Manuel</string> + <string name="mode_suspend_native">Native</string> + <string name="mode_suspend_steam_note">Auto sætter på pause og genoptager, når den vises. Native forbereder Steam til dvale først. Manuel venter på Genoptag. Aldrig fortsætter med at køre.</string> + <string name="mode_background_downloads">Fortsæt downloads</string> + <string name="mode_background_downloads_hint">Udskyder baggrundspause, mens Steam aktivt downloader.</string> + <string name="mode_suspend_note">Auto sætter på pause i baggrunden og genoptager, når den vises. Manuel venter på Genoptag. Aldrig fortsætter med at køre.</string> + <string name="mode_startup">Opstart</string> + <string name="mode_steam_startup">Start Steam med DroidDeck</string> + <string name="mode_steam_startup_hint">Åbner Steam-sessionen, når DroidDeck starter.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">Søger efter udgivelser…</string> + <string name="mode_decky_ready">Klar til at installere %1$s.</string> + <string name="mode_decky_none">Ingen kompatibel build fundet. Prøv igen senere.</string> + <string name="mode_decky_installed">Installeret · %1$s</string> + <string name="mode_decky_update">Opdatering tilgængelig · %1$s</string> + <string name="mode_decky_current">Opdateret · %1$s</string> + <string name="mode_decky_loader">Loader</string> + <string name="mode_decky_install_latest">Installer nyeste</string> + <string name="mode_decky_on">Starter med Steam. Mens den er til, kan andre apps på denne enhed muligvis styre Steam.</string> + <string name="mode_decky_off">Fra. Steams fjerndebugport forbliver lukket.</string> + <string name="mode_decky_remove_title">Afinstaller Decky Loader?</string> + <string name="mode_decky_remove_text">Fjerner loaderen. Dine plugins og indstillinger bevares.</string> + <string name="mode_client">Klient</string> + <string name="mode_mangoapp">Ydelsesoverlay</string> + <string name="mode_mangoapp_hint">Slå fra, hvis spil går ned eller bliver sorte i Deck-tilstand. Gælder fra næste session.</string> + <string name="mode_steam_repair">Reparer Steam-klienten</string> + <string name="mode_steam_repair_hint">Downloader Steam-klienten igen, næste gang Steam starter. Brug det, hvis Steam bliver hængende på sin indlæsningsskærm. Spil, login og indstillinger bevares.</string> + <string name="mode_steam_repair_queued">Steam-klienten downloades igen, næste gang Steam starter.</string> + <string name="mode_steam_repair_button">Reparer</string> + <string name="mode_added_art">Grafik</string> + <string name="mode_sgdb_key">SteamGridDB API-nøgle</string> + <string name="mode_sgdb_key_yours">Din nøgle</string> + <string name="mode_sgdb_edit">Rediger…</string> + <string name="mode_added_art_hint">Udfylder manglende grafik fra Steam, derefter SteamGridDB. Din egen cover.jpg, hero.jpg, logo.png eller icon.png i spillets mappe har forrang.</string> + <string name="summary_remove_game">Fjern %1$s</string> + <string name="mode_added_remove_confirm">Fjern?</string> + <string name="games_add">Tilføj spil</string> + <string name="games_add_title">Tilføj et spil:</string> + <string name="games_pick_exe_title">Vælg exe-filen:</string> + <string name="games_add_unreachable">Denne placering er ikke tilgængelig i sessionen</string> + <string name="mode_added_exe_check">Kontroller denne exe-fil</string> + <string name="wincomp_title">Windows-komponenter</string> + <string name="wincomp_pad_hint">LB/RB skifter mellem afsnittene.</string> + <string name="wincomp_card_none">Ingen</string> + <string name="wincomp_applies">Windows-dll’er, Proton ikke inkluderer, fra komponentlisten. Slå dem til, som dette spil har brug for: de lægges ind i spillets egen Windows-mappe, aldrig dets egne filer, og følger med, uanset hvilken Proton det køres på.</string> + <string name="wincomp_offline">Kunne ikke nå komponentlisten. Komponenter, der allerede er downloadet, kan stadig skiftes.</string> + <string name="wincomp_recommended">Anbefalet til dette spil</string> + <string name="wincomp_checking">Kontrollerer, hvad dette spil har brug for…</string> + <string name="wincomp_when_online">Tilgængelig online</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">Slår %1$d komponent til…</item> + <item quantity="other">Slår %1$d komponenter til…</item> + </plurals> + <string name="wincomp_auto_section">Til for dette spil</string> + <string name="wincomp_auto_steam">Steam installerer %1$s med %2$s (app %3$s)</string> + <string name="wincomp_auto_gog">Fra GOG’s liste for dette spil</string> + <string name="wincomp_auto_epic">Fra Epics forudsætninger for dette spil</string> + <string name="wincomp_auto_amazon">Fra Amazons installationstrin for dette spil</string> + <string name="wincomp_steam_by_name">Steam installerer %1$s med app %2$s, matchet efter navn</string> + <string name="wincomp_suggestions">Også på listen</string> + <string name="wincomp_suggestion_reason">Angivet af %1$s; ikke en komponent her</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">Spillet leveres med %1$s</string> + <string name="wincomp_found_shipped">Spillet har %1$s (fungerer tit som det er)</string> + <string name="wincomp_already_here">Allerede i dette spil</string> + <string name="wincomp_no_recommendation">Intet i spillets mappe peger på en komponent.</string> + <string name="wincomp_active">Installeret til dette spil (%1$d)</string> + <string name="wincomp_all">Alle komponenter (%1$d)</string> + <string name="wincomp_installers">Kræver et Windows-installationsprogram (%1$d)</string> + <string name="wincomp_installers_note">Disse er deres egne installationsprogrammer uden en pakke indeni at tage fra hinanden, og der er endnu ikke registreret, hvad de installerer. De fleste er gamle runtimes, som Mono inde i Proton allerede dækker; registreringer tilføjes, når de bliver lavet.</string> + <string name="wincomp_show">Vis</string> + <string name="wincomp_hide">Skjul</string> + <string name="wincomp_needs_installer">Kører sit eget installationsprogram (endnu ikke registreret)</string> + <string name="wincomp_unsupported">Ikke understøttet her endnu</string> + <string name="wincomp_loading">Indlæser komponenterne…</string> + <string name="wincomp_failed">Kunne ikke installere: %1$s</string> + <string name="wincomp_downloaded">Downloadet</string> + <string name="wincomp_downloading">Downloader %1$s</string> + <string name="wincomp_installing">Installerer: %1$s</string> + <string name="wincomp_reverting">Slår fra…</string> + <string name="wincomp_download_size">%1$s download</string> + <string name="wincomp_next_launch">Træder i kraft, næste gang spillet starter.</string> + <string name="mode_stretch169">Stræk 16:9-spil</string> + <string name="mode_stretch169_hint">På skærme mere kvadratiske end 16:9 strækkes spil, så de udfylder skærmen, i stedet for at vise sorte bjælker. Gælder fra næste session.</string> + <string name="mode_audio">Lyd</string> + <string name="mode_directaudio">DirectAudio til spil</string> + <string name="mode_directaudio_hint">Går uden om PulseAudio for lavere forsinkelse.</string> + <string name="mode_mic">Mikrofon</string> + <string name="mode_mic_hint">Til voicechat.</string> + <string name="mode_network">Netværk</string> + <string name="mode_wifi_discovery">Wi-Fi-søgning</string> + <string name="mode_wifi_discovery_hint">Vis Wi-Fi-navne og netværk i nærheden i Steam.</string> + <string name="mode_wifi_location_off">Placeringstjenester er fra.</string> + <string name="mode_wifi_permission_denied">Placeringstilladelse ikke givet.</string> + <string name="mode_wifi_open_settings">Åbn indstillinger</string> + <string name="mode_wifi_continue">Fortsæt</string> + <string name="mode_wifi_explain_title">Aktiver Wi-Fi-søgning?</string> + <string name="mode_wifi_explain_text">Android kræver placeringstilladelse for at læse Wi-Fi-navne og finde netværk i nærheden. DroidDeck bruger denne adgang til at vise disse netværk i Steam. Vi registrerer ikke din placering. Internetadgang fungerer uden denne tilladelse.</string> + <string name="mode_storage">Spillager</string> + <string name="mode_storage_auto">Automatisk – SD-kortet, når det er isat, plus dine Spil-mapper</string> + <string name="mode_storage_auto_none">Automatisk – dine Spil-mapper, plus SD-kortet, når det er isat (ingen nu)</string> + <string name="mode_storage_internal">Kun internt</string> + <string name="mode_storage_folder">Mappe: %1$s</string> + <string name="mode_second_library">Andet bibliotek</string> + <string name="mode_storage_note">Spil, der streamer data fra SD-kort eller delt lager, kan stamme.</string> + <string name="mode_storage_diagnostics">Lagerdiagnostik</string> + <string name="mode_storage_diagnostics_hint">Registrerer allokeringsresultater og lagertider i Del logs. Kan gøre installationer langsommere, mens det er aktivt. Gælder fra næste session.</string> + <string name="mode_choose_folder">Vælg en mappe…</string> + <string name="mode_custom_res_title">Brugerdefineret opløsning</string> + <string name="mode_custom_res_text">Erstatter grænsen og billedformatet. En størrelse, der ikke matcher skærmen, giver sorte bjælker.</string> + <string name="mode_width">Bredde</string> + <string name="mode_height">Højde</string> + <string name="mode_custom_res_range">Mellem 320×240 og 3840×2160.</string> + <plurals name="mode_added_count"> + <item quantity="one">%1$d spil</item> + <item quantity="other">%1$d spil</item> + </plurals> + <string name="common_ok">OK</string> + <string name="common_delete">Slet</string> + <string name="common_restore">Gendan</string> + <string name="common_download">Download</string> + <string name="common_delete_named">Slet %1$s</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Runtimedriver</string> + <string name="comp_runtime_driver_hint">Bruges af Steam, spil og skrivebordet. Gælder fra næste session.</string> + <string name="comp_import_turnip">Importer Turnip-zip…</string> + <string name="comp_display_driver">Skærmdriver</string> + <string name="comp_display_driver_hint">Tegner billeder til skærmen i begge tilstande. Genstart appen for at anvende.</string> + <string name="comp_import_adrenotools">Importer en AdrenoTools-zip…</string> + <string name="comp_never">aldrig</string> + <string name="comp_nightlies_checked">Nightlies kontrolleret %1$s</string> + <string name="comp_title">Komponenter</string> + <string name="comp_about">Om Komponenter</string> + <string name="comp_check_drivers">Søg efter nye drivere</string> + <string name="comp_check_nightlies">Søg Nightlies for nye pakker</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">Den Proton, der vælges her, kører alle spil, der ikke selv har valgt en i Steam, og en ændring foretaget i Steam vises også her. Steam erstatter Valves Proton, når den opdaterer; originalerne bevares for hver build.</string> + <string name="comp_scope">Konfiguration</string> + <string name="comp_profile_configure">Konfigurer profil</string> + <string name="comp_profile">Profil</string> + <string name="comp_scope_general">Standardprofil</string> + <string name="comp_scope_general_detail">Bruges automatisk af spil uden tilsidesættelser.</string> + <string name="comp_profile_game_detail">Gemte indstillinger bruges automatisk, når %1$s starter.</string> + <string name="comp_game_hint">Vælg kun de indstillinger, dette spil har brug for. Alt andet fortsætter med at bruge standardprofilen.</string> + <string name="comp_game_fex">FEX-forudindstilling</string> + <string name="comp_game_fex_inherited">Bruger standardprofilens forudindstilling.</string> + <string name="comp_game_fex_override">Brugerdefineret forudindstilling gemt for dette spil.</string> + <string name="comp_game_fex_custom">Brugerdefinerede FEX-variabler</string> + <string name="comp_game_use_general">Brug standard</string> + <string name="comp_game_compatibility">Spilkompatibilitet</string> + <string name="comp_game_environment_hint">Miljøindstillinger for Proton, DXVK, VKD3D og synkronisering for dette spil.</string> + <string name="comp_game_proton_inherited">Bruger standardprofilens Proton.</string> + <string name="comp_game_proton_override">Brugerdefineret Proton gemt for dette spil via Steams kompatibilitetstilknytning.</string> + <string name="comp_no_proton_short">Ingen Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Bruger standardprofilens pakke fra %1$s, hvis ikke dette spil vælger en anden pakke.</string> + <string name="comp_game_component_general">Bruger standardprofilens komponentpakke.</string> + <string name="comp_game_component_override">Bruger kun %1$s til dette spil.</string> + <string name="comp_game_runtime_options">Runtime-indstillinger</string> + <string name="comp_game_environment_other_hint">Andre miljøindstillinger pr. spil for Proton, DXVK, VKD3D-Proton og synkronisering.</string> + <string name="comp_game_saved_next">%1$s’s profil er gemt til næste spilstart.</string> + <string name="game_no_steam_title">Start uden Steam</string> + <string name="game_no_steam_detail">Til et spil, der lukker med det samme: starter det uden Protons Steam-opsætning. Steam-funktioner som overlay er slået fra for det.</string> + <string name="game_no_steam_on">Til</string> + <string name="comp_game_proton_live">%1$s’s Proton-valg er gemt og tilgængeligt i Steam nu.</string> + <string name="comp_game_proton_next">%1$s’s Proton-valg er gemt. Genstart Steam, inden du starter spillet.</string> + <string name="comp_game_proton_not_runnable">%1$s kan ikke bruge den valgte Proton på denne enhed.</string> + <string name="comp_game_load_failed">Kunne ikke indlæse %1$s’s profil: %2$s</string> + <string name="comp_game_save_failed">Spilprofilen kunne ikke gemmes: %1$s</string> + <string name="comp_game_reset">Nulstil spilprofil</string> + <string name="comp_game_reset_title">Nulstil %1$s’s profil?</string> + <string name="comp_game_reset_body">Fjern dette spils tilsidesættelser af Proton, FEX, komponentpakke, miljø, teksturfiltrering og Windows-komponenter? Det bruger herefter standardprofilen.</string> + <string name="comp_game_reset_done">%1$s bruger nu standardprofilen.</string> + <string name="comp_game_reset_failed">Spilprofilen kunne ikke nulstilles helt: %1$s</string> + <string name="comp_default_live">%1$s kører nu alle spil, der ikke selv har valgt en Proton i Steam.</string> + <string name="comp_default_next">%1$s kører alle spil, der ikke selv har valgt en Proton, fra næste gang Steam starter.</string> + <string name="comp_default_not_runnable">%1$s kan ikke starte spil her, så den kan ikke være standarden.</string> + <string name="comp_default_failed">Standard-Protonen kunne ikke gemmes: %1$s</string> + <string name="comp_swapped">%1$s · %2$d udskiftet</string> + <string name="comp_all_original">%1$s · alt originalt</string> + <string name="comp_game_running_suffix">%1$s · spil kører</string> + <string name="comp_esync_pack">%1$s · droiddeck-esync-pack</string> + <string name="comp_esync_wanted">%1$s · ingen droiddeck-esync-pack endnu</string> + <string name="comp_esync_builtin">%1$s · esync er indbygget, ingen pack nødvendig</string> + <string name="comp_gpu_drivers">GPU-drivere</string> + <string name="comp_reapplied">%1$s · genanvendt ved start %2$s</string> + <string name="comp_next_queued">Næste: %1$s · når spillet er lukket</string> + <string name="comp_in_use">%1$s i brug: %2$s</string> + <string name="comp_waits">Venter til spillet lukkes</string> + <string name="comp_applies_next_game">Gælder, næste gang et spil starter</string> + <string name="comp_import_zip">Importer .zip</string> + <string name="comp_reading">Læser Protons…</string> + <string name="comp_no_proton">Ingen Proton installeret endnu. Start Steam én gang for at downloade dens ARM64-Proton, eller tilføj GE-Proton, CachyOS eller DroidDeck-Proton fra Opsætning.</string> + <string name="comp_original">Original · %1$s</string> + <string name="comp_this_build">%1$s · denne build</string> + <string name="comp_earlier_build">%1$s · tidligere build</string> + <string name="comp_installed_count">Installeret · %1$d</string> + <string name="comp_nothing_stored">Intet gemt for %1$s endnu. Denne Protons egne filer gemmes som dens original.</string> + <string name="comp_waits_long">Et spil kører på denne Proton, så dette venter, til det lukkes.</string> + <string name="comp_applies_long">Gælder, næste gang et spil starter.</string> + <string name="comp_swap_title">Udskift %1$s?</string> + <string name="comp_swap_body">Læg %1$s ind i %2$s? De originale filer forbliver gemt. %3$s</string> + <string name="comp_restore_title">Gendan %1$s?</string> + <string name="comp_restore_body">Læg originalen af %1$s tilbage i %2$s? %3$s</string> + <string name="comp_delete_package_body">Fjerner pakken fra appen. Du kan downloade den igen.</string> + <string name="comp_delete_original_title">Slet denne original?</string> + <string name="comp_delete_original_body">Denne original af %1$s er fra en tidligere build af %2$s. Den aktuelle builds original bevares altid.</string> + <string name="comp_nightlies_count">På Nightlies · %1$d</string> + <string name="comp_refresh_to_list">Opdater for at se pakker på Nightlies.</string> + <string name="comp_nothing_new">Intet nyt siden sidste kontrol.</string> + <string name="comp_import_wcp">Importer .wcp</string> + <string name="comp_restore_original">Gendan denne Protons original</string> + <string name="comp_restore_original_body">Læg %1$s’s original %2$s tilbage?</string> + <string name="comp_swap">Udskift</string> + <string name="comp_about_text">FEX, DXVK og VKD3D-Proton for hver Proton, Steam kører spil med.\n\nVælg en installeret række for at sætte den i. Den træder i kraft, næste gang et spil starter, eller når det kørende spil er lukket.\n\nDine valg kontrolleres igen før hver start og lægges tilbage, hvis noget har ændret dem. Hver Protons originale filer bevares, så Steam-opdateringer aldrig fjerner dem.</string> + <string name="comp_tag_original">Original</string> + <string name="comp_tag_in_use">I brug</string> + <string name="comp_tag_new">Ny</string> + <string name="comp_tag_stored">Gemt</string> + <string name="perf_title">Ydelse</string> + <string name="perf_lede">CPU-kerner, sessionsrettelser og værtsnavn. Gælder fra næste session.</string> + <string name="perf_client_cores_group">Steam-klientens kerner</string> + <string name="perf_override">Tilsidesæt Steams kernevalg</string> + <string name="perf_override_hint">Fastgør Steam, dens UI-hjælper og gamescope til kernerne nedenfor.</string> + <string name="perf_client_cores">Klientkerner</string> + <string name="perf_client_cores_on">Kerner til Steam.</string> + <string name="perf_client_cores_off">Slå tilsidesættelsen til for at vælge.</string> + <string name="perf_game_cores_group">Spilkerner</string> + <string name="perf_game_cores">Spilkerner</string> + <string name="perf_game_cores_hint">Kerner til spil. Med alle valgt planlægger Android frit.</string> + <string name="perf_client_ui">Klientgrænseflade</string> + <string name="perf_glthread">Threaded GL</string> + <string name="perf_glthread_hint">Kan gøre Steam-menuer mere lydhøre.</string> + <string name="perf_zink">Zink: lazy descriptors</string> + <string name="perf_zink_hint">Anbefalet til drivere uden descriptor buffers.</string> + <string name="perf_noglerror">Spring GL-fejlkontrol over</string> + <string name="perf_noglerror_hint">Slår validering af GL fra for hvert kald.</string> + <string name="perf_gsrealtime">gamescope: realtids-GPU-kø</string> + <string name="perf_gsrealtime_hint">Prioriterer compositorens GPU-arbejde. Kan give jævnere billedtiming på bekostning af spillene.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">Hold GPU’en på dens højeste clock</string> + <string name="perf_gpuclock_hint">Kun Adreno. Færre hak, mere batteribrug og varme.</string> + <string name="perf_fixes">Sessionsrettelser</string> + <string name="perf_sysmem">Turnip: sysmem-rendering</string> + <string name="perf_sysmem_hint">Hurtigere til Steam og de fleste spil. Påkrævet på Adreno 710/720/722.</string> + <string name="perf_xalia">Spring Steams xalia-hjælper over</string> + <string name="perf_fastsync">droiddeck-ntsync (eksperimentel)</string> + <string name="perf_fastsync_hint">En stand-in til ntsync-driveren i Linux-kernen, i brugerrum, til test. Mens den er til, bruger Proton den i stedet for droiddeck-esync og droiddeck-fsync. Fungerer med Proton Experimental ARM64, GE-Proton og proton-cachyos. Gælder fra næste session.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync først</string> + <string name="perf_fsync_hint">Kører Protons egen fsync på de uændrede Proton-filer, med futex_waitv leveret i brugerrum, så det kræver ingen pack og fortsætter med at virke, lige så snart Steam opdaterer Proton. Mens den er til, bruger spil den før droiddeck-esync, undtagen de få, Proton holder uden for fsync. Gælder fra næste session.</string> + <string name="perf_esync_hint">Spils Windows-tråde venter på hinanden via eventfd og delt hukommelse i stedet for ture gennem wineserver. Bruger en droiddeck-esync-pack lavet til netop denne Proton-build; packs hentes automatisk. Indtil der findes en til en ny Proton-build, dækker droiddeck-fsync i stedet. Gælder fra næste session.</string> + <string name="perf_xalia_hint">Deaktiverer Protons gamepad-navigationshjælper, som koster CPU i hvert spil.</string> + <string name="perf_seccomp">Kør proot uden seccomp</string> + <string name="perf_seccomp_hint">Kan afhjælpe fejl om manglende systemkald på nogle kerner. Kan reducere ydelsen.</string> + <string name="perf_fastpath">proot fast path</string> + <string name="perf_fastpath_needs_seccomp">Kræver prootets seccomp-filter.</string> + <string name="perf_fastpath_hint">Hurtigere filopslag og mindre hak. Slå fra, hvis et program ikke kan finde sine filer.</string> + <string name="perf_identity">Sessionsidentitet</string> + <string name="perf_hostname">Værtsnavn</string> + <string name="perf_hostname_hint">Navnet Steam ser for denne enhed. Tomt gendanner %1$s.</string> + <string name="perf_hostname_invalid">Kun bogstaver, tal og bindestreger i midten, op til 63 tegn.</string> + <string name="perf_phantom_warning">Android er sat til at lukke denne session</string> + <string name="driver_installed">Installeret</string> + <string name="driver_available">Tilgængelig til download</string> + <string name="driver_nothing_new">Intet nyt siden sidste kontrol.</string> + <string name="driver_restore_builtin">Gendan indbyggede drivere</string> + <string name="driver_delete_builtin">Indbygget i appen: dette fjerner dens udpakkede filer og skjuler den. Gendan indbyggede drivere bringer den tilbage.</string> + <string name="driver_delete_bundle">Halvdelen af en Android- og Linux-bundle: begge halvdele fjernes. En driver i brug falder tilbage til standarden.</string> + <string name="driver_delete_plain">Dens filer fjernes fra appen.</string> + <string name="driver_delete_in_use">Dens filer fjernes. Den er i brug, så standarden tager over.</string> + <string name="drawer_page_display">Skærm</string> + <string name="drawer_page_effects">Effekter</string> + <string name="drawer_page_controls">Kontroller</string> + <string name="drawer_page_components">Komponenter</string> + <string name="drawer_page_controller">Controller</string> + <string name="drawer_page_settings">Indstillinger</string> + <string name="drawer_page_desc">Siden %1$s</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Skrivebord</string> + <string name="drawer_steam_menu">Steam-menu</string> + <string name="drawer_quick_access">Hurtig adgang</string> + <string name="drawer_open_qam">Åbn Hurtig adgang-menuen</string> + <string name="drawer_prev_page">Forrige side</string> + <string name="drawer_next_page">Næste side</string> + <string name="drawer_hud">Ydelses-HUD</string> + <string name="drawer_scaling">Skaleringstilstand</string> + <string name="drawer_scaling_sharpness">Skaleringsskarphed</string> + <string name="drawer_effects">Skærmeffekter</string> + <string name="drawer_effects_hint">Live. Koster lidt GPU-tid, mens en effekt er til.</string> + <string name="drawer_look">Forudindstillinger</string> + <string name="drawer_look_custom">Brugerdefineret</string> + <string name="drawer_effects_advanced">Avanceret</string> + <string name="widgets_expanded">Udvidet</string> + <string name="widgets_collapsed">Sammenfoldet</string> + <string name="drawer_cas">Skarphed (CAS)</string> + <string name="drawer_cas_level">Skarphedsniveau</string> + <string name="drawer_fake_hdr">Falsk HDR</string> + <string name="drawer_deband">Debanding</string> + <string name="drawer_deband_strength">Debanding-styrke</string> + <string name="drawer_brightness">Lysstyrke</string> + <string name="drawer_contrast">Kontrast</string> + <string name="drawer_gamma">Gamma</string> + <string name="drawer_saturation">Farvemætning</string> + <string name="drawer_fxaa">FXAA (kantudjævning)</string> + <string name="drawer_toon">Toon-konturer</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Teksturfiltrering</string> + <string name="drawer_texture_hint">DirectX 9–11-spil, fra deres næste start.</string> + <string name="drawer_anisotropy">Anisotropisk filtrering</string> + <string name="drawer_texture_sharpness">Teksturskarphed</string> + <string name="drawer_texture_sharpness_note">En negativ mip-bias: skarpere teksturer, lidt flimmer. Auto følger, hvor meget sessionen forstørres til skærmen.</string> + <string name="drawer_touch_auto">Auto (%1$s)</string> + <string name="drawer_keyboard">Tastatur</string> + <string name="drawer_pc_keyboard">PC-tastatur</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">Anden skærm</string> + <string name="drawer_second_screen_mode">Tilstand</string> + <string name="drawer_second_screen_shows">Viser</string> + <string name="drawer_android_apps">Android-apps</string> + <string name="drawer_launch_app">Start en app</string> + <string name="common_hide">Skjul</string> + <string name="common_show">Vis</string> + <string name="drawer_no_apps">Ingen apps, der kan startes</string> + <string name="drawer_session_behavior">Sessionsopførsel</string> + <string name="drawer_suspend_hint">Gælder nu og for fremtidige sessioner i denne tilstand.</string> + <string name="drawer_next_session">Næste session</string> + <string name="drawer_support">Support</string> + <string name="drawer_logs">Sessionslogs</string> + <string name="drawer_logs_hint">Vedhæft til en fejlrapport</string> + <string name="drawer_share_logs">Del logs</string> + <string name="drawer_background">Baggrund</string> + <string name="drawer_stop">Stop</string> + <string name="drawer_stop_title">Afslut session?</string> + <string name="drawer_running_now">Kører nu</string> + <string name="drawer_no_game">Intet spil kører på en Proton</string> + <string name="drawer_selected_profile">Spilprofil</string> + <string name="drawer_selected_profile_none">Profildetaljer er ikke tilgængelige endnu.</string> + <string name="drawer_profile_refresh_failed_initial">Kunne ikke indlæse spilprofiler. Forsøger automatisk igen.</string> + <string name="drawer_profile_refresh_failed_stale">Kunne ikke opdatere spilprofiler. Viser de sidst tilgængelige data; forsøger automatisk igen.</string> + <string name="drawer_profile_inspect_hint">Vis og rediger de gemte indstillinger, der bruges ved næste start.</string> + <string name="drawer_profile_default">Bruger standardprofilen</string> + <string name="drawer_profile_custom">Brugerdefineret profil</string> + <string name="drawer_component_inherited">Bruger %1$s fra standardprofilen.</string> + <string name="drawer_game_components_note">Ændringer gemmes i %1$s’s profil og gælder, næste gang spillet starter.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d spiltilsidesættelse</item> + <item quantity="other">%1$d spiltilsidesættelser</item> + </plurals> + <string name="drawer_no_proton">Ingen Proton er installeret endnu.</string> + <string name="drawer_game_running">spil kører</string> + <string name="drawer_reapplied">genanvendt ved start %1$s</string> + <string name="drawer_original">Original</string> + <string name="drawer_components_note">Importer og slet på siden Komponenter.</string> + <string name="setup_check_done">Klar</string> + <string name="setup_check_warn">Kræver handling</string> + <string name="setup_check_busy">Arbejder</string> + <string name="setup_limit_adb_failed">Kontroller IP-adressen og porten for Trådløs fejlretning, eller par igen, hvis Android har fjernet denne enhed.</string> + <string name="setup_install">Installer</string> + <string name="setup_manage">Administrer</string> + <string name="setup_tab_overview">Oversigt</string> + <string name="setup_tab_controller">Controller</string> + <string name="setup_tab_session">Session</string> + <string name="setup_tab_launcher">Launcher</string> + <string name="setup_title">Opsætning</string> + <string name="setup_all_set">● Alt er klar</string> + <string name="setup_n_ready">%1$d af %2$d klar</string> + <string name="setup_system_check">Systemkontrol</string> + <string name="setup_gpu_supported">Enhed understøttet</string> + <string name="setup_gpu_untested">Utestet GPU</string> + <string name="setup_gpu_unsupported">GPU ikke understøttet</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">Steam kræver en Adreno-driver; %1$s kan vise en sort skærm</string> + <string name="setup_runtime">Linux-runtime</string> + <string name="setup_runtime_progress">%1$s · %2$d%%</string> + <string name="setup_runtime_missing">Ikke installeret · omkring 3 GB, installeres ved første Spil</string> + <string name="setup_installed">Installeret</string> + <string name="setup_runtime_update">%1$s · opdatering tilgængelig</string> + <string name="setup_runtime_current">%1$s · opdateret</string> + <string name="setup_limit">Grænse for underprocesser</string> + <string name="setup_limit_on">Til · Android kan lukke Steam. Det tager et minut at slå den fra.</string> + <string name="setup_limit_unset">Ikke angivet · Android kan lukke Steam. Det tager et minut at slå den fra.</string> + <string name="setup_limit_unknown">Kunne ikke kontrolleres · det tager et minut at slå den fra.</string> + <string name="setup_fix_it">Ret det</string> + <string name="setup_details">Detaljer</string> + <string name="setup_dev_options">Udviklerindstillinger</string> + <string name="setup_use_wireless">Brug Trådløs fejlretning</string> + <string name="setup_check_again">Kontroller igen</string> + <string name="setup_limit_turn_on">Slå grænsen til</string> + <string name="setup_account">Steam-konto</string> + <string name="setup_signed_in_offline">Logget ind som %1$s · offlinetilstand</string> + <string name="setup_signed_in">Logget ind som %1$s</string> + <string name="setup_sign_in">Tryk på Spil, og log ind på Steam</string> + <string name="setup_tools">Værktøjer</string> + <string name="setup_controller">Controller</string> + <string name="setup_controller_unavailable">Controllerindstillinger er ikke tilgængelige.</string> + <string name="setup_session">Session</string> + <string name="setup_logs">Sessionslogs</string> + <string name="setup_logs_hint">Gemt efter hver session</string> + <string name="setup_agent_commands">Fejlfindingsværktøjer</string> + <string name="setup_latest_logs">Seneste sessionslogs</string> + <string name="setup_saved_logs">Gemte sessionslogs</string> + <string name="setup_saved_logs_hint">Gemmer de nyeste %1$d</string> + <string name="setup_clear_logs">Ryd logs</string> + <string name="setup_offline">Offlinetilstand</string> + <string name="setup_sign_in_first">Log ind på Steam først</string> + <string name="setup_launcher">Launcher</string> + <string name="setup_language">Sprog</string> + <string name="setup_language_hint">Steam bruger det også, fra næste gang det starter.</string> + <string name="setup_language_system">Systemstandard (%1$s)</string> + <string name="setup_language_system_short">Systemstandard</string> + <string name="setup_app_scale">App-skalering</string> + <string name="setup_app_scale_hint">Tekst og kontroller i DroidDeck</string> + <string name="setup_app_scale_default">%1$d%% (standard)</string> + <string name="setup_theme">Tema</string> + <string name="setup_theme_hint">Launcherens udseende</string> + <string name="setup_home">Brug som startskærm</string> + <string name="setup_home_on">DroidDeck kan være Home-appen</string> + <string name="setup_home_off">DroidDeck tilbydes aldrig som Home-app</string> + <string name="setup_orientation">Retning</string> + <string name="setup_orientation_auto">Automatisk liggende</string> + <string name="setup_orientation_landscape">Liggende</string> + <string name="setup_orientation_reverse">Omvendt liggende</string> + <string name="setup_fullscreen">Fuldskærm</string> + <string name="setup_fullscreen_on">Skjuler Androids status- og navigationsbjælker</string> + <string name="setup_fullscreen_off">Viser Androids status- og navigationsbjælker</string> + <string name="setup_default_home">Standard Home-app</string> + <string name="setup_choose_home">Vælg en Home-app</string> + <string name="setup_choose">Vælg</string> + <string name="setup_linux_apps">Linux-apps (beta)</string> + <string name="setup_tool_files">Filer</string> + <string name="setup_tool_files_hint">Gennemse og administrer filer</string> + <string name="setup_tool_protons">Proton-versioner</string> + <string name="setup_tool_protons_hint">Installer ARM64-Proton-builds</string> + <string name="setup_tool_performance">Ydelse</string> + <string name="setup_tool_performance_hint">Tildeling af CPU-kerner</string> + <string name="setup_tool_roms">ROM-mappe</string> + <string name="setup_tool_roms_hint">Hvor emulatorspil gemmes</string> + <string name="setup_card_components">Komponenter</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Kontroller</string> + <string name="setup_card_controls_hint">Knapmapning</string> + <string name="store_tab_discover">Udforsk</string> + <string name="store_tab_search">Søg</string> + <string name="store_tab_installed">Installeret</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Butik</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak klar</string> + <string name="store_needs_runtime">Installer Linux-runtimen fra Opsætning først.</string> + <string name="store_setup_title">Sæt Flatpak op</string> + <string name="store_setup_text">Tilføjer Flatpak og forbinder det til Flathub (ca. 20 MB). Den første app downloader også sin runtime.</string> + <string name="store_setting_up">Sætter op…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Opdateringer</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d%%</string> + <string name="store_starting">Starter…</string> + <string name="store_categories">Kategorier</string> + <string name="store_unreachable">Kunne ikke nå Flathub. Kontroller din forbindelse, og prøv igen.</string> + <string name="store_try_again">Prøv igen</string> + <string name="store_search_label">Søg på Flathub</string> + <string name="store_search_placeholder">Firefox, spil, emulatorer…</string> + <string name="store_search">Søg</string> + <string name="store_all">Alle</string> + <string name="store_searching">Søger…</string> + <string name="store_search_hint">Søg efter navn, eller vælg en kategori. Kun ARM64-apps vises.</string> + <string name="store_no_results">Ingen ARM64-apps på Flathub matcher det.</string> + <string name="store_results">Resultater</string> + <string name="store_check_updates">Søg efter opdateringer</string> + <string name="store_update_all">Opdater alle (%1$d)</string> + <string name="store_all_current">● Alt er opdateret</string> + <string name="store_no_apps">Ingen apps endnu. Find nogle under Udforsk eller Søg.</string> + <string name="store_setup_first_note">Sæt Flatpak op for at installere apps.</string> + <string name="store_update_available_prefix">Opdatering tilgængelig · %1$s</string> + <string name="store_open">Åbn</string> + <string name="store_details">Detaljer</string> + <string name="store_remove">Fjern</string> + <string name="store_press_again">Tryk igen for at fjerne</string> + <string name="store_installing">Installerer…</string> + <string name="store_install">Installer</string> + <string name="store_setup_first">Sæt Flatpak op først</string> + <string name="store_download_size">%1$s download</string> + <string name="store_installed_chip">● Installeret</string> + <string name="store_details_failed">Kunne ikke indlæse denne apps Flathub-side.</string> + <string name="store_loading">Indlæser fra Flathub…</string> + <string name="store_no_arm">Ingen ARM64-build på Flathub, så den kan ikke installeres her.</string> + <string name="store_screenshots">Skærmbilleder</string> + <string name="store_about">Om</string> + <string name="store_app_id">App-id</string> + <string name="store_license">Licens</string> + <string name="store_runtime">Runtime</string> + <string name="store_installed_size">Installeret størrelse</string> + <string name="store_installed_size_value">%1$s (plus dens delte runtime)</string> + <string name="store_website">Websted</string> + <string name="store_verified">Verificeret</string> + <string name="upd_title">Opdateringer</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Kontrolleret %1$s</string> + <string name="upd_not_checked">Ikke kontrolleret endnu</string> + <string name="upd_perm_title">Lad DroidDeck opdatere sig selv</string> + <string name="upd_perm_text">På næste skærm skal du slå Tillad fra denne kilde til og derefter komme tilbage. Opdateringen fortsætter selv.</string> + <string name="upd_open_settings">Åbn indstillinger</string> + <string name="upd_not_now">Ikke nu</string> + <string name="upd_signed_status">Signeret anderledes</string> + <string name="upd_signed_headline">Kan ikke opdatere på plads</string> + <string name="upd_signed_detail">Denne kopi er ikke signeret med DroidDecks udgivelsesnøgle, så opdateringer kan ikke installeres over den. Afinstaller den for at skifte.</string> + <string name="upd_available">Opdatering tilgængelig</string> + <string name="upd_blocked_status">Kan ikke installere</string> + <string name="upd_switch">Klar til at skifte</string> + <string name="upd_ahead_status">Foran Stable</string> + <string name="upd_ahead_headline">Du er foran Stable</string> + <string name="upd_ahead_detail">Denne build er nyere end den seneste Stable. Du flyttes til Stable med dens næste udgivelse.</string> + <string name="upd_gone_status">Test afsluttet</string> + <string name="upd_gone_headline">Denne test er afsluttet</string> + <string name="upd_gone_detail">Dens rettelse er føjet ind eller opgivet. Følg Preview for de nyeste rettelser.</string> + <string name="upd_current_status">Opdateret</string> + <string name="upd_current_headline">Du har den nyeste %1$s</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Opdater%1$s</string> + <string name="upd_install_button">Installer%1$s</string> + <string name="upd_install_anyway">Installer Stable %1$s alligevel</string> + <string name="upd_follow_preview">Følg Preview</string> + <string name="upd_stop_session">Afslut den kørende session for at opdatere.</string> + <string name="upd_less">Mindre</string> + <string name="upd_more">Mere</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Ny Preview-build</string> + <string name="upd_new_test">PR #%1$d-testbuild</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">OPDATERINGSKANAL</string> + <string name="upd_stable">Stable</string> + <string name="upd_stable_hint">Testede udgivelser, til de fleste</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Preview</string> + <string name="upd_preview_hint">Nyeste rettelser, før Stable</string> + <string name="upd_preview_current">Opdateret</string> + <string name="upd_tests">Testbuilds</string> + <string name="upd_tests_hint">Prøv en rettelse, før den udgives</string> + <string name="upd_tests_none">Ingen lige nu</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Ikke bygget til denne kopi af DroidDeck</string> + <string name="upd_updated">Opdateret %1$s</string> + <string name="upd_test_name">PR #%1$d-testen</string> + <string name="upd_while_ago">et stykke tid siden</string> + <string name="upd_just_now">lige nu</string> + <plurals name="upd_tests_count"> + <item quantity="one">%1$d at prøve</item> + <item quantity="other">%1$d at prøve</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="one">Ændringer siden din build · %1$d build bagud</item> + <item quantity="other">Ændringer siden din build · %1$d builds bagud</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="one">Seneste ændringer · %1$d build</item> + <item quantity="other">Seneste ændringer · %1$d builds</item> + </plurals> + <string name="gate_title">Én Android-indstilling, der skal ændres</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Android lukker apps, der starter mange baggrundsprocesser, og Steam starter dusinvis. Indtil dette ændres, lukker spil uden en fejlmeddelelse.</string> + <string name="gate_fix_for_me">Ret det for mig</string> + <string name="gate_toggle_steps">Slå “Deaktiver begrænsninger for underprocesser” til, og kom så tilbage. Lad Udviklerindstillinger stå til, ellers slås kontakten fra igen.</string> + <string name="gate_open_dev">Åbn Udviklerindstillinger</string> + <string name="gate_no_toggle">Denne Android-version har ingen kontakt til det, så DroidDeck ændrer det via Trådløs fejlretning. Tager omkring et minut, ingen computer nødvendig.</string> + <string name="gate_needs_wifi">Trådløs fejlretning kræver Wi-Fi. Alle netværk fungerer; det kræver ikke internet.</string> + <string name="gate_needs_notifications">Parringskoden kommer i en notifikation. Tillad notifikationer for DroidDeck, eller brug Andre måder.</string> + <string name="gate_allow_notifications">Tillad notifikationer</string> + <string name="gate_step1">Åbn Indstillinger → Udviklerindstillinger → Trådløs fejlretning, og slå den til</string> + <string name="gate_step2">Tryk på “Par enhed med parringskode”</string> + <string name="gate_step3">Indtast koden i DroidDeck-notifikationen</string> + <string name="gate_waiting">Venter på parringsvinduet…</string> + <string name="gate_code_needed">Vindue fundet. Indtast dets kode i notifikationen.</string> + <string name="gate_status">Status</string> + <string name="gate_polling">Kontrollerer hvert 2. sekund. Denne side lukker, når den er fra.</string> + <string name="gate_other_ways">Andre måder</string> + <string name="gate_other_toggle">Trådløs fejlretning, manuel adresse, computer-ADB</string> + <string name="gate_other_no_toggle">Manuel adresse, computer-ADB, Udviklerindstillinger</string> + <string name="gate_use_wireless_instead">Brug Trådløs fejlretning i stedet</string> + <string name="gate_some_roms">Nogle ROM’er tilføjer kontakten alligevel. Kig efter “underprocesser” i Udviklerindstillinger.</string> + <string name="gate_manual_hint">Hvis notifikationen ikke virker, kan du selv indtaste vinduets adresse og kode.</string> + <string name="gate_enter_manually">Indtast adresse manuelt</string> + <string name="gate_from_computer">Fra en computer med ADB:</string> + <string name="gate_copy_command">Kopier kommando</string> + <string name="gate_choose_display">Vælg, hvilken skærm Android-indstillinger skal åbnes på.</string> + <string name="gate_main_screen">Hovedskærm</string> + <string name="wadb_finding_port">Parret. Finder porten for Trådløs fejlretning…</string> + <string name="wadb_enter_port">Parret. Indtast porten fra Indstillinger.</string> + <string name="wadb_applying">Enhed fundet. Anvender indstillingen…</string> + <string name="wadb_apply_failed">Kunne ikke anvendes automatisk. Kontroller adressen nedenfor, og prøv igen. %1$s</string> + <string name="wadb_title_wireless">Trådløs fejlretning</string> + <string name="wadb_title_done">Grænsen for underprocesser opdateret</string> + <string name="wadb_title">Ændr grænsen for underprocesser</string> + <string name="wadb_lede_compact0">Hvis indstillingen mangler, skal du parre via Trådløs fejlretning.</string> + <string name="wadb_lede_compact1">Parret. Kontroller IP-adressen og porten i Trådløs fejlretning.</string> + <string name="wadb_lede_done">Grænsen for underprocesser blev opdateret.</string> + <string name="wadb_lede0">Brug trin 2, hvis Udviklerindstillinger ikke har en indstilling for underprocesser. Åbn Udviklerindstillinger → Trådløs fejlretning → Par enhed med parringskode.</string> + <string name="wadb_lede1">Parret. Brug adressen nedenfor, eller opdater den fra Trådløs fejlretning → IP-adresse og port.</string> + <string name="wadb_lede_done_long">Grænsen for underprocesser blev opdateret via Trådløs fejlretning.</string> + <string name="wadb_confirmed_on">Android bekræftede, at grænsen er til.</string> + <string name="wadb_confirmed_off">Android bekræftede, at grænsen er fra.</string> + <string name="wadb_pair_group">2 · Par Trådløs fejlretning</string> + <string name="wadb_pair_address">Parringsvinduets IP-adresse og port</string> + <string name="wadb_pin">Parringskode (PIN)</string> + <string name="wadb_pin_placeholder">6 cifre</string> + <string name="wadb_pair">Par</string> + <string name="wadb_apply_group">2 · Anvend via Trådløs fejlretning</string> + <string name="wadb_connect_address">Forbindelsens IP-adresse og port</string> + <string name="wadb_pair_again">Par igen</string> + <string name="wadb_turn_off">Slå grænsen fra</string> + <string name="wadb_other_ways">Andre måder at ændre det</string> + <string name="wadb_dev_try_first">1 · Udviklerindstillinger (prøv først)</string> + <string name="wadb_restrict_on">Slå “Begræns underprocesser” til.</string> + <string name="wadb_restrict_off">Slå “Begræns underprocesser” fra.</string> + <string name="wadb_computer_last">3 · Computer-ADB (sidste udvej)</string> + <string name="wadb_only_if">Kun hvis Trådløs fejlretning ikke er tilgængelig.</string> + <string name="wadb_computer_group">3 · Computer-ADB (sidste udvej)</string> + <string name="ctrl_osc_hint">Hvornår berøringskontrollerne vises i en session</string> + <string name="ctrl_color">Farve</string> + <string name="ctrl_color_hint">Farvetone for knapperne på skærmen</string> + <string name="ctrl_custom">Brugerdefineret</string> + <string name="ctrl_opacity">Gennemsigtighed</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">Knapstørrelse</string> + <string name="ctrl_size_hint" formatted="false">100% er standardstørrelsen</string> + <string name="ctrl_stick_click">Stick-klik</string> + <string name="ctrl_stick_click_hint">Dobbelttryk og hold en stick nede for L3 eller R3</string> + <string name="ctrl_adaptive">Adaptive sticks</string> + <string name="ctrl_rumble">Spilvibration</string> + <string name="ctrl_steam_button">Steam-knap</string> + <string name="ctrl_qam_button">QAM-knap</string> + <string name="ctrl_keyboard_button">Tastaturknap</string> + <string name="ctrl_select_steam">Tryk på Select for at åbne Steam-menuen</string> + <string name="ctrl_select_steam_hint">I stedet for View/Select.</string> + <string name="setup_animations">Animationer</string> + <string name="ctrl_adaptive_hint">Sticks vises, hvor du rører skærmen, og skjules, når du slipper</string> + <string name="ctrl_layout">Layout</string> + <string name="ctrl_layout_custom">Brugerdefinerede positioner gemt</string> + <string name="ctrl_layout_default">Tilpasset denne skærm</string> + <string name="ctrl_edit">Rediger</string> + <string name="ctrl_reset">Nulstil</string> + <string name="ctrl_mapping">Knapmapning</string> + <string name="ctrl_mapping_default">Hver knap sender sit eget input</string> + <string name="ctrl_mapping_count">%1$d af %2$d knapper ommappet</string> + <string name="ctrl_configure">Konfigurer</string> + <string name="ctrl_reset_all">Nulstil controller</string> + <string name="ctrl_reset_all_hint">Gendanner alle controllerindstillinger og layoutet</string> + <string name="ctrl_mapping_lede">Hvad hver knap på skærmen sender til spillet. Skjult fjerner knappen.</string> + <string name="ctrl_onscreen_buttons">Knapper på skærmen</string> + <string name="ctrl_defaults">Standarder</string> + <string name="ctrl_reset_mapping">Nulstil mapning</string> + <string name="content_android_apps">Android-apps</string> + <string name="content_apps_count">%1$d apps</string> + <string name="content_no_android_apps">Ingen Android-apps, der kan startes, fundet.</string> + <string name="content_desktop_installed">● Skrivebord installeret</string> + <string name="content_desktop_first_open">Installeres ved første åbning</string> + <string name="content_available">Tilgængelig til installation</string> + <string name="content_not_installed">Ikke installeret.</string> + <string name="content_not_installed_suffix">%1$s · ikke installeret</string> + <string name="content_open_named">Åbn %1$s</string> + <string name="content_games">Spil</string> + <string name="content_choose_roms">Vælg en ROM-mappe.</string> + <string name="content_retroarch_roms">Gå til /root/ROMs i RetroArch.</string> + <string name="content_add_roms">Tilføj %1$s-spil til ROMs/%2$s.</string> + <string name="content_install_named">Installer %1$s</string> + <string name="content_stop_to_install">Afslut session for at installere</string> + <string name="content_runtime_required">Runtime påkrævet</string> + <string name="content_loading_details">Indlæser installationsdetaljer…</string> + <string name="content_details_unavailable">Installationsdetaljer er ikke tilgængelige. Prøv igen senere.</string> + <string name="content_needs_runtime">Installer Linux-runtimen fra Opsætning først.</string> + <string name="content_stop_first">Afslut den kørende session for at installere skrivebordsapps.</string> + <string name="content_install_into">Installerer %1$s i Linux-skrivebordet.</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">Hvis den skjules, fjernes den fra Skrivebord. Dens filer forbliver i Linux-runtimen.</string> + <string name="content_rom_gone">Det spil er ikke længere i ROM-mappen.</string> + <string name="content_desktop_system">Skrivebord · %1$s</string> + <string name="content_launch_in">Start i %1$s</string> + <string name="content_also_in">Også i %1$s</string> + <string name="content_page_gone">Den side findes ikke længere. Vælg et afsnit til venstre.</string> + <string name="content_open_desktop">Åbn skrivebord</string> + <string name="content_install_open_desktop">Installer og åbn skrivebord</string> + <string name="desktop_remove_title">Afinstaller skrivebord</string> + <string name="desktop_remove_text">Fjerner skrivebordet og dets medfølgende apps. Spil, tilføjede apps, indstillinger og delte biblioteker bevares.</string> + <string name="desktop_removing">Fjerner skrivebord</string> + <string name="desktop_removed">Skrivebord fjernet</string> + <string name="desktop_cleaning_legacy">Fjerner gamle LXQt-filer</string> + <string name="desktop_removal_unknown">Opdater DroidDeck, før du fjerner denne skrivebordsversion.</string> + <string name="content_linux_desktop">Linux-skrivebord</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox og dine emulatorer</string> + <string name="games_empty">Spil, du installerer i Steam, vises her efter sessionen.</string> + <string name="games_play_steam">Spil Steam</string> + <string name="games_launch_settings">Startindstillinger</string> + <string name="games_launch">Start</string> + <string name="games_files">Spilfiler</string> + <string name="games_prefix">Proton-prefix</string> + <string name="games_saves_looking">Søger efter gemte spil…</string> + <string name="games_saves_none">Ingen savemappe fundet ved navn. Eksport tager hele brugermappen.</string> + <string name="games_saves_folders">%1$d savemapper</string> + <string name="games_saves_summary">%1$s · %2$d filer · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">Administrer gemte spil</string> + <string name="games_saves">Gemte spil</string> + <string name="games_import_saves">Importer gemte spil…</string> + <string name="games_export_gamehub">Eksporter som GameHub-zip…</string> + <string name="games_export_winlator">Eksporter som Winlator-zip…</string> + <string name="games_open_save_folder">Åbn savemappe</string> + <string name="games_never_played">Aldrig spillet</string> + <string name="games_runtime_busy_percent">Runtime optaget · %1$d%%</string> + <string name="games_runtime_busy">Runtime optaget</string> + <string name="games_runtime_first_play">Runtime installeres ved første Spil</string> + <string name="games_runtime_update">Runtimeopdatering tilgængelig</string> + <string name="games_runtime_ready">● Runtime klar</string> + <string name="games_last_played">Sidst spillet %1$s</string> + <string name="games_internal">Internt lager</string> + <string name="games_added">Tilføjet spil</string> + <string name="games_custom">Brugerdefineret spil</string> + <string name="games_store_storage">Spillager</string> + <string name="gpu_auto_hint">Det bedste par til denne GPU, holdt opdateret</string> + <string name="gpu_manual_hint">Vælg et par selv</string> + <string name="gpu_checking">Søger efter drivere…</string> + <string name="gpu_none_listed">Ingen drivere på listen endnu. Opdater for at søge efter dem.</string> + <string name="gpu_hide_other">Skjul drivere til andre GPU’er</string> + <string name="gpu_show_other">Vis drivere til andre GPU’er</string> + <string name="gpu_advanced">Avanceret</string> + <string name="gpu_set_with_display">Angives sammen med skærmdriveren: %1$s dækker begge.</string> + <string name="gpu_set_with_runtime">Angives sammen med runtimedriveren: %1$s dækker begge.</string> + <string name="gpu_runtime_hint">Bruges af spil og skrivebordet.</string> + <string name="gpu_runtime_default">Runtime-standard</string> + <string name="gpu_display_hint">Tegner billeder til skærmen. Gælder, når DroidDeck genstarter.</string> + <string name="gpu_auto_by_gpu">Auto – valgt efter GPU</string> + <string name="gpu_manage">Download, importer eller slet…</string> + <string name="gpu_in_use_caps">I BRUG</string> + <string name="gpu_builtin">Driverne indbygget i DroidDeck</string> + <string name="gpu_matched_pair">Runtime- og skærmdrivere angivet sammen som et matchende par.</string> + <string name="gpu_recommended_for">Anbefalet til denne GPU: %1$s.</string> + <string name="gpu_refresh_hint">Opdater for at søge efter drivere til denne GPU.</string> + <string name="gpu_recommended_caps">ANBEFALET</string> + <string name="gpu_unavailable">Ikke tilgængelig</string> + <string name="load_runtime">Linux-runtime</string> + <string name="load_runtime_detail">Gør Linux-runtimen klar</string> + <string name="load_session">Session</string> + <string name="load_session_detail">Starter skærm og lyd</string> + <string name="load_steam">Steam-klient</string> + <string name="load_steam_detail">Kontrollerer Steam og dets kompatibilitetsværktøjer</string> + <string name="load_opening_steam">Åbner Steam</string> + <string name="load_opening_steam_detail">Venter på Steams første billede</string> + <string name="load_desktop">Skrivebord</string> + <string name="load_desktop_detail">Gør skrivebordet klar</string> + <string name="load_opening">Åbner</string> + <string name="load_opening_detail">Venter på det første billede</string> + <string name="load_starting_steam">Starter Steam</string> + <string name="load_ended">Sessionen blev afsluttet</string> + <string name="load_step">Trin %1$d af %2$d</string> + <string name="load_steam_paused">Steam er sat på pause</string> + <string name="deck_tab_grips">Greb</string> + <string name="deck_tab_left">Venstre pad</string> + <string name="deck_tab_right">Højre pad</string> + <string name="deck_tab_both">Begge</string> + <string name="deck_tab_gamepad">Gamepad</string> + <string name="deck_surface_stick">Stick</string> + <string name="deck_surface_trackpad">Trackpad</string> + <string name="deck_surface_switch">Skift mellem stick og trackpad</string> + <string name="deck_more_controls">Flere Deck-kontroller</string> + <string name="deck_panels">PANELER</string> + <string name="deck_close">Luk kontrollerne til den anden skærm</string> + <string name="deck_open_steam_menu">Åbn Steam-menuen</string> + <string name="deck_left_upper">Venstre · øverst</string> + <string name="deck_left_lower">Venstre · nederst</string> + <string name="deck_right_upper">Højre · øverst</string> + <string name="deck_right_lower">Højre · nederst</string> + <string name="deck_click">Klik</string> + <string name="deck_right_trackpad">Højre trackpad</string> + <string name="deck_left_trackpad">Venstre trackpad</string> + <string name="second_trackpad">Trackpad</string> + <string name="second_show_keyboard">Vis tastatur</string> + <string name="second_hide_keyboard">Skjul tastatur</string> + <string name="second_terminal">Linux-terminal</string> + <string name="second_terminal_not_ready">Linux-sessionen er ikke klar til en terminal endnu.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Spil</string> + <string name="rail_desktop">Skrivebord</string> + <string name="rail_store">Butik</string> + <string name="rail_components">Komponenter</string> + <string name="rail_apps">Apps</string> + <string name="rail_setup">Opsætning</string> + <string name="rail_updates">Opdateringer</string> + <string name="rail_percent">%1$d%%</string> + <string name="rail_resume_named">Genoptag %1$s</string> + <string name="roms_text">Vises som /root/ROMs i hver session. Intern lagerplads er på /root/Storage.</string> + <string name="roms_none">Ingen mappe valgt</string> + <string name="roms_applies">Gælder fra næste session. SD-kort fungerer.</string> + <string name="roms_choose">Vælg mappe</string> + <string name="roms_change">Skift mappe</string> + <string name="proton_lede">Installer en ARM64 Proton-build, og vælg den derefter for hvert spil i Steam → Egenskaber → Kompatibilitet.</string> + <string name="proton_stop_first">Stop den kørende session for at installere eller fjerne et kompatibilitetsværktøj.</string> + <string name="proton_available">Tilgængelige builds</string> + <string name="proton_installed">Installeret %1$s</string> + <string name="proton_queued">Afventer fra en tidligere anmodning</string> + <string name="proton_not_installed">Ikke installeret</string> + <string name="proton_update_available">Opdatering tilgængelig → %1$s</string> + <string name="proton_remove_older">Fjern gamle %1$s-versioner</string> + <string name="proton_remove_older_body">Fjerner %1$s. Spil, der er sat til disse versioner, skal skiftes til en anden Proton.</string> + <string name="proton_remove_body">Spil, der er sat til denne version, skal skiftes til en anden Proton.</string> + <string name="proton_install_now">Installer nu</string> + <string name="proton_cancel_queued">Annuller afventende</string> + <string name="pckb_android">Android-tastatur</string> + <string name="pckb_hide">Skjul ✕</string> + <string name="main_launch_named">Start %1$s</string> + <string name="main_choose_screen">Vælg en skærm. Sekundær: %1$s.</string> + <string name="main_secondary_gone">Den sekundære skærm er ikke længere tilgængelig.</string> + <string name="main_primary">Primær skærm</string> + <string name="main_secondary">Sekundær skærm</string> + <string name="widgets_prev_tab">Forrige fane</string> + <string name="widgets_next_tab">Næste fane</string> + <string name="widgets_on">Til</string> + <string name="widgets_all_n">Alle %1$d</string> + <string name="widgets_none">Ingen</string> + <string name="widgets_n_of_m">%1$d af %2$d</string> + <string name="widgets_off">Fra</string> + <string name="screen_bottom">Nederste skærm</string> + <string name="game_env_applies">Gælder fra næste spilstart. Et spils profil tilsidesætter den fælles.</string> + <string name="game_env_other">Et andet spil via app-id…</string> + <string name="game_env_done">Udført</string> + <string name="game_env_default">Standard</string> + <string name="game_env_off">Fra</string> + <string name="game_env_change">Skift…</string> + <string name="game_env_change_title">Skift variabel</string> + <string name="game_env_restore_default">Tilbage til standard</string> + <string name="game_env_eyebrow">Proton-spil</string> + <string name="pip_title">Billede i billede</string> + <string name="pip_auto">Gå automatisk til billede i billede</string> + <string name="unfolded_controls">Kontroller på den udfoldede skærm</string> + <string name="unfolded_controls_hint">Når skærmen er foldet ud, ligger spillet over Steam Decks trackpads og grebsknapper. Når den er foldet sammen, fylder spillet hele skærmen.</string> + <string name="pip_suspended">Sat i bero</string> + <string name="pip_not_ready">Billede i billede er tilgængeligt, når sessionen er klar.</string> + <string name="pip_permission">Tillad billede i billede for DroidDeck i Android-indstillingerne.</string> + <string name="pip_unavailable">Android kunne ikke åbne billede i billede.</string> + <string name="game_shortcut">Genvej</string> + <string name="game_shortcut_add">Tilføj til hjemmeskærm</string> + <string name="game_shortcut_choose">Vælg spil</string> + <string name="game_shortcut_scanning">Scanner installerede spil…</string> + <string name="game_link_copy">Kopier startlink</string> + <string name="game_link_copied">Startlink kopieret</string> + <string name="game_shortcut_unsupported">Denne launcher kan ikke tilføje genveje. Brug Kopier startlink i stedet.</string> + <string name="game_link_invalid">Ugyldigt startlink til spil.</string> + <string name="game_link_missing">Dette spil findes ikke længere i dit DroidDeck-bibliotek.</string> + <string name="game_link_session_busy">Stop den aktuelle session, før du starter et Steam-spil.</string> + <string name="game_link_failed">Kunne ikke sende spillet til Steam. Prøv at starte det igen.</string> + <string name="setup_retry_removal">Prøv fjernelse igen</string> + <string name="runtime_removing">Fjerner</string> + <string name="runtime_removal_incomplete">Fjernelse ikke fuldført</string> + <string name="game_file_export">Eksporter spilfil</string> + <string name="game_file_sync">Synkroniser alle spilfiler til mappe</string> + <string name="game_file_stop_sync">Stop synkronisering af spilfiler</string> + <string name="game_file_folder">Vælg en mappe til frontend-spilfiler</string> + <string name="game_file_exported">Spilfiler gemt i %1$s</string> + <string name="game_frontend_files">Frontend-filer</string> + + <string name="main_wait_runtime">Vent til runtime-handlingen er færdig</string> + <string name="main_wait_proton_install">Vent til installationen af kompatibilitetsværktøjet er færdig</string> + <string name="main_wait_desktop_install">Vent til installationen af skrivebordsappen er færdig</string> + <string name="main_wait_install_game">Vent til installationen er færdig, før du starter et spil</string> + <string name="main_saves_close_session">Luk Steam-sessionen først, så spillet ikke overskriver importen</string> + <string name="main_saves_importing">Importerer til %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="one">Importerede %1$d fil fra %2$s til %3$s</item> + <item quantity="other">Importerede %1$d filer fra %2$s til %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">Importerede %1$d fil fra %2$s til %3$s. Gamle gemte spil sikkerhedskopieret til Download/DroidDeck/Saves/backups</item> + <item quantity="other">Importerede %1$d filer fra %2$s til %3$s. Gamle gemte spil sikkerhedskopieret til Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Vælg en zip med gemte spil til %1$s</string> + <string name="main_saves_exporting">Eksporterer %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="one">Eksporterede %1$d fil som %2$s: %3$s</item> + <item quantity="other">Eksporterede %1$d filer som %2$s: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Vælg, hvor %1$s skal gemmes (%2$s)</string> + <string name="main_action_failed">%1$s mislykkedes: %2$s</string> + <string name="main_no_logs_yet">Ingen sessionslogs endnu: kør en session først.</string> + <string name="main_no_logs_to_clear">Ingen sessionslogs at rydde.</string> + <plurals name="main_logs_cleared"> + <item quantity="one">Ryddede %1$d sessionslogmappe.</item> + <item quantity="other">Ryddede %1$d sessionslogmapper.</item> + </plurals> + <string name="main_wadb_pair_failed">Parring til trådløs fejlretning mislykkedes</string> + <string name="main_wadb_command_failed">Kommando til trådløs fejlretning mislykkedes</string> + <string name="main_wadb_unavailable">Trådløs fejlretning er ikke tilgængelig</string> + <string name="main_adb_copied">ADB-kommando kopieret</string> + <string name="main_pick_roms">Vælg ROMs-mappen</string> + <string name="main_non_adreno_title">Ikke en Adreno-GPU</string> + <string name="main_non_adreno_text">Turnip understøtter Adreno-GPU\'er. På %1$s kan Steam vise en sort skærm. Download: %2$.0f MB.</string> + <string name="main_install_anyway">Installer alligevel</string> + <string name="main_remove_runtime_title">Fjern Linux-runtime</string> + <string name="main_remove_runtime_text">Dette sletter runtime\'en, Steam-klienten i den og alle spil installeret der.</string> + <string name="main_applied">Anvendt: %1$s</string> + <string name="android_app_open_failed_primary">Kunne ikke åbne %1$s på den primære skærm</string> + <string name="android_app_open_failed_on">Kunne ikke åbne %1$s på %2$s</string> + <string name="android_app_open_failed_display">Kunne ikke åbne %1$s på skærm %2$d</string> + <string name="comp_busy_importing">Importerer</string> + <string name="comp_busy_swapping">Skifter</string> + <string name="comp_busy_restoring">Gendanner</string> + <string name="comp_busy_cancelling">Annullerer</string> + <string name="comp_busy_deleting">Sletter</string> + <string name="comp_cannot_read">kan ikke læse filen</string> + <string name="comp_imported">Importerede %1$s</string> + <string name="comp_swap_cancelled">Den ventende udskiftning blev annulleret.</string> + <string name="comp_pick_wcp">Vælg en komponentpakke (-linux .wcp)</string> + <string name="comp_nightlies_unreachable">Nightlies kunne ikke nås</string> + <string name="comp_stored">Gemte %1$s</string> + <string name="comp_download_failed">Download mislykkedes: %1$s</string> + <string name="drivers_pick_linux">Vælg en Linux-runtime-driver (-Linux-zip eller Android + Linux-bundle)</string> + <string name="drivers_pick_android">Vælg en skærmdriver (AdrenoTools-zip eller Android + Linux-bundle)</string> + <string name="drivers_pick_any">Vælg en driver-zip eller en Android + Linux-bundle</string> + <string name="main_pkg_problem">%1$s: %2$s</string> + <string name="main_removing_named">Fjerner %1$s…</string> + <string name="main_forgetting_named">Glemmer %1$s…</string> + <string name="main_pick_game_storage">Vælg mappen til spillagring</string> + <string name="main_pick_decky_zip">Vælg en Decky-plugin-ZIP</string> + <string name="main_storage_not_usable">Ikke brugbar: %1$s</string> + <string name="main_running_program">Program</string> + <string name="main_removing_runtime">Fjerner Linux-runtime</string> + <string name="main_settings_main_screen">Kunne ikke åbne Indstillinger på den nederste skærm; åbnede den på hovedskærmen.</string> + <string name="main_open_dev_options">Åbn Indstillinger for udviklere i Indstillinger</string> + + <string name="upd_unknown_apps_failed">Kunne ikke åbne Androids indstilling ”Installer ukendte apps”</string> + <string name="upd_couldnt_check">Kunne ikke kontrollere: %1$s</string> + <string name="upd_no_download">Denne build har ingen download til denne kopi af DroidDeck</string> + <string name="upd_stop_first">Stop den kørende session først, og opdater derefter</string> + <string name="upd_stage_downloading">Downloader</string> + <string name="upd_stage_installing">Installerer</string> + <string name="upd_stage_confirm">Venter på din bekræftelse</string> + <string name="upd_failed">Opdateringen mislykkedes</string> + <string name="upd_not_offered">Android tilbød ikke at installere opdateringen</string> + + <string name="decky_install_failed">Decky-installation mislykkedes: %1$s</string> + <string name="decky_plugin_starting">Starter import af plugin…</string> + <string name="decky_plugin_failed">Import af plugin mislykkedes: %1$s</string> + <string name="decky_plugin_installed">Plugin installeret. Genstart Steam for at indlæse det.</string> + + <string name="drivers_not_checked">Ikke kontrolleret endnu - tryk på opdater for at søge efter nye drivere</string> + <string name="drivers_bundle_sets_display">Android + Linux: angiver skærmdriveren samtidig</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: angiver runtime-driveren samtidig</string> + <string name="drivers_runtime_builtin">Turnip indbygget i runtime\'en</string> + <string name="drivers_system_vulkan">system-Vulkan: ingen medfølgende build til denne GPU</string> + <string name="drivers_bundled">%1$s (medfølgende)</string> + <string name="drivers_manual">Manuel</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">Kun den ene halvdel er udgivet lige nu</string> + <string name="drivers_pair_size">%1$.0f MB for begge</string> + <string name="drivers_none_to_set">Ingen drivere at angive: %1$s</string> + <string name="drivers_checking_latest">Søger efter de nyeste drivere…</string> + <string name="drivers_up_to_date_check_failed">Opdateret (kunne ikke kontrollere: %1$s)</string> + <string name="drivers_recommended_unpublished">De anbefalede drivere er ikke udgivet lige nu</string> + <string name="drivers_downloading_pair">Downloader %1$s %2$s…</string> + <string name="drivers_updated_to">Opdateret til %1$s %2$s.</string> + <string name="drivers_updated_to_restart">Opdateret til %1$s %2$s. Skærmdriveren træder i kraft, når DroidDeck genstarter.</string> + <string name="drivers_switched_to">Skiftet til %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Skiftet til %1$s %2$s. Skærmdriveren træder i kraft, når DroidDeck genstarter.</string> + <string name="drivers_using">Bruger %1$s %2$s.</string> + <string name="drivers_using_restart">Bruger %1$s %2$s. Skærmdriveren træder i kraft, når DroidDeck genstarter.</string> + <string name="drivers_not_a_driver">Ikke en driver-zip: hverken en AdrenoTools-driver, en -Linux-Turnip eller en Android + Linux-bundle</string> + <string name="drivers_import_failed">Import mislykkedes: %1$s</string> + <string name="drivers_imported_unnamed">Importeret driver</string> + <string name="drivers_using_bundle">Bruger %1$s som både runtime- og skærmdriver.</string> + <string name="drivers_using_bundle_restart">Bruger %1$s som både runtime- og skærmdriver. Skærmdriveren træder i kraft, når DroidDeck genstarter.</string> + <string name="drivers_deleted_bundle">Slettede %1$s (begge drivere)</string> + <string name="drivers_deleted">Slettede %1$s</string> + <string name="drivers_release_status">Nyeste: %1$s · kontrolleret %2$s</string> + <string name="drivers_release_status_failed">Nyeste: %1$s · %2$s kunne ikke nås · kontrolleret %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">%1$d min siden</item> + <item quantity="other">%1$d min siden</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">%1$d t siden</item> + <item quantity="other">%1$d t siden</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">%1$d dag siden</item> + <item quantity="other">%1$d dage siden</item> + </plurals> + <string name="drivers_checking_sources">Kontrollerer Banners-Turnip, WinNative og DroidDeck…</string> + <string name="drivers_check_failed">kontrol mislykkedes</string> + <string name="drivers_installed_pick_either">Installeret %1$s - vælg den i en af listerne for at angive begge drivere</string> + <string name="drivers_installed_pick">Installeret %1$s - vælg den i menuen</string> + + <string name="session_wait_removal">Vent til fjernelse af runtime er færdig</string> + <string name="session_title_game">Spil</string> + <string name="session_touch_auto_touchpad">touchpad</string> + <string name="session_touch_auto_direct">direkte</string> + <string name="session_swap_failed">Udskiftning mislykkedes: %1$s</string> + <string name="session_profile_save_failed">Lagring af profil mislykkedes: %1$s</string> + <string name="session_desktop_paused">Skrivebordet er sat på pause</string> + <string name="session_named_paused">%1$s er sat på pause</string> + <string name="session_program_paused">Programmet er sat på pause</string> + <string name="session_starting_desktop">Starter skrivebordet</string> + <string name="session_starting_named">Starter %1$s</string> + <string name="session_starting_program">Starter programmet</string> + <string name="session_no_logs">Ingen logs for denne session.</string> + <string name="session_logs_archive_failed">Kunne ikke oprette arkivet med sessionslogs.</string> + <string name="session_logs_share_failed">Kunne ikke dele sessionslogs.</string> + <string name="session_starting_session">Starter sessionen…</string> + <string name="session_runtime_downloading">downloader Linux-runtime</string> + <string name="session_runtime_downloading_mb">downloader Linux-runtime · %1$d af %2$d MB</string> + <string name="session_runtime_checking">kontrollerer Linux-runtime</string> + <string name="session_runtime_unpacking">udpakker Linux-runtime</string> + <string name="session_desktop_downloading">downloader skrivebordet</string> + <string name="session_desktop_downloading_mb">downloader skrivebordet · %1$d af %2$d MB</string> + <string name="session_desktop_checking">kontrollerer skrivebordet</string> + <string name="session_desktop_unpacking">udpakker skrivebordet</string> + <string name="session_proton_downloading">downloader Proton Experimental (ARM64)</string> + <string name="session_proton_downloading_mb">downloader Proton Experimental (ARM64) · %1$d af %2$d MB</string> + <string name="session_proton_checking">kontrollerer Proton Experimental (ARM64)</string> + <string name="session_proton_unpacking">udpakker Proton Experimental (ARM64)</string> + <string name="session_runtime_catalog_unreachable">Kunne ikke nå runtime-kataloget. Kontroller forbindelsen, og tryk på Spil igen.</string> + <string name="session_runtime_install_failed">Linux-runtime blev ikke installeret. Kontroller runtime i Opsætning, og tryk på Spil igen.</string> + <string name="session_desktop_install_failed">Skrivebordet blev ikke installeret (%1$s). Kontroller forbindelsen, og tryk på Skrivebord igen.</string> + <string name="session_ended_named">%1$s stoppede uventet. Del logs med en fejlrapport, eller prøv igen.</string> + <string name="session_ended_program">Programmet stoppede uventet. Del logs med en fejlrapport, eller prøv igen.</string> + <string name="session_ended_desktop">Skrivebordet stoppede uventet. Del logs med en fejlrapport, eller prøv igen.</string> + <string name="session_ended_detail">Afslutningsstatus %1$d · %2$s</string> + <string name="session_ended_no_log">ingen log</string> + <plurals name="session_hint_enosys"> + <item quantity="one">Loggen viser %1$d \"Function not implemented\"-fejl: proots seccomp-acceleration fejler i en hjælpeproces på denne enhed. Prøv %2$s → \"%3$s\" (eller \"%4$s\"), og start igen.</item> + <item quantity="other">Loggen viser %1$d \"Function not implemented\"-fejl: proots seccomp-acceleration fejler i en hjælpeproces på denne enhed. Prøv %2$s → \"%3$s\" (eller \"%4$s\"), og start igen.</item> + </plurals> + + <string name="mode_decky_plugins">Plugins</string> + <string name="mode_decky_plugins_hint">Installer en plugin-ZIP til den næste Steam-session.</string> + <string name="mode_decky_plugin_downloading">Downloader…</string> + <string name="mode_decky_plugin_install_zip">Installer fra ZIP</string> + <string name="store_cat_games">Spil</string> + <string name="store_cat_audio_video">Lyd og video</string> + <string name="store_cat_graphics">Grafik</string> + <string name="store_cat_internet">Internet</string> + <string name="store_cat_office">Kontor</string> + <string name="store_cat_developer">Udvikler</string> + <string name="store_cat_education">Uddannelse</string> + <string name="store_cat_science">Videnskab</string> + <string name="store_cat_system">System</string> + <string name="store_cat_utilities">Hjælpeprogrammer</string> + <string name="store_section_popular">Populære</string> + <string name="store_section_trending">Trends</string> + <string name="store_section_new">Nye</string> + <string name="store_section_updated">Opdaterede</string> + <string name="store_section_loading">indlæser…</string> + <string name="store_flatpak_setup">Flatpak-opsætning</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Kunne ikke søge efter opdateringer på Flathub</string> + <string name="content_rom_installed">installeret</string> + <string name="content_rom_folder">mappe</string> + <string name="theme_graphite">Grafit</string> + <string name="theme_graphite_detail">Grafitoverflader, med logoets egen kugleblå farve til alt, der er valgt, fokuseret eller trykket.</string> + <string name="theme_paper">Papir på sort</string> + <string name="theme_paper_detail">Hvid er primærfarven, ikonets blå er signalfarven til streger, ringe og prikker.</string> + <string name="theme_icon_blue">Ikonblå</string> + <string name="theme_icon_blue_detail">Sort baggrund, hvid skrift, blå til alt, der er valgt, fokuseret eller trykket.</string> + <string name="theme_electric">Elektrisk marineblå</string> + <string name="theme_electric_detail">Den blå er klarere, på overflader der trækker mod marineblåt i stedet for gråt.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Space</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <string name="game_sync_err_runtime">Installer runtime først</string> + <string name="game_sync_err_steam_library">Steam-biblioteket er ikke tilgængeligt</string> + <string name="game_sync_err_previous_sd">Det tidligere SD-bibliotek er ikke tilgængeligt</string> + <string name="game_sync_err_game_storage">Spillagring er ikke tilgængelig</string> + <string name="game_sync_err_added_games">Mappen med tilføjede spil er ikke tilgængelig</string> + <string name="game_sync_err_manifests">Steam-manifester er ikke tilgængelige</string> + <string name="content_system_many">Mange systemer</string> + <string name="fm_drive_internal">Intern</string> + <string name="fm_drive_linux">Linux-system</string> + <string name="fm_drive_sd">SD-kort</string> + <string name="fm_drive_storage">Lagerplads</string> + <string name="fm_select">Vælg</string> + <string name="fm_properties">Egenskaber</string> + <string name="fm_favorite_add">Tilføj til Favoritter</string> + <string name="fm_favorite_remove">Fjern fra Favoritter</string> + <string name="fm_rename">Omdøb</string> + <string name="fm_copy">Kopier</string> + <string name="fm_cut">Klip</string> + <string name="fm_actions">Handlinger</string> + <string name="fm_favorites">Favoritter</string> + <string name="fm_pin_current">Fastgør aktuel mappe</string> + <string name="fm_no_favorites">Ingen favoritter endnu - fastgør en mappe via dens ⋮-menu for hurtigt at komme tilbage hertil.</string> + <string name="fm_unpin_favorite">Fjern fra favoritter</string> + <string name="fm_size_zero">0 byte</string> + <string name="fm_unit_bytes">byte</string> + <string name="fm_prop_location">Placering</string> + <string name="fm_prop_type">Type</string> + <string name="fm_prop_size">Størrelse</string> + <string name="fm_prop_modified">Ændret</string> + <string name="fm_type_folder">Mappe</string> + <string name="fm_type_file">Fil</string> + <string name="fm_type_ext_file">%1$s-fil</string> + <string name="fm_read_only">Skrivebeskyttet</string> + <string name="fm_read_only_hint">Forhindrer spil (og Wine) i at overskrive eller slette denne fil.</string> + <string name="fm_read_only_failed">Kunne ikke ændre Skrivebeskyttet</string> + <string name="fm_hidden">Skjult</string> + <string name="fm_hidden_unsupported">Denne lagerplads kan ikke gemme det skjulte flag.</string> + <string name="fm_hidden_hint">Markerer filen som skjult i Windows (Wines skjulte attribut).</string> + <string name="fm_hidden_failed">Kunne ikke ændre Skjult på denne lagerplads</string> + <string name="fm_done">Udført</string> + <string name="fm_op_deleting">Sletter...</string> + <string name="fm_delete_failed">Sletning mislykkedes</string> + <string name="fm_op_moving">Flytter...</string> + <string name="fm_op_copying">Kopierer...</string> + <string name="fm_op_moving_item">Flytter - %1$s</string> + <string name="fm_op_copying_item">Kopierer - %1$s</string> + <string name="fm_op_moving_batch">Flytter %1$d/%2$d - %3$s</string> + <string name="fm_op_copying_batch">Kopierer %1$d/%2$d - %3$s</string> + <string name="fm_paste_failed">%1$d gennemført, %2$d mislykkedes</string> + <string name="fm_paste_skipped">%1$d gennemført, %2$d sprunget over</string> + <plurals name="fm_paste_moved"> + <item quantity="one">%1$d element flyttet</item> + <item quantity="other">%1$d elementer flyttet</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">%1$d element kopieret</item> + <item quantity="other">%1$d elementer kopieret</item> + </plurals> + <string name="fm_already_exists">\"%1$s\" findes allerede</string> + <string name="fm_op_renaming">Omdøber...</string> + <string name="fm_rename_failed">Omdøbning mislykkedes</string> + <string name="fm_op_creating_folder">Opretter mappe...</string> + <string name="fm_create_folder_failed">Kunne ikke oprette mappe</string> + <string name="fm_new_folder">Ny mappe</string> + <string name="fm_folder_name">Mappenavn</string> + <string name="fm_create">Opret</string> + <string name="fm_new_name">Nyt navn</string> + <string name="fm_delete_title">Slet?</string> + <string name="fm_delete_text">Slet \"%1$s\" permanent?</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">Slet %1$d element?</item> + <item quantity="other">Slet %1$d elementer?</item> + </plurals> + <string name="fm_cannot_undo">Dette kan ikke fortrydes.</string> + <plurals name="fm_and_more"> + <item quantity="one">…og %1$d til</item> + <item quantity="other">…og %1$d til</item> + </plurals> + <string name="fm_op_deleting_batch">Sletter %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">%1$d kunne ikke slettes</item> + <item quantity="other">%1$d kunne ikke slettes</item> + </plurals> + <string name="fm_conflict_merge_hint">Sammenlægning tilføjer og udskifter filer i den eksisterende mappe.</string> + <string name="fm_conflict_overwrite_hint">Overskriv udskifter den eksisterende fil.</string> + <string name="fm_conflict_apply_all">Anvend på alle konflikter</string> + <string name="fm_conflict_merge">Sammenlæg</string> + <string name="fm_conflict_overwrite">Overskriv</string> + <string name="fm_conflict_keep_both">Behold begge</string> + <string name="fm_conflict_skip">Spring over</string> + <string name="fm_select_this_folder">Vælg denne mappe</string> + <string name="fm_back">Tilbage</string> + <string name="fm_drive_not_readable">%1$s er monteret, men kan ikke læses lige nu</string> + <string name="fm_list_view">Listevisning</string> + <string name="fm_grid_view">Gittervisning</string> + <string name="fm_search">Søg</string> + <string name="fm_sort">Sorter</string> + <string name="fm_sort_name">Navn</string> + <string name="fm_sort_date">Dato ændret</string> + <string name="fm_rows_comfortable">Luftige rækker</string> + <string name="fm_rows_compact">Kompakte rækker</string> + <string name="fm_hide_hidden">Skjul skjulte filer</string> + <string name="fm_show_hidden">Vis skjulte filer</string> + <string name="fm_hide_favorites">Skjul favoritter</string> + <string name="fm_show_favorites">Vis favoritter</string> + <string name="fm_filter_folder">Filtrer denne mappe</string> + <string name="fm_free_space">%1$s ledig</string> + <string name="fm_selected_count">%1$d valgt</string> + <string name="fm_select_none">Ingen</string> + <string name="fm_select_all">Alle</string> + <string name="fm_paste">Indsæt</string> + <plurals name="fm_items"> + <item quantity="one">%1$d element</item> + <item quantity="other">%1$d elementer</item> + </plurals> + <string name="fm_paste_here">Indsæt %1$s her</string> + <string name="fm_paste_move_here">Indsæt %1$s (flyt) her</string> + <string name="fm_rail_title">Filer</string> + <string name="fm_rail_storage">LAGERPLADS</string> + <string name="fm_rail_quick">GENVEJE</string> + <string name="fm_rail_favorites">FAVORITTER</string> + <string name="fm_rail_downloads">Download</string> + <string name="fm_rail_downloads_short">Download</string> + <string name="fm_rail_roms">ROMs</string> + <string name="fm_rail_session_logs">Sessionslogs</string> + <string name="fm_rail_pictures">Billeder</string> + <string name="fm_favorite_added">Tilføjede \"%1$s\" til Favoritter</string> + <string name="fm_favorite_removed">Fjernede \"%1$s\" fra Favoritter</string> + <string name="fm_empty_directory">Tom mappe</string> + <string name="fm_up_one_level">Et niveau op</string> + <string name="fm_choose_folder">Vælg en mappe</string> + <string name="fm_choose_file">Vælg en fil</string> + <string name="ctrl_tint_steam_blue">Steam-blå</string> + <string name="ctrl_tint_sky">Himmelblå</string> + <string name="ctrl_tint_white">Hvid</string> + <string name="ctrl_tint_violet">Violet</string> + <string name="ctrl_tint_green">Grøn</string> + <string name="ctrl_tint_amber">Ravgul</string> + <string name="ctrl_tint_red">Rød</string> + <string name="ctrl_tint_pink">Pink</string> + <string name="ctrl_button_a">A-knap</string> + <string name="ctrl_button_b">B-knap</string> + <string name="ctrl_button_x">X-knap</string> + <string name="ctrl_button_y">Y-knap</string> + <string name="ctrl_button_lb">Venstre bumper</string> + <string name="ctrl_button_rb">Højre bumper</string> + <string name="ctrl_button_lt">Venstre trigger</string> + <string name="ctrl_button_rt">Højre trigger</string> + <string name="ctrl_button_view">View-knap</string> + <string name="ctrl_button_menu">Menu-knap</string> + <string name="ctrl_target_view">View</string> + <string name="ctrl_target_menu">Menu</string> + <string name="ctrl_target_up">D-pad op</string> + <string name="ctrl_target_down">D-pad ned</string> + <string name="ctrl_target_left">D-pad venstre</string> + <string name="ctrl_target_right">D-pad højre</string> + <string name="ctrl_target_hidden">Skjult</string> + <string name="ctrl_editor_drag">Træk for at flytte</string> + <string name="ctrl_editor_snap">Fastgør</string> + <string name="ctrl_editor_save">Gem</string> + <string name="second_mode_none">Ingen</string> + <string name="second_mode_keyboard_trackpad">Tastatur + trackpad</string> + <string name="second_mode_terminal">Terminal</string> + <string name="second_mode_deck_controls">Deck-greb + trackpads</string> + + <string name="rtinst_removing">Fjerner Linux-runtime</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">Fjerner Linux-runtime · %1$d element</item> + <item quantity="other">Fjerner Linux-runtime · %1$d elementer</item> + </plurals> + <string name="rtinst_removed">Linux-runtime fjernet</string> + <string name="rtinst_removal_failed">Kunne ikke afslutte fjernelsen af Linux-runtime. Prøv fjernelse igen i Opsætning.</string> + <string name="rtinst_removing_leftovers">Fjerner rester af runtime</string> + <string name="rtinst_downloading">Downloader</string> + <string name="rtinst_verifying">Verificerer</string> + <string name="rtinst_extracting">Udpakker</string> + <string name="rtinst_channel_description">Vises, mens Linux-runtime installeres eller fjernes</string> + <string name="rtinst_title_installing">Installerer Linux-runtime</string> + <string name="rtinst_title_removing">Fjerner Linux-runtime</string> + + <string name="deskpkg_runtime_missing">Linux-runtime er ikke installeret</string> + <string name="deskpkg_no_checksum">Kataloget har ingen tjeksum for %1$s</string> + <string name="deskpkg_checksum_mismatch">Tjeksum stemmer ikke - intet blev ændret</string> + <string name="deskpkg_installing">Installerer %1$s</string> + <string name="deskpkg_place_failed">Kunne ikke placere AppImage</string> + <string name="deskpkg_extract_failed">Udpakning mislykkedes</string> + <string name="deskpkg_install_failed">Installation mislykkedes</string> + + <string name="flatpakmgr_setup_failed">Flatpak-opsætning mislykkedes (afslutningskode %1$d)</string> + <string name="flatpakmgr_setup_unfinished">Flatpak-opsætning blev ikke afsluttet</string> + <string name="flatpakmgr_busy">En anden butiksopgave er i gang</string> + <string name="flatpakmgr_not_ready">Opsæt Flatpak først</string> + <string name="flatpakmgr_starting">Starter</string> + <string name="flatpakmgr_exit_status">Flatpak afsluttede med status %1$d</string> + <string name="flatpakmgr_installing">Installerer %1$s</string> + <string name="flatpakmgr_updating">Opdaterer %1$s</string> + <string name="flatpakmgr_removing">Fjerner %1$s</string> + <string name="flatpakmgr_installing_step">Installerer %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_updating_step">Opdaterer %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_removing_step">Fjerner %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_ref_translations">oversættelser</string> + <string name="flatpakmgr_ref_graphics_drivers">grafikdrivere</string> + <string name="flatpakmgr_ref_media_codecs">mediecodecs</string> + <string name="flatpakmgr_ref_runtime">%1$s-runtime</string> + <string name="flatpakmgr_ref_runtime_branch">%1$s-runtime %2$s</string> + + <string name="appimg_too_small">Filen er for lille til at være en AppImage</string> + <string name="appimg_unreadable">Filen kan ikke læses: %1$s</string> + <string name="appimg_not_linux">Dette er ikke en AppImage (det er ikke et Linux-program)</string> + <string name="appimg_wrong_arch">Denne AppImage er bygget til hverken ARM64 (aarch64) eller x86-pc\'er</string> + <string name="appimg_not_appimage">Dette er et Linux-program, men ikke en AppImage</string> + <string name="appimg_wrong_type">Kun type 2-AppImages kan importeres (denne er type %1$d)</string> + <string name="appimg_no_space_import">Ikke nok ledig plads: import kræver cirka %1$s</string> + <string name="appimg_no_space_update">Ikke nok ledig plads: opdatering kræver cirka %1$s</string> + <string name="appimg_import_failed">Import mislykkedes</string> + <string name="appimg_gone">Den AppImage findes ikke længere</string> + <string name="appimg_update_failed">Opdatering mislykkedes</string> + <string name="appimg_extracting">Udpakker %1$s</string> + <string name="appimg_extract_failed">AppImage kunne ikke udpakkes</string> + <string name="appimg_extract_failed_detail">AppImage kunne ikke udpakkes: %1$s</string> + + <string name="deckymgr_stop_session_loader">Stop den aktive session, før du installerer Decky Loader</string> + <string name="deckymgr_download_failed">Download af Decky Loader mislykkedes</string> + <string name="deckymgr_size_mismatch">Decky Loaders størrelse passede ikke med metadata for udgivelsen</string> + <string name="deckymgr_checksum_download_failed">Kunne ikke downloade tjeksummen for udgivelsen</string> + <string name="deckymgr_checksum_missing">Tjeksum for udgivelsen mangler</string> + <string name="deckymgr_checksum_invalid">Tjeksum for udgivelsen er ugyldig</string> + <string name="deckymgr_checksum_mismatch">Tjeksum for Decky Loader stemmer ikke</string> + <string name="deckymgr_not_elf">Udgivelsesfilen er ikke en kompatibel 64-bit Linux-eksekverbar fil</string> + <string name="deckymgr_session_started_loader">En session startede under downloadet; stop den før du installerer Decky Loader</string> + <string name="deckymgr_loader_not_executable">Kunne ikke markere PluginLoader som eksekverbar</string> + <string name="deckymgr_cef_failed">Kunne ikke aktivere Steam CEF-fjernfejlretning</string> + <string name="deckymgr_stage_failed">Kunne ikke forberede PluginLoader</string> + <string name="deckymgr_loader_preserve_failed">Kunne ikke bevare den eksisterende PluginLoader</string> + <string name="deckymgr_loader_activate_failed">Kunne ikke aktivere den nye PluginLoader</string> + <string name="deckymgr_stop_session_plugin">Stop den aktive session, før du installerer et plugin</string> + <string name="deckymgr_loader_required">Installer Decky Loader, før du importerer et plugin</string> + <string name="deckymgr_zip_unreadable">Kunne ikke læse den valgte ZIP-fil</string> + <string name="deckymgr_plugins_dir_failed">Kunne ikke oprette mappen til Decky-plugins</string> + <string name="deckymgr_extracting_zip">Udpakker plugin-ZIP</string> + <string name="deckymgr_zip_one_folder">Plugin-ZIP skal indeholde én plugin-mappe på øverste niveau</string> + <string name="deckymgr_zip_missing_files">Plugin-ZIP mangler plugin.json, package.json eller dist/index.js</string> + <string name="deckymgr_plugin_name_missing">Plugin-navn mangler</string> + <string name="deckymgr_session_started_plugin">En session startede under installationen; stop den før du installerer et plugin</string> + <string name="deckymgr_plugin_installed">Plugin installeret · genstart Steam for at indlæse det</string> + <string name="deckymgr_plugin_install_failed">Installation af plugin-ZIP mislykkedes</string> + <string name="deckymgr_zip_too_many">Plugin-ZIP indeholder for mange elementer</string> + <string name="deckymgr_zip_invalid_path">Plugin-ZIP indeholder en ugyldig sti</string> + <string name="deckymgr_zip_duplicate_paths">Plugin-ZIP indeholder gentagne stier</string> + <string name="deckymgr_zip_unsafe_path">Plugin-ZIP indeholder en usikker sti</string> + <string name="deckymgr_folder_failed">Kunne ikke oprette plugin-mappe</string> + <string name="deckymgr_zip_too_large">Plugin-ZIP udvider sig til mere end 1 GiB-grænsen</string> + <string name="deckymgr_bundled_not_executable">Kunne ikke markere den medfølgende plugin-binærfil som eksekverbar</string> + <string name="deckymgr_remote_entry_invalid">Ugyldig remote_binary-post</string> + <string name="deckymgr_remote_name_invalid">Ugyldigt navn på fjern-binærfil</string> + <string name="deckymgr_remote_url_https">URL til fjern-binærfil skal bruge HTTPS</string> + <string name="deckymgr_remote_hash_invalid">Fjern-binærfilen %1$s har en ugyldig SHA-256-hash</string> + <string name="deckymgr_bin_dir_failed">Kunne ikke oprette plugin-bin-mappen</string> + <string name="deckymgr_downloading_binary">Downloader plugin-binærfil · %1$s</string> + <string name="deckymgr_binary_download_failed">Kunne ikke downloade plugin-binærfilen %1$s</string> + <string name="deckymgr_binary_checksum_mismatch">Tjeksum for plugin-binærfil stemmer ikke · %1$s</string> + <string name="deckymgr_binary_not_executable">Kunne ikke markere plugin-binærfil som eksekverbar · %1$s</string> + <string name="deckymgr_binary_replace_failed">Kunne ikke udskifte plugin-binærfil · %1$s</string> + <string name="deckymgr_binary_install_failed">Kunne ikke installere plugin-binærfil · %1$s</string> + <string name="deckymgr_folder_taken">Plugin-mappen hører til et andet plugin</string> + <string name="deckymgr_preserve_failed">Kunne ikke bevare det eksisterende plugin</string> + <string name="deckymgr_activate_failed">Kunne ikke aktivere det importerede plugin</string> + <string name="deckymgr_settings_failed">Kunne ikke opdatere Decky-pluginindstillinger</string> + <string name="deckymgr_restore_failed">Kunne ikke gendanne det eksisterende plugin</string> + + <string name="adbfix_save_failed">Kunne ikke gemme identitet til Trådløs fejlretning</string> + <string name="adbfix_command_failed">ADB-kommando mislykkedes (%1$d)</string> + <string name="adbfix_not_confirmed">Android bekræftede ikke, at procesgrænsen blev ændret: %1$s</string> + <string name="adbfix_no_pairing">Ingen parring for Trådløs fejlretning er gemt</string> + <string name="adbfix_enter_address">Indtast den aktuelle IP-adresse og port for Trådløs fejlretning</string> + + <string name="adbpair_timed_out">Der opstod timeout under venten på parringsvinduet. Forsøg igen fra DroidDeck.</string> + <string name="adbpair_discovery_failed_code">Kunne ikke lede efter parringsvinduet (%1$d). Tjek at Wi-Fi er tændt.</string> + <string name="adbpair_discovery_failed">Kunne ikke lede efter parringsvinduet: %1$s</string> + <string name="adbpair_six_digits">Indtast alle 6 cifre i koden</string> + <string name="adbpair_pairing">Parrer…</string> + <string name="adbpair_code_rejected">Den kode virkede ikke. Åbn parringsvinduet igen, og indtast dens nye kode.</string> + <string name="adbpair_connecting">Parret. Forbinder…</string> + <string name="adbpair_port_not_found">Parret, men porten til Trådløs fejlretning blev ikke fundet. Lad Trådløs fejlretning være tændt, og forsøg igen.</string> + <string name="adbpair_applying">Anvender indstillingen…</string> + <string name="adbpair_command_failed">Kommando til Trådløs fejlretning mislykkedes</string> + <string name="adbpair_channel_name">Opsætning af Trådløs fejlretning</string> + <string name="adbpair_channel_description">Spørger efter parringskoden til Trådløs fejlretning, mens Indstillinger er åben</string> + <string name="adbpair_title_pair">Par Trådløs fejlretning</string> + <string name="adbpair_waiting_text">Tænd Trådløs fejlretning, og tryk derefter på „Par enhed med parringskode“.</string> + <string name="adbpair_waiting_long">Tænd Trådløs fejlretning, og tryk derefter på „Par enhed med parringskode“. Bliv i Indstillinger: koden vises i denne notifikation.</string> + <string name="adbpair_code_label">6-cifret parringskode</string> + <string name="adbpair_code_hint">Tryk på Indtast kode, og skriv de 6 cifre, der vises i Indstillinger.</string> + <string name="adbpair_title_code">Indtast Wi-Fi-parringskoden</string> + <string name="adbpair_title_retry">Forsøg koden igen</string> + <string name="adbpair_enter_code">Indtast kode</string> + <string name="adbpair_title_working">Sætter Steam op</string> + <string name="adbpair_title_done">Steam er klar</string> + <string name="adbpair_done_text">Tryk for at gå tilbage til DroidDeck. Du kan slukke Trådløs fejlretning nu.</string> + <string name="adbpair_title_failed">Opsætning af Trådløs fejlretning stoppet</string> + + <string name="selfinst_http">Downloadet svarede HTTP %1$d</string> + <string name="selfinst_size_mismatch">Downloadets størrelse passede ikke med den udgivne APK og blev kasseret - forsøg igen</string> + <string name="selfinst_checksum_mismatch">Downloadet matchede ikke sin tjeksum og blev kasseret - forsøg igen</string> + <string name="selfinst_wrong_package">Opdateringen er til Android-pakken %1$s, ikke %2$s</string> + <string name="selfinst_metadata_signer">Opdateringens metadata angiver ikke DroidDecks udgivelsessigneringsnøgle</string> + <string name="selfinst_unreadable">Android kunne ikke læse den downloadede APK</string> + <string name="selfinst_apk_wrong_package">Den downloadede APK er til %1$s, ikke %2$s</string> + <string name="selfinst_version_mismatch">Den downloadede APK siger versionCode %1$d, ikke den udgivne %2$d</string> + <string name="selfinst_downgrade">Android installerer ikke versionCode %1$d over den installerede %2$d</string> + <string name="selfinst_no_signing">Den downloadede APK har ingen signeringsoplysninger</string> + <string name="selfinst_wrong_signer">Den downloadede APK er ikke signeret med DroidDecks udgivelsesnøgle</string> + <string name="appupd_http">Opdateringskataloget svarede HTTP %1$d</string> + + <string name="loadstate_starting">Starter sessionen…</string> + <string name="loadstate_elapsed">%1$d:%2$02d forløbet · arbejder stadig</string> + <string name="loadstate_steam_update">Downloader Steam-klientopdateringen · %1$d%%</string> + <string name="loadstate_steam_client">Downloader Steam-klienten · %1$s</string> + + <string name="pextra_busy">En installation af et kompatibilitetsværktøj er allerede i gang</string> + <string name="pextra_stop_session">Stop den aktive session, før du installerer kompatibilitetsværktøjer</string> + <string name="pextra_no_release">Kunne ikke finde en ARM64-udgivelse af %1$s</string> + <string name="pextra_no_space">Mindst 4 GB ledig plads plus downloadets størrelse er nødvendig for at installere %1$s</string> + <string name="pextra_downloading">Downloader %1$s %2$s</string> + <string name="pextra_retrying">Forsøger download igen · forsøg %1$d af %2$d</string> + <string name="pextra_interrupted">Download afbrudt</string> + <string name="pextra_download_failed">Downloadet mislykkedes; den delvise fil beholdes, så et nyt forsøg kan genoptage</string> + <string name="pextra_too_big">Downloadet overskred udgivelsens størrelse og blev slettet</string> + <string name="pextra_incomplete">Downloadet var ufuldstændigt; den delvise fil beholdes til et genoptageligt forsøg</string> + <string name="pextra_verifying">Bekræfter download</string> + <string name="pextra_checksum_unreadable">Kunne ikke læse tjeksummen for udgivelsen; forsøg igen</string> + <string name="pextra_no_checksum">%1$s %2$s udgiver ingen tjeksum; intet blev installeret</string> + <string name="pextra_checksum_mismatch">Tjeksum stemmer ikke; downloadet blev slettet</string> + <string name="pextra_session_started">En session startede under downloadet; stop den før du installerer kompatibilitetsværktøjer</string> + <string name="pextra_installing">Installerer %1$s</string> + <string name="pextra_install_failed">Installation af %1$s mislykkedes (afslutningskode %2$d)</string> + <string name="pextra_could_not_install">Kunne ikke installere %1$s</string> + <string name="esync_fetching">Henter droiddeck-esync-pakken</string> + + <string name="cmgr_fex_linux_helpers">med Linux-hjælpeprogrammer</string> + <string name="cmgr_fex_dlls_only">Kun DLL\'er</string> + <string name="cmgr_original_proton_updated">Original (Proton opdateret siden sidste ombytning)</string> + <string name="cmgr_changed_outside">Ændret uden for Komponenter</string> + <string name="cmgr_not_present">ikke til stede</string> + <string name="cmgr_original_version">Original %1$s</string> + <string name="cmgr_original_from">Original fra %1$s</string> + <string name="cmgr_swap_queued">%1$s %2$s sættes ind i %3$s, når spillet der kører på det, lukker.</string> + <string name="cmgr_swap_done">%1$s %2$s er nu i %3$s. Det træder i kraft næste gang et spil starter.</string> + <string name="cmgr_original_missing">Den originale pakke er ikke gemt</string> + <string name="cmgr_restore_queued">%1$s af %2$s går tilbage til sin original, når spillet der kører på det, lukker.</string> + <string name="cmgr_restore_done">%1$s af %2$s gendannet fra sin %3$s-original.</string> + <string name="cmgr_package_in_use">%1$s er i brug i %2$s. Byt den Proton til noget andet først.</string> + <string name="cmgr_package_not_stored">%1$s er ikke gemt</string> + <string name="cmgr_deleted">%1$s slettet.</string> + <string name="cmgr_original_kept">Originalen af den installerede version beholdes: den er vejen tilbage.</string> + <string name="cmgr_not_stored">Ikke gemt</string> + <string name="cmgr_deleted_original">Slettede %1$s-originalen af %2$s.</string> + <string name="cmgr_nightlies_only">Downloads kommer kun fra Nightlies-udgivelserne</string> + <string name="cmgr_list_predates_checksums">Denne pakkeliste er ældre end tjeksummer - opdater den, og forsøg igen</string> + <string name="cmgr_checksum_mismatch">Tjeksum stemmer ikke</string> + + <string name="sprefs_upscaler_linear">Lineær</string> + <string name="sprefs_upscaler_nearest">Nærmeste</string> + <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (kvalitet)</string> + <string name="sprefs_upscaler_sharpen">Kun skærpning</string> + <string name="sprefs_lod_bias_auto">Auto (matcher skalering)</string> + + <string name="look_off_desc">Intet anvendt.</string> + <string name="look_game_clarity">Spilklarhed</string> + <string name="look_game_clarity_desc">Skærpning med et strejf af kontrast - den til hver dag.</string> + <string name="look_vivid">Levende</string> + <string name="look_vivid_desc">Farven skubbet hårdt op. Deband slået til, så himlen ikke striber.</string> + <string name="look_cinematic">Filmisk</string> + <string name="look_cinematic_desc">Dybere kontrast, koldere farve, udjævnede kanter.</string> + <string name="look_competitive">Konkurrence</string> + <string name="look_competitive_desc">Skygger hævet og tung skærpning - se folk først.</string> + <string name="look_adaptive_sharpen">Adaptiv skærpning</string> + <string name="look_adaptive_sharpen_desc">Kun skærpning, farven er uberørt.</string> + <string name="look_filmic">Film</string> + <string name="look_filmic_desc">Filmagtig kurve, let dæmpet.</string> + <string name="look_arcade">Arkade</string> + <string name="look_arcade_desc">Højt og slagkraftigt.</string> + <string name="look_retro_crt">Retro CRT</string> + <string name="look_retro_crt_desc">CRT-rørets og det analoge signals udseende sammen.</string> + <string name="look_upscale_sharp">Opskalering skarp</string> + <string name="look_upscale_sharp_desc">Til kørsel under skærmens opløsning.</string> + <string name="look_pixel_clean">Rene pixels</string> + <string name="look_pixel_clean_desc">Skarpe pixels til 2D og gamle titler.</string> + <string name="look_anime_edge">Anime-kant</string> + <string name="look_anime_edge_desc">Toon-konturer plus skærpning.</string> + + <string name="gpuinfo_not_adreno">Ikke en Adreno-GPU</string> + <string name="gpuinfo_supported">Understøttet</string> + <string name="gpuinfo_experimental">Eksperimentel: dens drivere er testversioner</string> + <string name="gpuinfo_below_tested">Under testet hardware (Adreno 725 og nyere): kører muligvis ikke</string> + <string name="gpuinfo_unsupported">Ikke understøttet: DroidDeck kræver en Adreno (Snapdragon) GPU</string> + + <string name="turnip_rate_limited">GitHubs hastighedsgrænse blev nået - forsøg igen senere</string> + <string name="turnip_github_http">GitHub svarede HTTP %1$d</string> + <string name="turnip_download_http">downloadet svarede HTTP %1$d</string> + <string name="turnip_list_predates_checksums">Denne driverliste er ældre end tjeksummer - opdater den, og forsøg igen</string> + <string name="turnip_checksum_mismatch">Tjeksum stemmer ikke - downloadet blev kasseret</string> + <string name="turnip_not_adrenotools">Ikke en AdrenoTools-driver: %1$s</string> + <string name="turnip_reject_no_meta">ingen meta.json</string> + <string name="turnip_reject_meta_unreadable">meta.json kan ikke læses (%1$s)</string> + <string name="turnip_reject_linux_driver">dette er en Linux-runtime-driver (importer den under „Linux runtime driver“)</string> + <string name="turnip_reject_wayland">dette er en Wayland-spildriver, som denne app ikke har brug for</string> + <string name="turnip_reject_no_library_name">meta.json angiver ikke noget libraryName, så intet kunne gives til AdrenoTools</string> + <string name="turnip_reject_library_missing">meta.json angiver %1$s, som ikke findes i zip-filen</string> + <string name="turnip_reject_not_aarch64">%1$s er ikke et 64-bit AArch64 ELF-delt bibliotek</string> + <string name="turnip_reject_glibc">%1$s linker glibc - en „-Linux“ Turnip, som hører under „Linux runtime driver“</string> + <string name="lvd_no_library">Ingen libvulkan_freedreno*.so i denne zip-fil. En Android (AdrenoTools) eller -Wayland Turnip-zip er ikke en Linux-runtime-driver.</string> + <string name="lvd_not_aarch64">%1$s er ikke et 64-bit AArch64 ELF-delt bibliotek.</string> + <string name="lvd_not_glibc">%1$s er ikke en glibc-driver - den linker Androids libc. Linux-runtime kræver en „-Linux“-zip; en almindelig eller „-Wayland“ Turnip kan slet ikke indlæses af Steam-klienten.</string> + <string name="bundle_no_manifest">Ikke en driverpakke: ingen manifest.json, der angiver en android- og en linux-driver</string> + <string name="bundle_entry_outside">Ikke en driverpakke: %1$s peger uden for den</string> + <string name="bundle_too_large">Ikke en driverpakke: den udpakkes til mere end %1$d MB</string> + + <string name="gstore_sd_card">SD-kort</string> + <string name="gstore_folder">Mappe</string> + + <string name="logshare_chooser">Del sessionslogs</string> + + <string name="hdr_needs_android_10">kræver Android 10 eller nyere (skærmlag med et farverum)</string> + <string name="hdr_none">denne skærm rapporterer ingen HDR-understøttelse</string> + <string name="hdr_not_hdr10">denne skærm understøtter %1$s, ikke HDR10</string> + + <string name="gpu_this_gpu">denne GPU</string> + <string name="phantom_title_on">Procesgrænse er tændt</string> + <string name="phantom_title_unset">Procesgrænse er ikke sat</string> + <string name="phantom_title_unreadable">Procesgrænse kunne ikke tjekkes</string> + <string name="phantom_title_off">Procesgrænse er slået fra</string> + <string name="phantom_title_not_required">Procesgrænse er ikke nødvendig</string> + <string name="phantom_instructions_fix">%1$s Hvis det ikke er muligt, så forbind enheden til en computer med ADB, og køre kommandoen nedenfor. Kom tilbage hertil, og tjek igen.</string> + <string name="phantom_instructions_off">Steam-sessioner kan starte.</string> + <string name="phantom_instructions_not_applicable">Denne Android-version bruger ikke denne grænse.</string> + <string name="phantom_fix_toggle">Tænd „Disable child process restrictions“ i Udvikleremuligheder, og lad Udvikleremuligheder være tændt bagefter.</string> + <string name="phantom_fix_no_toggle">Denne Android-version har ingen kontakt til det. Brug Trådløs fejlretning til at ændre det fra denne enhed.</string> + <string name="upd_block_no_apk">Denne version har ingen download til denne kopi af DroidDeck.</string> + <string name="upd_block_version_code">Android installerer den ikke over denne version, fordi dens versionCode %1$d er under den installerede %2$d.</string> + <string name="upd_fail_downgrade">Android installerer ikke en ældre version over en nyere. Bliv på denne version, indtil kanalen følger efter.</string> + <string name="upd_fail_signature">Denne kopi af DroidDeck er signeret anderledes (en lokal version?), så Android opdaterer den ikke direkte.</string> + <string name="upd_fail_storage">Der er ikke nok ledig plads til opdateringen.</string> + <string name="upd_fail_aborted">Opdateringen blev annulleret.</string> + <string name="upd_fail_with_message">Opdateringen blev ikke installeret: %1$s</string> + <string name="upd_fail_generic">Opdateringen blev ikke installeret.</string> + <string name="saves_layout_zip">%1$s-zip</string> + <string name="saves_zip">zip</string> + <string name="saves_not_launched">%1$s er ikke startet endnu</string> + <string name="saves_none_found">Ingen gemte filer fundet for %1$s</string> + <string name="saves_not_save_zip">%1$s har ingen drive_c-mappe, så det er ikke en gemt-zip fra GameHub eller Winlator</string> + <string name="gstore_cannot_create">kan ikke oprette mapper i %1$s</string> + <string name="gstore_cannot_write">kan ikke skrive i %1$s</string> + <string name="gstore_cannot_write_detail">kan ikke skrive i %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">Mappen er ikke tilgængelig</string> + <string name="gfiles_name_taken">En anden fil bruger allerede %1$s</string> + <string name="gfiles_art_unavailable">Grafikmappen er ikke tilgængelig</string> + <string name="gfiles_unrelated_inventory">Mappen har en urelateret synkroniseringsoversigt</string> + <string name="gfiles_remove_failed">Kunne ikke fjerne %1$s</string> + <string name="gfiles_outside_folder">Filen forlader eksportmappen</string> + <string name="cmgr_fex_unnamed">FEX (version uden versionsnavn)</string> + <string name="cmgr_no_profile">ingen profile.json i %1$s</string> + <string name="cmgr_not_component_package">%1$s er ikke en Linux-komponentpakke (type %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s fri</string> + <string name="mode_storage_auto_short">Automatisk</string> + <string name="appupd_catalog_rejected">Opdateringskataloget bestod ikke sine kontroller (%1$s)</string> + <string name="main_adb_clip_label">DroidDeck-indstilling for procesgrænse</string> + <string name="setup_signed_in_offline_unnamed">Logget ind · offline-tilstand</string> + <string name="setup_signed_in_unnamed">Logget ind</string> + <string name="driver_tag_bundled">INDBYGGET</string> + <string name="driver_tag_downloaded">DOWNLOADET</string> + <string name="driver_tag_imported">IMPORTERET</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Sessionsskærm</string> + <string name="display_resolution">Opløsning</string> + <string name="display_game_fit">16:9-spiltilpasning</string> + <string name="display_game_fit_hint">Hvordan 16:9-spil tilpasses denne skærm. Fra næste session.</string> + <string name="display_game_fit_unavailable">Vælg Match skærm eller en brugerdefineret størrelse, der er smallere end 16:9.</string> + <string name="display_preserve">Bevar proportioner</string> + <string name="display_stretch">Stræk til skærm</string> + <string name="display_image_scaling">Billedskalering</string> + <string name="display_filter">Skaleringsfilter</string> + <string name="display_filter_hint">Deles af Steam og Desktop.</string> + <string name="display_filter_live">Træder i kraft øjeblikkeligt. Deles af Steam og Desktop.</string> + <string name="display_filter_note">FSR, GSR og NIS anvendes ved forstørrelse af sessionsbilledet. Kun skærpning virker også uden forstørrelse.</string> + <string name="display_sharpness">Filterskarphed</string> + <string name="display_frame_rate">Billedfrekvens</string> + <string name="display_look_filter">%1$s Indstiller skalering til %2$s.</string> + <string name="display_match_screen">Match skærm (%1$d×%2$d)</string> + <string name="display_follow_screen">Følg skærm (ændrer størrelse ved fold/udfold)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Vis enhedsoplysninger</string> + <string name="device_info_hide">Skjul enhedsoplysninger</string> + <string name="device_info_loading">Læser enheden…</string> + <string name="device_info_copy">Kopier</string> + <string name="device_info_copy_hint">Indsæt disse oplysninger i en fejlrapport.</string> + <string name="device_info_copied">Kopieret.</string> + <string name="force_ssbs_title">Tving SSBS</string> + <string name="force_ssbs_hint">Bevarer en CPU-hastighedsindstilling, som Wine hele tiden slår fra. Kan gøre Windows-spil hurtigere på Snapdragon 8 Elite; lille ændring set på ældre chips. Træder i kraft ved næste spilstart.</string> + + <string name="rail_stores">Butikker</string> + <string name="source_steam">Steam</string> + <string name="source_added">Brugerdefineret</string> + <string name="setup_stores">Butikker</string> + <string name="setup_stores_open_on">Åbn en butik på</string> + <string name="setup_stores_speed">Downloadhastighed</string> + <string name="setup_stores_engine">Butiksmotor</string> + <string name="setup_stores_engine_chip_ready">● Motor klar</string> + <string name="setup_stores_engine_chip_missing">Ikke bygget</string> + <string name="stores_tier_balanced">Afbalanceret</string> + <string name="stores_tier_fast">Hurtig</string> + <string name="stores_tier_max">Maks</string> + <string name="stores_title">Butikker</string> + <string name="stores_settings">Butiksindstillinger</string> + <string name="stores_settings_eyebrow">Indstillinger</string> + <string name="stores_chip_signed_out">(ikke logget ind)</string> + <string name="stores_tab_store">Butik</string> + <string name="stores_tab_installed">Installeret</string> + <string name="stores_tab_library">Bibliotek</string> + <string name="stores_tab_all">Alle</string> + <string name="stores_tab_downloads">Downloads</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Log ind</string> + <string name="stores_signout_named">%1$s · log ud</string> + <string name="stores_signout_title">Log ud af %1$s?</string> + <string name="stores_signout_note">Installerede spil forbliver. Biblioteket hentes igen, når du logger ind igen.</string> + <string name="stores_signout_stay">Bliv logget ind</string> + <string name="stores_signout_go">Log ud</string> + <string name="stores_search_catalog">Søg i %1$s-kataloget…</string> + <string name="stores_search_installed">Søg i installerede spil…</string> + <string name="stores_search_library">Søg i dit bibliotek…</string> + <string name="stores_search_open">Søg</string> + <string name="stores_installed_empty">Intet installeret</string> + <string name="stores_library_loading">Indlæser</string> + <string name="stores_nothing_matches">Ingen spil</string> + <string name="stores_shelves_loading">Indlæser butikken…</string> + <string name="stores_amazon_no_catalog">Intet katalog</string> + <string name="stores_shelf_new">Nyheder</string> + <string name="stores_shelf_deals">Tilbud</string> + <string name="stores_shelf_free">Gratis spil</string> + <string name="stores_shelf_trending">Populært</string> + <string name="stores_shelf_library">Dit bibliotek</string> + <string name="stores_play">Spil</string> + <string name="stores_install">Installer</string> + <string name="stores_resume_install">Genoptag installation</string> + <string name="stores_dl_interrupted">Afbrudt</string> + <string name="stores_install_size">Installation · %1$s</string> + <string name="stores_get_free">Få gratis</string> + <string name="stores_view_on">Se på %1$s</string> + <string name="stores_buy_price">Køb · %1$s</string> + <string name="stores_installed_chip">Installeret</string> + <string name="stores_in_library">I biblioteket</string> + <string name="stores_free">Gratis</string> + <string name="stores_resume">Genoptag</string> + <string name="stores_refresh">Opdater</string> + <string name="stores_show_mature">Vis indhold for voksne</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Offline</string> + <string name="epic_card_none">Fra</string> + <string name="epic_eos">Epic-login (EOS)</string> + <string name="epic_offline">Start offline</string> + <string name="epic_resolve">Løs Epic-login</string> + <string name="store_cloud_saves">Cloud-gemte spil</string> + <string name="cloud_upload">Upload til cloud</string> + <string name="cloud_download">Download fra cloud</string> + <string name="cloud_none">Ingen cloud-gemte spil</string> + <string name="cloud_conflict">Cloud-konflikt</string> + <string name="cloud_conflict_files">Cloud-konflikt · %1$d filer</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Behold cloud</string> + <string name="cloud_keep_local">Behold lokal</string> + <string name="cloud_checking">Tjekker cloud…</string> + <string name="cloud_last_sync">Senest synkroniseret %1$s</string> + <string name="cloud_never">Ikke synkroniseret endnu</string> + <string name="cloud_done">%1$d filer</string> + <string name="stores_eyebrow_installed">installeret</string> + <string name="stores_eyebrow_library">i dit bibliotek</string> + <string name="stores_eyebrow_store">butik</string> + <string name="achievements_title">Bedrifter</string> + <string name="achievements_count">%1$d af %2$d låst op</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Låst op · %1$d</string> + <string name="achievements_group_locked">Låst · %1$d</string> + <string name="achievements_group_hidden">Skjult · %1$d</string> + <string name="achievements_hidden">Skjult bedrift</string> + <string name="achievements_hidden_detail">Detaljer vises, når den er låst op.</string> + <string name="achievements_all">Alle bedrifter låst op</string> + <string name="achievements_reading">Indlæser…</string> + <string name="achievements_signed_out">Log ind på %1$s for at se dine bedrifter.</string> + <string name="achievements_unreachable">Kunne ikke oprette forbindelse til %1$s. Prøv igen, når du er online.</string> + <string name="achievements_none">%1$s har ingen bedrifter for dette spil.</string> + <string name="achievements_show_all">Vis alle %1$d</string> + <string name="achievements_dialog_title">Bedrifter · %1$s</string> + <string name="stores_in_steam">✓ I Steam</string> + <string name="stores_uninstall">Afinstaller</string> + <string name="stores_uninstalling">Afinstallerer…</string> + <string name="stores_uninstall_title">Afinstaller %1$s?</string> + <string name="stores_uninstall_body_size">Frigør %1$s og fjerner det fra Steam.</string> + <string name="stores_uninstall_body">Fjerner dets filer og dets genvej i Steam.</string> + <string name="stores_launch_not_registered">Steam fanger spillet op næste gang det starter; start det derfra, eller åbn Steam nu.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Steams eget valg pr. spil</string> + <string name="stores_card_audio">Lyd</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Komponenter</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Motor</string> + <string name="stores_card_engine_value">%1$s · native</string> + <string name="stores_card_engine_java">%1$s · indbygget</string> + <string name="stores_how_it_runs">Hvordan det køres</string> + <string name="stores_kv_fetched">Hentet fra</string> + <string name="stores_kv_fetched_value">%1$s, med din konto</string> + <string name="stores_kv_installed_to">Installeret til</string> + <string name="stores_kv_installed_to_value">Spillagring (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Startes af</string> + <string name="stores_kv_launched_by_value">Steam og Proton, samme vej som Steam-spil</string> + <string name="stores_kv_launched_by_epic">Steam og Proton · Epic-login pr. start, EOS-overlay slået fra</string> + <string name="stores_kv_launched_by_amazon">Steam og Proton · Amazon Games Services-variabler er sat</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">tilføjet til Steam ved installation (Deck UI + Steam Input)</string> + <string name="stores_downloads_empty">Ingen downloads</string> + <string name="stores_dl_paused">Pause</string> + <string name="stores_dl_queued">I kø</string> + <string name="stores_dl_queued_at">I kø · #%1$d</string> + <string name="stores_dl_cancelled">Annulleret</string> + <string name="stores_dl_failed">Mislykkedes</string> + <string name="stores_dl_failed_reason">Mislykkedes · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Sæt på pause</string> + <string name="stores_dl_resume">Genoptag</string> + <string name="stores_dl_cancel">Annuller</string> + <string name="stores_dl_retry">Forsøg igen</string> + <string name="stores_dl_clear">Ryd</string> + <string name="stores_dl_clear_all">Ryd gennemførte</string> + <string name="stores_stage_manifest">Manifest</string> + <string name="stores_stage_download">Download</string> + <string name="stores_stage_verify">Bekræft</string> + <string name="stores_stage_install">Installer</string> + <string name="stores_stage_done">Færdig</string> + <string name="stores_stage_checking">Tjekker</string> + <string name="stores_stage_downloading">Downloader</string> + <string name="stores_dl_cancel_confirm">Slet download?</string> + <string name="stores_dl_failed_kept">Mislykkedes · %1$s beholdt</string> + <string name="stores_stage_verifying">Bekræfter</string> + <string name="stores_stage_installing">Installerer</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d filer</string> + <string name="stores_download_manager">Downloadadministrator</string> + <string name="stores_parallel">Downloads på samme tid</string> + <string name="stores_install_where">Installer til</string> + <string name="stores_install_title">Installer %1$s:</string> + <string name="stores_target_internal">Intern</string> + <string name="stores_target_free">%1$s fri</string> + <string name="stores_notification_channel">Butiksdownloads</string> + <string name="stores_notification_channel_hint">Viser fremskridt for spil-downloads og holder downloads kørende i baggrunden</string> + <string name="stores_notification_one">Downloader</string> + <string name="stores_notification_many">%1$d downloads</string> + <string name="stores_notification_waiting">Forbereder…</string> + <string name="stores_notification_done_channel">Afsluttede butiksdownloads</string> + <string name="stores_notification_installed">%1$s installeret</string> + <string name="stores_notification_failed">Download af %1$s mislykkedes</string> + <string name="stores_notification_delete">Slet</string> + <string name="stores_notification_keep">Behold</string> + <string name="stores_login_title">Log ind på %1$s</string> + <string name="stores_login_provider_title">Log ind med din udbyder</string> + <string name="stores_login_reload">Genindlæs</string> + <string name="stores_login_close">Luk</string> + <string name="stores_login_retry">Forsøg igen</string> + <string name="stores_login_finishing">Afslutter login…</string> + <string name="stores_login_error_network">Loginsiden kunne ikke indlæses: %1$s</string> + <string name="stores_login_error_idp_blocked">Denne udbyder afviser login fra en indlejret side. Log i stedet ind med din butikskodeord.</string> + <string name="stores_login_error_http">Loginsiden svarede med en fejl (HTTP %1$d).</string> + <string name="stores_login_error_ssl">Forbindelsen til loginsiden er ikke sikker.</string> + <string name="stores_login_error_verification">Login kunne ikke bekræftes. Forsøg igen.</string> + <string name="stores_login_error_generic">Login mislykkedes. Forsøg igen.</string> + <string name="common_close">Luk</string> + <string name="common_back">Tilbage</string> + <string name="common_edit">Rediger</string> + <string name="games_add_all">Tilføj alle spil i denne mappe</string> + <string name="games_added_none">Intet nyt i denne mappe</string> + <string name="edit_name">Navn</string> + <string name="edit_target">Mål</string> + <string name="edit_start_in">Start i</string> + <string name="edit_launch_options">Startindstillinger</string> + <string name="edit_artwork">Grafik</string> + <string name="edit_other_exe">Anden exe…</string> + <string name="edit_remove">Fjern</string> + <string name="art_cover">Omslag</string> + <string name="art_background">Baggrund</string> + <string name="art_logo">Logo</string> + <string name="art_icon">Ikon</string> + <string name="art_src_folder">Spilmappe</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">Din fil</string> + <string name="art_src_exe">Fra exe-filen</string> + <string name="art_src_auto">Automatisk</string> + <string name="art_pick_image">Vælg billede…</string> + <string name="art_pick_title">Vælg et billede: %1$s</string> + <string name="art_reset">Nulstil til automatisk</string> + <plurals name="games_added_n"> + <item quantity="one">%1$d spil tilføjet</item> + <item quantity="other">%1$d spil tilføjet</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="one">%1$d allerede i Spil</item> + <item quantity="other">%1$d allerede i Spil</item> + </plurals> + <string name="hop_back">B · Tilbage til %1$s</string> + <string name="hop_from_games">spillet</string> + <string name="hop_from_play">Spilindstillinger</string> + <string name="sheet_graphics_drivers">Grafikdrivere</string> + <string name="comp_this_proton">Denne Proton</string> + <string name="comp_every_proton">Hver Proton</string> + <string name="comp_for_every_proton">For hver Proton</string> + <string name="comp_available_count">Tilgængelig · %1$d</string> + <string name="comp_checked">Kontrolleret %1$s</string> + <string name="comp_check_all">Søg efter nye komponenter og drivere</string> + <string name="comp_get">Hent</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton og FEX nedenfor hører til denne Proton</string> + <string name="comp_device_tested">%1$s · testet</string> + <string name="comp_device_untested">%1$s · ikke testet</string> + <string name="comp_device_unsupported">%1$s · ikke understøttet</string> + <string name="gpu_auto_keeps">Holder det anbefalede par ajour</string> + <string name="gpu_bar_auto">Auto · %1$s</string> + <string name="gpu_separately">Runtime- og skærmdrivere separat</string> + <string name="gpu_use_title">Brug %1$s?</string> + <string name="proton_remove_title">Fjern %1$s?</string> + <string name="setup_tab_diagnostics">Diagnostik</string> + <string name="setup_ready">Klar til at spille</string> + <string name="setup_not_ready">Ikke klar til at spille endnu</string> + <string name="setup_ready_runtime">runtime %1$s</string> + <string name="setup_ready_limit_off">procesgrænse fra</string> + <string name="setup_ready_signed_in">logget ind som %1$s</string> + <string name="setup_needs_you">Når noget kræver dig</string> + <string name="setup_group_look">Udseende</string> + <string name="setup_group_device">Denne enhed</string> + <string name="setup_group_art">Biblioteksgrafik</string> + <string name="flathub_eyebrow">LINUX-APPS</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Desktopapps til Linux-desktoppen, åbnet fra Desktop.</string> + <string name="flathub_setup">Konfigurer Flathub</string> + <string name="flathub_remove_title">Fjern %1$s?</string> + <string name="flathub_remove_note">Dets indstillinger og filer bevares.</string> + <string name="flathub_keep">Behold</string> + <string name="desktop_size">Størrelse på disk</string> + <string name="desktop_uninstall_title">Afinstaller desktoppen?</string> + <string name="desktop_all_systems">Alle</string> + <string name="gog_achievements">GOG-bedrifter</string> + <string name="gog_achievements_hint">Synkroniserer bedrifter for understøttede GOG-spil. Gælder fra næste start.</string> + <string name="gog_achievements_off">Bedrifter slået fra</string> + <string name="gog_achievements_on">Bedrifter aktiveret</string> + <string name="gog_achievements_signin">Log ind på GOG igen</string> + <string name="gog_achievements_uploaded">Bedrifter uploadet</string> + <string name="gog_achievements_pending">Upload af bedrift afventer</string> + <string name="gog_achievements_enable_store">Aktivér GOG-bedrifter i Butik-indstillinger</string> + <string name="gog_achievements_unavailable">Bedrifter ikke tilgængelige; se sessionslogs</string> + <string name="achievement_unlocked">Bedrift låst op</string> + <string name="touch_paste">Indsæt</string> + <string name="touch_dpad">Styrekryds</string> + <string name="touch_left_stick">Venstre stick</string> + <string name="touch_right_stick">Højre stick</string> + <string name="touch_menu">Menu</string> + <string name="touch_keyboard">Tastatur</string> + <string name="touch_dpad_up">Styrekryds op</string> + <string name="touch_dpad_down">Styrekryds ned</string> + <string name="touch_dpad_left">Styrekryds venstre</string> + <string name="touch_dpad_right">Styrekryds højre</string> + <string name="touch_title">Berøringsstyring</string> + <string name="touch_controller">Berøringscontroller</string> + <string name="touch_mouse">Mus</string> + <string name="touch_on">Til</string> + <string name="touch_off">Fra</string> + <string name="touch_speed_slow">Langsom</string> + <string name="touch_speed_normal">Normal</string> + <string name="touch_speed_fast">Hurtig</string> + <string name="touch_edit_layout">Rediger layout</string> + <string name="touch_droiddeck_menu">DroidDeck-menu</string> + <string name="touch_drag_hint">Træk kontroller ud på skærmen</string> + <string name="touch_arrange">Arrangér</string> + <string name="touch_discard">Kassér</string> + <string name="touch_saving">Gemmer…</string> + <string name="touch_icon">Ikon</string> + <string name="touch_narrower">B−</string> + <string name="touch_wider">B+</string> + <string name="touch_shorter">H−</string> + <string name="touch_taller">H+</string> + <string name="touch_hide">Skjul</string> + <string name="touch_color_opacity">Farve og gennemsigtighed…</string> + <string name="touch_copy_layout">Kopiér dette layout</string> + <string name="touch_layout_copied">Layout kopieret; indsæt det i berøringslayoutet for et vilkårligt spil</string> + <string name="touch_paste_layout">Indsæt kopieret layout</string> + <string name="touch_reset_layout">Nulstil til Steams standard</string> + <string name="touch_no_binding">Denne kontrol har ingen binding i Steam, der kan tildeles et ikon</string> + <string name="touch_icon_for">Ikon for %1$s</string> + <string name="touch_layout_color">Farve på berøringslayout</string> + <string name="touch_done">Udført</string> + <string name="touch_save_failed">Berøringslayoutet kunne ikke gemmes</string> + <string name="touch_saved">Berøringslayout gemt i Steam</string> + <string name="touch_app_id">App %1$d</string> + <string name="touch_label_hint">Etiket (valgfrit)</string> + <string name="touch_icons_missing">Steams ikoner blev ikke fundet</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d ikon fra Steam</item> + <item quantity="other">%1$d ikoner fra Steam</item> + </plurals> + <string name="touch_icon_color">Ikonfarve</string> + <string name="touch_button_color">Knapfarve</string> + <string name="touch_no_icon">Intet ikon</string> + + <string name="stores_sync_sign_in">Kontrollerer login…</string> + <string name="stores_sync_library">Henter dit bibliotek…</string> + <string name="stores_sync_game_list">Henter spilliste…</string> + <string name="stores_sync_details">Indlæser spildetaljer… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">Synkroniserer %1$d spil…</item> + <item quantity="other">Synkroniserer %1$d spil…</item> + </plurals> + <string name="stores_sync_progress">Synkroniserer… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Henter coverbilleder…</string> + <string name="stores_sync_failed">Synkronisering mislykkedes</string> + <string name="stores_sync_no_games">Der blev ikke fundet spil i dit %1$s-bibliotek</string> + <string name="stores_sync_amazon_no_answer">Amazon Games svarede ikke om dit bibliotek: opdater, eller log ind igen</string> + <string name="stores_sync_gog_unreachable">Kunne ikke oprette forbindelse til GOG</string> + <string name="stores_sync_gog_unreadable">Dit GOG-bibliotek kunne ikke læses</string> + <string name="stores_registering">Registrerer hos Steam…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Cardboard-tilstand</string> + <string name="cardboard_mode_hint">Billeder til venstre og højre øje til en VR-brille til telefonen.</string> + <string name="cardboard_source">Billede</string> + <string name="cardboard_source_screen">Virtuel skærm</string> + <string name="cardboard_source_sbs">Side-by-side-3D</string> + <string name="cardboard_viewer">Brille</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Ingen linse</string> + <string name="cardboard_ipd">Øjenafstand</string> + <string name="cardboard_screen_width">Skærmbredde</string> + <string name="cardboard_screen_distance">Skærmafstand</string> + <string name="cardboard_head_tracking">Hovedsporing</string> + <string name="cardboard_no_sensor">Denne enhed har ingen rotationssensor.</string> + <string name="cardboard_recenter">Centrer visning</string> + <string name="cardboard_recenter_button">Centrer</string> + <string name="cardboard_steamvr">SteamVR (eksperimentel)</string> + <string name="cardboard_steamvr_install">Installer</string> + <string name="cardboard_steamvr_install_hint">Downloader Valves SteamVR til ARM64 (571 MB).</string> + <string name="cardboard_steamvr_installing">Downloader og udpakker SteamVR…</string> + <string name="cardboard_steamvr_start">Start</string> + <string name="cardboard_steamvr_ready_hint">Kører SteamVR med dit hoved som headset; billedet skifter til side-by-side-3D.</string> + <string name="cardboard_steamvr_stop">Stop</string> + <string name="cardboard_steamvr_running_hint">SteamVR kører.</string> + <string name="cardboard_steamvr_unavailable">Ikke tilgængelig i denne session.</string> + <string name="cardboard_steamvr_failed">SteamVR svarede ikke; se sessionsloggen.</string> + <string name="drawer_component_downloads">Download komponenter</string> + <string name="drawer_component_downloads_hint">Downloads deles med siden Komponenter. Vælg en downloadet version ovenfor for at bruge den fra næste spilstart.</string> + <string name="drawer_component_choose_download">Vælg en version at downloade</string> + <string name="stores_fact_developer">Udvikler</string> + <string name="stores_fact_publisher">Udgiver</string> + <string name="stores_fact_released">Udgivet</string> + <string name="stores_hltb_main">Hovedhistorie</string> + <string name="stores_hltb_extras">+ Ekstra</string> + <string name="stores_hltb_completionist">Komplettering</string> + <string name="stores_hltb_hours">%1$s t</string> + <string name="stores_hltb_minutes">%1$d min</string> +</resources> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml new file mode 100644 index 000000000..620972946 --- /dev/null +++ b/app/src/main/res/values-de/strings.xml @@ -0,0 +1,2235 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>Beim ersten Start wird Steam heruntergeladen. Das kann ein paar Minuten dauern.</item> + <item>Standardmäßig pausiert Steam, wenn du die App verlässt, und macht bei deiner Rückkehr dort weiter, wo es aufgehört hat.</item> + <item>Dein Controller erscheint als Xbox-360-Pad. Ansonsten nutze die Bildschirmsteuerung.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>Beim ersten Start wird der Desktop eingerichtet. Das kann ein paar Minuten dauern.</item> + <item>Standardmäßig wird die Sitzung pausiert, wenn du die App verlässt, und macht bei deiner Rückkehr dort weiter, wo sie aufgehört hat.</item> + <item>Touch funktioniert wie ein Touchpad: Ziehen bewegt den Zeiger, Tippen klickt.</item> + <item>Dein Controller erscheint als Xbox-360-Pad. Ansonsten nutze die Bildschirmsteuerung.</item> + </string-array> + <string name="session_channel">Sitzung</string> + <string name="session_channel_description">Wird angezeigt, solange eine Steam- oder Desktop-Sitzung geöffnet ist</string> + <string name="session_notification">Steam läuft</string> + <string name="session_notification_downloads">Steam-Download aktiv</string> + <string name="session_notification_paused">Sitzung pausiert – tippe auf „Fortsetzen“, wenn du bereit bist</string> + <string name="resume_session">Fortsetzen</string> + <string name="suspend_now">Jetzt anhalten</string> + <string name="stop_session">Sitzung beenden</string> + <string name="frame_gen_title">Frame-Generierung</string> + <string name="frame_gen_hint">Modus der Frame-Generierung auswählen</string> + <string name="frame_gen_off">Aus</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG Adaptiv %1$d</string> + <string name="lsfg_needs_steam">Benötigt Lossless Scaling auf Steam</string> + <string name="lsfg_needs_install">Installiere Lossless Scaling in Steam oder importiere Lossless.dll</string> + <string name="lsfg_import">Lossless.dll importieren…</string> + <string name="lsfg_using_steam">Kopie aus Steam wird verwendet</string> + <string name="lsfg_using_import">Importierte Kopie wird verwendet</string> + <string name="lsfg_pick_title">Lossless.dll auswählen</string> + <string name="lsfg_imported">Lossless.dll importiert</string> + <string name="lsfg_import_unchanged">Diese Lossless.dll wird bereits verwendet</string> + <string name="lsfg_import_not_owned">Dieses Steam-Konto besitzt Lossless Scaling nicht</string> + <string name="lsfg_import_invalid">Diese Datei ist nicht die Lossless.dll von Lossless Scaling</string> + <string name="lsfg_import_failed">LSFG konnte aus dieser Lossless.dll nicht vorbereitet werden</string> + <string name="lsfg_problem_missing">LSFG benötigt Lossless Scaling aus Steam oder eine importierte Lossless.dll</string> + <string name="lsfg_problem_failed">LSFG konnte seine Shader nicht aus Lossless.dll vorbereiten</string> + <string name="hud_fg_unsupported">%1$s: nicht unterstützt</string> + <string name="hud_fg_failed">%1$s: Engine-Fehler</string> + <string name="hud_fg_needs_lossless">%1$s: benötigt Lossless Scaling</string> + <string name="hud_fg_starting">%1$s %2$s · startet</string> + <string name="second_library_import_hint">Fügt eine Steam-Bibliothek hinzu und durchsucht sie nach Windows-Spielen. Eigene Spiele nutzen ihre Steam-Einträge, der Rest wird zu Nicht-Steam-Verknüpfungen.</string> + <string name="game_env_title">Spielumgebung</string> + <string name="game_env_hint">Umgebungsvariablen für Proton-Spiele.</string> + <string name="game_env_edit">Bearbeiten</string> + <string name="game_env_shared">Alle Proton-Spiele</string> + <string name="game_env_scope">Profil</string> + <string name="game_env_app_id">Steam-App-ID</string> + <string name="game_env_open">Öffnen</string> + <string name="game_env_profile">Spiel %1$s</string> + <string name="game_env_add">Variable hinzufügen</string> + <string name="game_env_name">Variablenname</string> + <string name="game_env_value">Wert</string> + <string name="game_env_save">Speichern</string> + <string name="game_env_remove">Entfernen</string> + <string name="game_env_restore">Wieder erben</string> + <string name="game_env_reset">Umgebungsüberschreibungen löschen</string> + <string name="game_env_unset">Für dieses Profil aufheben</string> + <string name="game_env_empty">Keine Variablen gesetzt.</string> + <string name="game_env_inherited">Geerbt</string> + <string name="game_env_custom">Eigene Variable</string> + <string name="game_env_error">Laden oder Speichern fehlgeschlagen. Prüfe den freien Speicherplatz und versuch es noch einmal.</string> + <string name="game_env_invalid">Verwende einen gültigen Namen und einen Wert mit bis zu 8192 Zeichen.</string> + <string name="game_env_unused">Die Standard-Proton-Laufzeit liest diese Variable nicht.</string> + <string name="game_env_loading">Wird geladen…</string> + <string name="game_env_feature">Überschreibt das D3D12-Feature-Level. 12_2 meldet DirectX-12-Ultimate-Funktionen; nicht unterstützte Funktionen können fehlschlagen. Für automatische Erkennung nicht setzen.</string> + <string name="game_env_shader">Überschreibt das D3D12-Shader-Model. 6_9 erfordert ein ausreichend aktuelles VKD3D und passende GPU-Funktionen. Für automatische Erkennung nicht setzen.</string> + <string name="game_env_cache">Lass das Mesa-Shader-Caching aktiviert, um wiederholtes Kompilieren zu vermeiden. false aktiviert das Caching.</string> + <string name="game_env_cache_size">Maximale Größe des Mesa-Shader-Caches, z. B. 1G. Größere Caches belegen mehr Speicherplatz.</string> + <string name="game_env_vkd3d_config">VKD3D-Optionen für D3D12. nodxr deaktiviert Raytracing; nur bei Spielen verwenden, die das brauchen.</string> + <string name="game_env_proton_log">Auf 1 setzen für ein Proton-Diagnoseprotokoll. Protokollieren kann die Leistung mindern und Speicherplatz belegen.</string> + <string name="game_env_wined3d">Auf 1 setzen, um für D3D9–11-Kompatibilitätstests das OpenGL-basierte WineD3D statt DXVK zu verwenden.</string> + <string name="game_env_no_ntsync">Auf 1 setzen, um dieses Spiel von ntsync fernzuhalten (droiddeck-ntsync oder ein Kerneltreiber); es nutzt dann droiddeck-esync, wo ein Pack existiert, sonst droiddeck-fsync.</string> + <string name="game_env_no_esync">Auf 1 setzen, um droiddeck-esync für dieses Spiel abzuschalten; es nutzt stattdessen droiddeck-fsync oder droiddeck-ntsync, wo auch fsync aus ist.</string> + <string name="game_env_no_fsync">Auf 1 setzen, um dieses Spiel von droiddeck-fsync fernzuhalten; es nutzt dann droiddeck-esync, wo ein Pack existiert, sonst droiddeck-ntsync.</string> + <string name="game_env_fsync_first">Auf 1 setzen, um dieses Spiel mit droiddeck-fsync auszuführen, oder auf 0, um droiddeck-esync vorzuziehen, wo ein Pack zu seinem Proton passt.</string> + <string name="game_env_xalia">Auf 0 setzen, um Protons Gamepad-Navigationshilfe für dieses Spiel zu deaktivieren.</string> + <string name="game_env_hud">DXVK-Overlay für D3D9–11. fps zeigt die Bildrate an; ohne Wert wird es ausgeblendet.</string> + <string name="game_env_dxvk_config">DXVK-Einstellungen, durch Semikolons getrennt. Neuere Builds nutzen dxvk.maxFrameRate zur Bildratenbegrenzung; fürs normale Spielen besser das Bildratenlimit der Sitzung verwenden.</string> + <string name="game_env_vkd3d_limit">D3D12-Bildratenlimit. 0 hebt das Limit auf.</string> + <string name="game_env_glthread">Mesa-OpenGL-Threading. true kann CPU-limitierten OpenGL-Spielen helfen; pro Spiel testen.</string> + <string name="sync_backend_title">Wine-Sync</string> + <string name="sync_backend_hint">Wie Spiele ihre Threads im Gleichschritt halten. Gilt ab der nächsten Sitzung.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">FEX-Voreinstellung</string> + <string name="fex_next_launch">Gilt ab dem nächsten Spielstart.</string> + <string name="game_settings_title">Spiele</string> + <string name="fex_default_label">FEX-Standard</string> + <string name="fex_default_detail">Verwendet die Standardwerte von FEX.</string> + <string name="fex_stability_label">Stabilität</string> + <string name="fex_stability_detail">Sicherste, langsamste Einstellungen.</string> + <string name="fex_compatibility_label">Kompatibilität</string> + <string name="fex_compatibility_detail">Volles TSO mit Multiblock.</string> + <string name="fex_intermediate_label">Mittel</string> + <string name="fex_intermediate_detail">Ausgewogenes TSO mit reduzierter x87-Genauigkeit.</string> + <string name="fex_performance_label">Leistung</string> + <string name="fex_performance_detail">TSO komplett deaktiviert; Multiblock.</string> + <string name="fex_performance_tso_label">Leistung + TSO</string> + <string name="fex_performance_tso_detail">Leistungseinstellungen mit aktiviertem Haupt-TSO.</string> + <string name="fex_extreme_label">Extrem</string> + <string name="fex_extreme_detail">Deaktiviert Prüfungen auf selbstmodifizierenden Code. JIT-Spiele und DRM können ausfallen.</string> + <string name="fex_extreme_tso_label">Extrem + TSO</string> + <string name="fex_extreme_tso_detail">Extrem-Einstellungen mit aktiviertem Haupt-TSO.</string> + <string name="fex_extreme_gn_label">Extrem-gn</string> + <string name="fex_extreme_gn_detail">Leistung mit TSC- und Metadaten-Hinweisen. Vor Extrem ausprobieren.</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">Leistung mit vollständigen Codeprüfungen und verstecktem Hypervisor-Bit.</string> + <string name="game_env_custom_value">Eigener Wert</string> + <string name="game_env_select_value">Wert auswählen</string> + <string name="user_apps_section">Emulatoren & Apps</string> + <string name="user_apps_add">Hinzufügen</string> + <string name="user_apps_add_detail">Eigene App oder eigenes Spiel</string> + <string name="user_apps_kind_script">Skript</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d %%</string> + <string name="user_apps_starting">Wird gestartet…</string> + <string name="user_apps_added">%1$s hinzugefügt</string> + <string name="user_apps_removed">%1$s entfernt</string> + <string name="user_apps_back">Desktop</string> + <string name="user_apps_gone">Diese App gibt es nicht mehr. Wähle sie erneut auf dem Desktop aus.</string> + <string name="user_apps_open">%1$s öffnen</string> + <string name="user_apps_remove">Entfernen</string> + <string name="user_apps_remove_confirm">Zum Entfernen erneut drücken</string> + <string name="user_apps_stop_session">Zum Entfernen Sitzung beenden</string> + <string name="user_apps_script_linked">Führt %1$s aus seinem Ordner aus, der an Ort und Stelle in die Sitzung eingebunden wird. Beim Entfernen der App bleibt dieser Ordner unangetastet.</string> + <string name="user_apps_script_copied">Führt %1$s aus einer Kopie seines Ordners in DroidDeck aus. Beim Entfernen der App wird die Kopie gelöscht, nicht das Original.</string> + <string name="user_apps_runtime_required">Installiere zuerst die Linux-Laufzeit</string> + <string name="user_apps_file_gone">Die Datei ist nicht mehr vorhanden</string> + <string name="user_apps_no_space">Nicht genug freier Speicherplatz: Zum Kopieren des Ordners werden %1$s benötigt</string> + <string name="user_apps_copying">%1$s wird kopiert</string> + <string name="user_apps_failed">Hinzufügen der App fehlgeschlagen</string> + <string name="user_apps_github_checking">Suche nach einem Release von %1$s</string> + <string name="user_apps_github_unreachable">Releases von %1$s konnten nicht gelesen werden</string> + <string name="user_apps_github_no_appimage">Kein Release von %1$s enthält ein ARM64- oder x86_64-AppImage</string> + <string name="user_apps_downloading">%1$s wird heruntergeladen</string> + <string name="user_apps_download_failed">Download fehlgeschlagen</string> + <string name="user_apps_update_failed">Aktualisieren der App fehlgeschlagen</string> + <string name="user_apps_icon_failed">Dieses Symbol konnte nicht gelesen werden</string> + <string name="user_apps_saved">%1$s gespeichert</string> + <string name="user_apps_edit">Bearbeiten</string> + <string name="user_apps_check_updates">Nach Updates suchen</string> + <string name="user_apps_checking_updates">Suche nach Updates…</string> + <string name="user_apps_update_to">Auf %1$s aktualisieren</string> + <string name="user_apps_up_to_date">Aktuell · %1$s</string> + <string name="user_apps_updated">%1$s aktualisiert</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">App hinzufügen</string> + <string name="add_app_tab_script">Skript</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">Eine .sh-Datei. Sie läuft aus ihrem eigenen Ordner, im Vollbild unter gamescope.</string> + <string name="add_app_appimage_hint">Ein ARM64-AppImage (aarch64) oder ein x86_64-AppImage, das über FEX läuft. Es wird einmalig in die Linux-Laufzeit entpackt.</string> + <string name="add_app_github_hint">Installiert das ARM64-AppImage aus dem neuesten Release des Repositorys oder dessen x86_64-Version über FEX.</string> + <string name="app_fex_title">FEX verwenden</string> + <string name="app_fex_auto">Auto</string> + <string name="app_fex_on">An</string> + <string name="app_fex_off">Aus</string> + <string name="app_fex_hint">FEX führt x86-Linux-Programme auf diesem ARM64-Gerät aus. Auto nutzt es, wenn das Programm für x86 gebaut ist.</string> + <string name="app_fex_x86">%1$s über FEX</string> + <string name="app_fex_not_ready">Dieses Programm ist für %1$s-PCs gebaut und läuft über FEX, das noch nicht eingerichtet ist. Öffne Steam einmal, während du angemeldet bist: DroidDeck lässt es FEX und die nötige Steam Linux Runtime herunterladen, danach lässt sich das Programm öffnen.</string> + <string name="session_end_program">Dieses Programm</string> + <string name="session_end_fex_missing">%1$s ist für x86-PCs gebaut und läuft über FEX, das noch nicht installiert ist. Starte Steam einmal, während du angemeldet bist, und lass es FEX und die Steam Linux Runtime herunterladen. Starte es danach erneut.</string> + <string name="add_app_flatpak_hint">Eine Flathub-App-ID oder ihre Flathub-Seite. Flatpak wird zuerst eingerichtet, falls noch nicht geschehen.</string> + <string name="add_app_file">Datei</string> + <string name="add_app_no_file">Keine Datei ausgewählt</string> + <string name="add_app_choose_file">Datei auswählen</string> + <string name="add_app_change_file">Ändern</string> + <string name="add_app_pick_script">Skript auswählen</string> + <string name="add_app_pick_appimage">AppImage auswählen</string> + <string name="add_app_pick_icon">Symbol auswählen</string> + <string name="add_app_repository">Repository</string> + <string name="add_app_repository_placeholder">besitzer/repo oder ein github.com-Link</string> + <string name="add_app_repository_invalid">Kein GitHub-Repository</string> + <string name="add_app_flatpak_id">App-ID</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">Keine Flathub-App-ID</string> + <string name="add_app_flatpak_bundle_hint">Oder eine .flatpak-Datei für eine App, die es nicht auf Flathub gibt.</string> + <string name="add_app_pick_flatpak">.flatpak-Datei auswählen</string> + <string name="add_app_source">Quelle</string> + <string name="add_app_appearance">Auf deinem Desktop</string> + <string name="add_app_preview_name">Neue App</string> + <string name="add_app_name">Name</string> + <string name="add_app_name_placeholder">Aus der App</string> + <string name="add_app_icon">Symbol</string> + <string name="add_app_icon_default">Standardsymbol</string> + <string name="add_app_choose_icon">Symbol auswählen</string> + <string name="add_app_clear_icon">Entfernen</string> + <string name="add_app_icon_suggestions">Aus dem Repository</string> + <string name="add_app_icon_searching">Suche nach Symbolen…</string> + <string name="add_app_icon_reset">Standard</string> + <string name="edit_app_title">%1$s bearbeiten</string> + <string name="edit_app_save">Speichern</string> + <string name="add_app_checking_folder">Ordner wird geprüft…</string> + <string name="add_app_folder_linked">Sein Ordner wird in die Sitzung eingebunden: %1$s</string> + <string name="add_app_folder_copied">Sein Ordner enthält Linux-Programme, die im gemeinsamen Speicher nicht laufen können, daher wird er in DroidDeck kopiert (%1$s).</string> + <string name="add_app_confirm">Hinzufügen</string> + <string name="add_app_cancel">Abbrechen</string> + <string name="add_app_runtime_required">Installiere zuerst die Linux-Laufzeit unter „Einrichtung“.</string> + <string name="add_app_busy">Warte, bis %1$s fertig ist.</string> + <string name="steam_compat_label">Kompatibel</string> + <string name="proton_compatible_note">Builds, die in Steams Menü „Kompatibilität“ mit „%1$s“ markiert sind, enthalten die Präfix-Korrekturen von DroidDeck und starten hier Spiele. Standard ist Proton Experimental ARM64 (%1$s) bzw. Proton 11.0 ARM64 (%1$s), sobald dieser neuere Build installiert ist. Valves eigene Proton-Einträge ohne Markierung können auf diesem Gerät kein Spiel starten.</string> + <string name="proton_installed_compatible">Installiert: %1$s · %2$s</string> + <string name="common_cancel">Abbrechen</string> + <string name="common_auto">Auto</string> + <string name="common_never">Nie</string> + <string name="common_always">Immer</string> + <string name="common_uninstall">Deinstallieren</string> + <string name="common_check">Prüfen</string> + <string name="common_update">Aktualisieren</string> + <string name="common_checking">Wird geprüft…</string> + <string name="common_working">Wird ausgeführt…</string> + <string name="common_forget">Vergessen</string> + <string name="common_add_ellipsis">Hinzufügen…</string> + <string name="common_use">Verwenden</string> + <string name="common_applies_next_session">Gilt ab der nächsten Sitzung.</string> + <string name="mode_steam_title">Steam-Sitzung</string> + <string name="mode_desktop_title">Desktop-Sitzung</string> + <string name="mode_display">Anzeige</string> + <string name="mode_resolution">Auflösung</string> + <string name="mode_res_720">Bis zu 720p</string> + <string name="mode_res_900">Bis zu 900p</string> + <string name="mode_res_1080">Bis zu 1080p</string> + <string name="mode_res_native">Nativ</string> + <string name="mode_res_default">%1$s (Standard)</string> + <string name="mode_res_custom_value">Benutzerdefiniert · %1$d×%2$d</string> + <string name="mode_res_custom">Benutzerdefiniert…</string> + <string name="mode_res_note">720p kann Menüs reaktionsschneller machen.</string> + <string name="mode_ratio">Seitenverhältnis</string> + <string name="mode_ratio_custom">Durch die benutzerdefinierte Auflösung festgelegt.</string> + <string name="mode_ratio_auto">Auto verwendet mindestens 16:9.</string> + <string name="mode_fps">Bildratenlimit</string> + <string name="mode_fps_note">Begrenzt die gesamte Sitzung. Das Display wechselt auf eine Bildwiederholrate, die ein ganzzahliges Vielfaches des Limits ist.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">HDR10-Ausgabe</string> + <string name="mode_hdr_unavailable">Nicht verfügbar: %1$s.</string> + <string name="mode_hdr_restart">Starte die App neu, um dies anzuwenden.</string> + <string name="mode_drivers">Treiber</string> + <string name="mode_gpu_drivers">GPU-Treiber</string> + <string name="mode_gpu_drivers_hint">Für Steam und Desktop gemeinsam. Wird auf der Seite „Komponenten“ festgelegt.</string> + <string name="mode_touch_controls">Touch & Steuerung</string> + <string name="mode_touch">Touch</string> + <string name="mode_touch_touchpad">Touchpad</string> + <string name="mode_touch_direct">Direkt</string> + <string name="mode_touch_note">Auto: Touchpad auf dem Desktop, direkt in Steam.</string> + <string name="mode_osc">Bildschirmsteuerung</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">Auto zeigt die Steuerung an, wenn kein Controller verbunden ist.</string> + <string name="osc_steam_touch">Steam-Touch (Beta)</string> + <string name="osc_style_next_session">Steams Touch-Steuerung startet mit der nächsten Steam-Sitzung</string> + <string name="osc_steam_layout">Steam-Touch-Layout</string> + <string name="osc_steam_layout_hint">Touch-Steuerung dieses Spiels verschieben, skalieren oder ausblenden; wird in Steam gespeichert</string> + <string name="mode_controller">Controller</string> + <string name="mode_controller_hint">Wie Steam deinen Controller erkennt. Gilt ab der nächsten Sitzung.</string> + <string name="mode_controller_deck">Steam-Deck-Controller</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Xbox-360-Controller</string> + <string name="mode_controller_note">Deck: Schnellzugriffstaste und Gyro. Steam Controller: dasselbe, mit Griffsensor an den Rücktasten. DualShock 4 und DualSense: Gyro, die Trackpads dienen als deren Touchpad. Xbox 360: Schnellzugriff mit Guide+A öffnen.</string> + <string name="mode_back">Zurück</string> + <string name="back_menu_then_qam">Menü → QAM</string> + <string name="back_qam_then_menu">QAM → Menü</string> + <string name="mode_session">Sitzung</string> + <string name="mode_suspend">Verhalten im Hintergrund</string> + <string name="mode_suspend_hint">Wenn die App ausgeblendet ist oder der Bildschirm ausgeht.</string> + <string name="mode_suspend_manual">Manuell</string> + <string name="mode_suspend_native">Nativ</string> + <string name="mode_suspend_steam_note">Auto pausiert und setzt fort, sobald die App wieder sichtbar ist. Nativ bereitet Steam zuerst auf den Ruhemodus vor. Manuell wartet auf „Fortsetzen“. Nie lässt alles weiterlaufen.</string> + <string name="mode_background_downloads">Downloads weiterlaufen lassen</string> + <string name="mode_background_downloads_hint">Verzögert das Anhalten im Hintergrund, solange Steam aktiv herunterlädt.</string> + <string name="mode_suspend_note">Auto pausiert im Hintergrund und setzt fort, sobald die App wieder sichtbar ist. Manuell wartet auf „Fortsetzen“. Nie lässt alles weiterlaufen.</string> + <string name="mode_startup">Start</string> + <string name="mode_steam_startup">Steam mit DroidDeck starten</string> + <string name="mode_steam_startup_hint">Öffnet die Steam-Sitzung, wenn DroidDeck startet.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">Suche nach Releases…</string> + <string name="mode_decky_ready">Bereit zur Installation von %1$s.</string> + <string name="mode_decky_none">Kein kompatibler Build gefunden. Versuch es später noch einmal.</string> + <string name="mode_decky_installed">Installiert · %1$s</string> + <string name="mode_decky_update">Update verfügbar · %1$s</string> + <string name="mode_decky_current">Aktuell · %1$s</string> + <string name="mode_decky_loader">Loader</string> + <string name="mode_decky_install_latest">Neueste Version installieren</string> + <string name="mode_decky_on">Startet mit Steam. Solange aktiv, können andere Apps auf diesem Gerät Steam möglicherweise steuern.</string> + <string name="mode_decky_off">Aus. Der Remote-Debugging-Port von Steam bleibt geschlossen.</string> + <string name="mode_decky_remove_title">Decky Loader deinstallieren?</string> + <string name="mode_decky_remove_text">Entfernt den Loader. Deine Plugins und Einstellungen bleiben erhalten.</string> + <string name="mode_client">Client</string> + <string name="mode_mangoapp">Leistungs-Overlay</string> + <string name="mode_mangoapp_hint">Deaktivieren, wenn Spiele im Deck-Modus abstürzen oder schwarz werden. Gilt ab der nächsten Sitzung.</string> + <string name="mode_steam_repair">Steam-Client reparieren</string> + <string name="mode_steam_repair_hint">Lädt den Steam-Client beim nächsten Start von Steam erneut herunter. Nutze dies, wenn Steam im Ladebildschirm hängen bleibt. Spiele, Anmeldung und Einstellungen bleiben erhalten.</string> + <string name="mode_steam_repair_queued">Der Steam-Client wird beim nächsten Start von Steam erneut heruntergeladen.</string> + <string name="mode_steam_repair_button">Reparieren</string> + <string name="mode_added_art">Grafiken</string> + <string name="mode_sgdb_key">SteamGridDB-API-Schlüssel</string> + <string name="mode_sgdb_key_yours">Dein Schlüssel</string> + <string name="mode_sgdb_edit">Bearbeiten…</string> + <string name="mode_added_art_hint">Ergänzt fehlende Grafiken aus Steam, dann aus SteamGridDB. Deine eigene cover.jpg, hero.jpg, logo.png oder icon.png im Spielordner hat Vorrang.</string> + <string name="summary_remove_game">%1$s entfernen</string> + <string name="mode_added_remove_confirm">Entfernen?</string> + <string name="games_add">Spiel hinzufügen</string> + <string name="games_add_title">Spiel hinzufügen:</string> + <string name="games_pick_exe_title">EXE-Datei auswählen:</string> + <string name="games_add_unreachable">Dieser Ort ist in der Sitzung nicht verfügbar</string> + <string name="mode_added_exe_check">Diese EXE-Datei prüfen</string> + <string name="wincomp_title">Windows-Komponenten</string> + <string name="wincomp_pad_hint">Mit LB / RB zwischen den Abschnitten wechseln.</string> + <string name="wincomp_card_none">Keine</string> + <string name="wincomp_applies">Windows-DLLs aus der Komponentenliste, die Proton nicht enthält. Aktiviere die, die dieses Spiel braucht: Sie landen im eigenen Windows-Ordner des Spiels, nie bei seinen Dateien, und bleiben erhalten, egal mit welchem Proton es läuft.</string> + <string name="wincomp_offline">Komponentenliste nicht erreichbar. Bereits heruntergeladene Komponenten lassen sich weiterhin umschalten.</string> + <string name="wincomp_recommended">Für dieses Spiel empfohlen</string> + <string name="wincomp_checking">Prüfe, was dieses Spiel braucht…</string> + <string name="wincomp_when_online">Verfügbar, wenn online</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">%1$d Komponente wird aktiviert…</item> + <item quantity="other">%1$d Komponenten werden aktiviert…</item> + </plurals> + <string name="wincomp_auto_section">Für dieses Spiel aktiv</string> + <string name="wincomp_auto_steam">Steam installiert %1$s mit %2$s (App %3$s)</string> + <string name="wincomp_auto_gog">Aus der GOG-Liste für dieses Spiel</string> + <string name="wincomp_auto_epic">Aus den Epic-Voraussetzungen für dieses Spiel</string> + <string name="wincomp_auto_amazon">Aus den Amazon-Installationsschritten für dieses Spiel</string> + <string name="wincomp_steam_by_name">Steam installiert %1$s mit App %2$s, per Name zugeordnet</string> + <string name="wincomp_suggestions">Ebenfalls aufgeführt</string> + <string name="wincomp_suggestion_reason">Aufgeführt von %1$s; hier keine Komponente</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">Das Spiel liefert %1$s mit</string> + <string name="wincomp_found_shipped">Das Spiel hat %1$s (funktioniert oft so)</string> + <string name="wincomp_already_here">Bereits in diesem Spiel</string> + <string name="wincomp_no_recommendation">Nichts im Spielordner deutet auf eine Komponente hin.</string> + <string name="wincomp_active">Für dieses Spiel installiert (%1$d)</string> + <string name="wincomp_all">Alle Komponenten (%1$d)</string> + <string name="wincomp_installers">Benötigen einen Windows-Installer (%1$d)</string> + <string name="wincomp_installers_note">Das sind eigenständige Setup-Programme ohne Paket, das sich zerlegen ließe, und für sie gibt es noch keine Aufzeichnung dessen, was sie installieren. Die meisten sind alte Laufzeiten, die das Mono in Proton bereits ersetzt; Aufzeichnungen kommen hinzu, sobald sie erstellt werden.</string> + <string name="wincomp_show">Anzeigen</string> + <string name="wincomp_hide">Ausblenden</string> + <string name="wincomp_needs_installer">Führt ein eigenes Setup-Programm aus (noch keine Aufzeichnung)</string> + <string name="wincomp_unsupported">Hier noch nicht unterstützt</string> + <string name="wincomp_loading">Komponenten werden geladen…</string> + <string name="wincomp_failed">Installation fehlgeschlagen: %1$s</string> + <string name="wincomp_downloaded">Heruntergeladen</string> + <string name="wincomp_downloading">%1$s wird heruntergeladen</string> + <string name="wincomp_installing">Wird installiert: %1$s</string> + <string name="wincomp_reverting">Wird deaktiviert…</string> + <string name="wincomp_download_size">%1$s Download</string> + <string name="wincomp_next_launch">Wirkt beim nächsten Spielstart.</string> + <string name="mode_stretch169">16:9-Spiele strecken</string> + <string name="mode_stretch169_hint">Auf Bildschirmen, die quadratischer als 16:9 sind, werden Spiele bildfüllend gestreckt statt mit Balken angezeigt. Gilt ab der nächsten Sitzung.</string> + <string name="mode_audio">Audio</string> + <string name="mode_directaudio">DirectAudio für Spiele</string> + <string name="mode_directaudio_hint">Umgeht PulseAudio für geringere Latenz.</string> + <string name="mode_mic">Mikrofon</string> + <string name="mode_mic_hint">Für Sprachchat.</string> + <string name="mode_network">Netzwerk</string> + <string name="mode_wifi_discovery">Wi-Fi-Erkennung</string> + <string name="mode_wifi_discovery_hint">Wi-Fi-Namen und Netzwerke in der Nähe in Steam anzeigen.</string> + <string name="mode_wifi_location_off">Standortdienste sind deaktiviert.</string> + <string name="mode_wifi_permission_denied">Standortberechtigung nicht erteilt.</string> + <string name="mode_wifi_open_settings">Einstellungen öffnen</string> + <string name="mode_wifi_continue">Weiter</string> + <string name="mode_wifi_explain_title">Wi-Fi-Erkennung aktivieren?</string> + <string name="mode_wifi_explain_text">Android verlangt die Standortberechtigung, um Wi-Fi-Namen zu lesen und Netzwerke in der Nähe zu finden. DroidDeck nutzt diesen Zugriff, um diese Netzwerke in Steam anzuzeigen. Wir verfolgen deinen Standort nicht. Der Internetzugang funktioniert auch ohne diese Berechtigung.</string> + <string name="mode_storage">Spielspeicher</string> + <string name="mode_storage_auto">Automatisch - die SD-Karte, falls vorhanden, plus deine Games-Ordner</string> + <string name="mode_storage_auto_none">Automatisch - deine Games-Ordner plus die SD-Karte, falls vorhanden (derzeit keine)</string> + <string name="mode_storage_internal">Nur intern</string> + <string name="mode_storage_folder">Ordner: %1$s</string> + <string name="mode_second_library">Zweite Bibliothek</string> + <string name="mode_storage_note">Spiele, die Daten von der SD-Karte oder aus dem gemeinsamen Speicher streamen, können ruckeln.</string> + <string name="mode_storage_diagnostics">Speicherdiagnose</string> + <string name="mode_storage_diagnostics_hint">Zeichnet Zuweisungsergebnisse und stichprobenartige Speicher-Timings für „Logs teilen“ auf. Kann Installationen verlangsamen, solange aktiv. Gilt ab der nächsten Sitzung.</string> + <string name="mode_choose_folder">Ordner auswählen…</string> + <string name="mode_custom_res_title">Benutzerdefinierte Auflösung</string> + <string name="mode_custom_res_text">Ersetzt Obergrenze und Seitenverhältnis. Eine Größe, die nicht zum Bildschirm passt, bekommt Balken.</string> + <string name="mode_width">Breite</string> + <string name="mode_height">Höhe</string> + <string name="mode_custom_res_range">Zwischen 320×240 und 3840×2160.</string> + <plurals name="mode_added_count"> + <item quantity="one">%1$d Spiel</item> + <item quantity="other">%1$d Spiele</item> + </plurals> + <string name="common_ok">OK</string> + <string name="common_delete">Löschen</string> + <string name="common_restore">Wiederherstellen</string> + <string name="common_download">Herunterladen</string> + <string name="common_delete_named">%1$s löschen</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Laufzeittreiber</string> + <string name="comp_runtime_driver_hint">Wird von Steam, Spielen und dem Desktop genutzt. Gilt ab der nächsten Sitzung.</string> + <string name="comp_import_turnip">Turnip-ZIP importieren…</string> + <string name="comp_display_driver">Anzeigetreiber</string> + <string name="comp_display_driver_hint">Bringt in beiden Modi die Frames auf den Bildschirm. Starte die App neu, um dies anzuwenden.</string> + <string name="comp_import_adrenotools">AdrenoTools-ZIP importieren…</string> + <string name="comp_never">nie</string> + <string name="comp_nightlies_checked">Nightlies geprüft: %1$s</string> + <string name="comp_title">Komponenten</string> + <string name="comp_about">Über Komponenten</string> + <string name="comp_check_drivers">Nach neuen Treibern suchen</string> + <string name="comp_check_nightlies">Nightlies nach neuen Paketen durchsuchen</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">Das hier gewählte Proton führt jedes Spiel aus, das in Steam kein eigenes gewählt hat, und eine Änderung in Steam erscheint auch hier. Steam ersetzt Valves Proton bei Updates; die Originale werden pro Build aufbewahrt.</string> + <string name="comp_scope">Konfiguration</string> + <string name="comp_profile_configure">Profil konfigurieren</string> + <string name="comp_profile">Profil</string> + <string name="comp_scope_general">Standardprofil</string> + <string name="comp_scope_general_detail">Wird automatisch von Spielen ohne eigene Überschreibungen verwendet.</string> + <string name="comp_profile_game_detail">Gespeicherte Einstellungen, die beim Start von %1$s automatisch verwendet werden.</string> + <string name="comp_game_hint">Wähle nur die Einstellungen, die dieses Spiel braucht. Alles andere nutzt weiterhin das Standardprofil.</string> + <string name="comp_game_fex">FEX-Voreinstellung</string> + <string name="comp_game_fex_inherited">Voreinstellung des Standardprofils wird verwendet.</string> + <string name="comp_game_fex_override">Eigene Voreinstellung für dieses Spiel gespeichert.</string> + <string name="comp_game_fex_custom">Eigene FEX-Variablen</string> + <string name="comp_game_use_general">Standard verwenden</string> + <string name="comp_game_compatibility">Spielkompatibilität</string> + <string name="comp_game_environment_hint">Umgebungsoptionen für Proton, DXVK, VKD3D und Synchronisation in diesem Spiel.</string> + <string name="comp_game_proton_inherited">Proton des Standardprofils wird verwendet.</string> + <string name="comp_game_proton_override">Eigenes Proton für dieses Spiel über die Steam-Kompatibilitätszuordnung gespeichert.</string> + <string name="comp_no_proton_short">Kein Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Nutzt das Paket des Standardprofils aus %1$s, sofern dieses Spiel kein anderes Paket wählt.</string> + <string name="comp_game_component_general">Nutzt das Komponentenpaket des Standardprofils.</string> + <string name="comp_game_component_override">Nutzt %1$s nur für dieses Spiel.</string> + <string name="comp_game_runtime_options">Laufzeitoptionen</string> + <string name="comp_game_environment_other_hint">Weitere spielspezifische Umgebungsoptionen für Proton, DXVK, VKD3D-Proton und Synchronisation.</string> + <string name="comp_game_saved_next">Das Profil von %1$s ist für den nächsten Spielstart gespeichert.</string> + <string name="game_no_steam_title">Ohne Steam starten</string> + <string name="game_no_steam_detail">Für ein Spiel, das sich sofort schließt: startet es ohne Protons Steam-Einrichtung. Steam-Funktionen wie das Overlay sind dafür deaktiviert.</string> + <string name="game_no_steam_on">An</string> + <string name="comp_game_proton_live">Die Proton-Auswahl für %1$s ist gespeichert und jetzt in Steam verfügbar.</string> + <string name="comp_game_proton_next">Die Proton-Auswahl für %1$s ist gespeichert. Starte Steam neu, bevor du das Spiel startest.</string> + <string name="comp_game_proton_not_runnable">%1$s kann das gewählte Proton auf diesem Gerät nicht verwenden.</string> + <string name="comp_game_load_failed">Profil von %1$s konnte nicht geladen werden: %2$s</string> + <string name="comp_game_save_failed">Das Spielprofil konnte nicht gespeichert werden: %1$s</string> + <string name="comp_game_reset">Spielprofil zurücksetzen</string> + <string name="comp_game_reset_title">Profil von %1$s zurücksetzen?</string> + <string name="comp_game_reset_body">Überschreibungen dieses Spiels für Proton, FEX, Komponentenpaket, Umgebung, Texturfilterung und Windows-Komponenten entfernen? Es nutzt dann das Standardprofil.</string> + <string name="comp_game_reset_done">%1$s nutzt jetzt das Standardprofil.</string> + <string name="comp_game_reset_failed">Das Spielprofil konnte nicht vollständig zurückgesetzt werden: %1$s</string> + <string name="comp_default_live">%1$s führt jetzt jedes Spiel aus, das in Steam kein eigenes Proton gewählt hat.</string> + <string name="comp_default_next">%1$s führt ab dem nächsten Start von Steam jedes Spiel aus, das kein eigenes Proton gewählt hat.</string> + <string name="comp_default_not_runnable">%1$s kann hier keine Spiele starten und daher nicht der Standard sein.</string> + <string name="comp_default_failed">Das Standard-Proton konnte nicht gespeichert werden: %1$s</string> + <string name="comp_swapped">%1$s · %2$d ausgetauscht</string> + <string name="comp_all_original">%1$s · alles original</string> + <string name="comp_game_running_suffix">%1$s · Spiel läuft</string> + <string name="comp_esync_pack">%1$s · droiddeck-esync-Pack</string> + <string name="comp_esync_wanted">%1$s · noch kein droiddeck-esync-Pack</string> + <string name="comp_esync_builtin">%1$s · esync integriert, kein Pack nötig</string> + <string name="comp_gpu_drivers">GPU-Treiber</string> + <string name="comp_reapplied">%1$s · beim Start erneut angewendet %2$s</string> + <string name="comp_next_queued">Als Nächstes: %1$s · nach Ende des Spiels</string> + <string name="comp_in_use">%1$s in Verwendung: %2$s</string> + <string name="comp_waits">Wartet, bis das Spiel beendet ist</string> + <string name="comp_applies_next_game">Gilt ab dem nächsten Spielstart</string> + <string name="comp_import_zip">.zip importieren</string> + <string name="comp_reading">Protons werden gelesen…</string> + <string name="comp_no_proton">Noch kein Proton installiert. Starte Steam einmal, um sein ARM64-Proton herunterzuladen, oder füge GE-Proton, CachyOS oder DroidDeck-Proton unter „Einrichtung“ hinzu.</string> + <string name="comp_original">Original · %1$s</string> + <string name="comp_this_build">%1$s · dieser Build</string> + <string name="comp_earlier_build">%1$s · früherer Build</string> + <string name="comp_installed_count">Installiert · %1$d</string> + <string name="comp_nothing_stored">Für %1$s ist noch nichts gespeichert. Die eigenen Dateien dieses Protons werden als sein Original gesichert.</string> + <string name="comp_waits_long">Auf diesem Proton läuft ein Spiel, daher wartet dies, bis es beendet ist.</string> + <string name="comp_applies_long">Gilt ab dem nächsten Spielstart.</string> + <string name="comp_swap_title">%1$s austauschen?</string> + <string name="comp_swap_body">%1$s in %2$s einsetzen? Die Originaldateien bleiben gesichert. %3$s</string> + <string name="comp_restore_title">%1$s wiederherstellen?</string> + <string name="comp_restore_body">Das Original von %1$s wieder in %2$s einsetzen? %3$s</string> + <string name="comp_delete_package_body">Entfernt das Paket aus der App. Du kannst es erneut herunterladen.</string> + <string name="comp_delete_original_title">Dieses Original löschen?</string> + <string name="comp_delete_original_body">Dieses %1$s-Original stammt aus einem früheren Build von %2$s. Das Original des aktuellen Builds wird immer behalten.</string> + <string name="comp_nightlies_count">In den Nightlies · %1$d</string> + <string name="comp_refresh_to_list">Aktualisiere, um die Pakete in den Nightlies aufzulisten.</string> + <string name="comp_nothing_new">Nichts Neues seit der letzten Prüfung.</string> + <string name="comp_import_wcp">.wcp importieren</string> + <string name="comp_restore_original">Original dieses Protons wiederherstellen</string> + <string name="comp_restore_original_body">Das originale %2$s von %1$s wiederherstellen?</string> + <string name="comp_swap">Austauschen</string> + <string name="comp_about_text">FEX, DXVK und VKD3D-Proton für jedes Proton, mit dem Steam Spiele ausführt.\n\nWähle eine installierte Zeile, um sie einzusetzen. Das gilt ab dem nächsten Spielstart oder sobald das laufende Spiel beendet ist.\n\nDeine Auswahl wird vor jedem Start erneut geprüft und wiederhergestellt, falls etwas sie geändert hat. Die Originaldateien jedes Protons werden aufbewahrt, sodass Steam-Updates sie nie verlieren.</string> + <string name="comp_tag_original">Original</string> + <string name="comp_tag_in_use">In Verwendung</string> + <string name="comp_tag_new">Neu</string> + <string name="comp_tag_stored">Gespeichert</string> + <string name="perf_title">Leistung</string> + <string name="perf_lede">CPU-Kerne, Sitzungskorrekturen und der Hostname. Gilt ab der nächsten Sitzung.</string> + <string name="perf_client_cores_group">Steam-Client-Kerne</string> + <string name="perf_override">Kernauswahl von Steam überschreiben</string> + <string name="perf_override_hint">Bindet Steam, seinen UI-Helfer und gamescope an die Kerne unten.</string> + <string name="perf_client_cores">Client-Kerne</string> + <string name="perf_client_cores_on">Kerne für Steam.</string> + <string name="perf_client_cores_off">Aktiviere die Überschreibung, um auszuwählen.</string> + <string name="perf_game_cores_group">Spielkerne</string> + <string name="perf_game_cores">Spielkerne</string> + <string name="perf_game_cores_hint">Kerne für Spiele. Sind alle ausgewählt, verteilt Android frei.</string> + <string name="perf_client_ui">Client-Oberfläche</string> + <string name="perf_glthread">Threaded GL</string> + <string name="perf_glthread_hint">Kann Steam-Menüs reaktionsschneller machen.</string> + <string name="perf_zink">Zink: verzögerte Deskriptoren</string> + <string name="perf_zink_hint">Empfohlen für Treiber ohne Deskriptorpuffer.</string> + <string name="perf_noglerror">GL-Fehlerprüfungen überspringen</string> + <string name="perf_noglerror_hint">Schaltet die GL-Validierung pro Aufruf ab.</string> + <string name="perf_gsrealtime">gamescope: Echtzeit-GPU-Warteschlange</string> + <string name="perf_gsrealtime_hint">Priorisiert die GPU-Arbeit des Compositors. Kann das Frame-Pacing glätten, kostet Spiele aber etwas Leistung.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">GPU auf höchstem Takt halten</string> + <string name="perf_gpuclock_hint">Nur Adreno. Weniger Ruckler, mehr Akkuverbrauch und Wärme.</string> + <string name="perf_fixes">Sitzungskorrekturen</string> + <string name="perf_sysmem">Turnip: Sysmem-Rendering</string> + <string name="perf_sysmem_hint">Schneller für Steam und die meisten Spiele. Erforderlich bei Adreno 710/720/722.</string> + <string name="perf_xalia">Steams xalia-Helfer überspringen</string> + <string name="perf_fastsync">droiddeck-ntsync (experimentell)</string> + <string name="perf_fastsync_hint">Ein Userspace-Ersatz für den Linux-Treiber ntsync, zum Testen. Solange er aktiv ist, nutzt Proton ihn statt droiddeck-esync und droiddeck-fsync. Funktioniert mit Proton Experimental ARM64, GE-Proton und proton-cachyos. Gilt ab der nächsten Sitzung.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync bevorzugen</string> + <string name="perf_fsync_hint">Führt Protons eigenes fsync mit den unveränderten Proton-Dateien aus, wobei futex_waitv im Userspace bereitgestellt wird. Es braucht daher kein Pack und funktioniert weiter, sobald Steam Proton aktualisiert. Solange es aktiv ist, nutzen Spiele es vor droiddeck-esync, außer den wenigen, die Proton von fsync fernhält. Gilt ab der nächsten Sitzung.</string> + <string name="perf_esync_hint">Die Windows-Threads von Spielen warten über eventfd und gemeinsamen Speicher aufeinander statt über Umwege durch wineserver. Nutzt ein droiddeck-esync-Pack, das für genau diesen Proton-Build erstellt wurde; Packs werden automatisch geladen. Bis es eines für einen neuen Proton-Build gibt, springt droiddeck-fsync ein. Gilt ab der nächsten Sitzung.</string> + <string name="perf_xalia_hint">Deaktiviert Protons Gamepad-Navigationshilfe, die in jedem Spiel CPU-Leistung kostet.</string> + <string name="perf_seccomp">proot ohne seccomp ausführen</string> + <string name="perf_seccomp_hint">Kann Fehler wegen fehlender Syscalls auf manchen Kerneln beheben. Kann die Leistung mindern.</string> + <string name="perf_fastpath">proot-Schnellpfad</string> + <string name="perf_fastpath_needs_seccomp">Benötigt den seccomp-Filter von proot.</string> + <string name="perf_fastpath_hint">Schnellere Dateisuche und weniger Ruckler. Deaktivieren, wenn ein Programm seine Dateien nicht findet.</string> + <string name="perf_identity">Sitzungsidentität</string> + <string name="perf_hostname">Hostname</string> + <string name="perf_hostname_hint">Der Name, den Steam für dieses Gerät sieht. Leer lassen stellt %1$s wieder her.</string> + <string name="perf_hostname_invalid">Nur Buchstaben, Ziffern und innere Bindestriche, bis zu 63 Zeichen.</string> + <string name="perf_phantom_warning">Android ist so eingestellt, dass es diese Sitzung beendet</string> + <string name="driver_installed">Installiert</string> + <string name="driver_available">Zum Download verfügbar</string> + <string name="driver_nothing_new">Nichts Neues seit der letzten Prüfung.</string> + <string name="driver_restore_builtin">Integrierte Treiber wiederherstellen</string> + <string name="driver_delete_builtin">In die App integriert: Dies entfernt die entpackten Dateien und blendet ihn aus. „Integrierte Treiber wiederherstellen“ holt ihn zurück.</string> + <string name="driver_delete_bundle">Hälfte eines Android- + Linux-Bundles: Beide Hälften werden entfernt. Ein verwendeter Treiber fällt auf den Standard zurück.</string> + <string name="driver_delete_plain">Seine Dateien werden aus der App entfernt.</string> + <string name="driver_delete_in_use">Seine Dateien werden entfernt. Er ist in Verwendung, daher übernimmt der Standard.</string> + <string name="drawer_page_display">Anzeige</string> + <string name="drawer_page_effects">Effekte</string> + <string name="drawer_page_controls">Steuerung</string> + <string name="drawer_page_components">Komponenten</string> + <string name="drawer_page_controller">Controller</string> + <string name="drawer_page_settings">Einstellungen</string> + <string name="drawer_page_desc">Seite %1$s</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Desktop</string> + <string name="drawer_steam_menu">Steam-Menü</string> + <string name="drawer_quick_access">Schnellzugriff</string> + <string name="drawer_open_qam">Schnellzugriffsmenü öffnen</string> + <string name="drawer_prev_page">Vorherige Seite</string> + <string name="drawer_next_page">Nächste Seite</string> + <string name="drawer_hud">Leistungs-HUD</string> + <string name="drawer_scaling">Skalierungsmodus</string> + <string name="drawer_scaling_sharpness">Skalierungsschärfe</string> + <string name="drawer_effects">Bildschirmeffekte</string> + <string name="drawer_effects_hint">Live. Kostet etwas GPU-Zeit, solange ein Effekt aktiv ist.</string> + <string name="drawer_look">Voreinstellungen</string> + <string name="drawer_look_custom">Benutzerdefiniert</string> + <string name="drawer_effects_advanced">Erweitert</string> + <string name="widgets_expanded">Ausgeklappt</string> + <string name="widgets_collapsed">Eingeklappt</string> + <string name="drawer_cas">Schärfen (CAS)</string> + <string name="drawer_cas_level">Schärfegrad</string> + <string name="drawer_fake_hdr">Fake-HDR</string> + <string name="drawer_deband">Debanding</string> + <string name="drawer_deband_strength">Debanding-Stärke</string> + <string name="drawer_brightness">Helligkeit</string> + <string name="drawer_contrast">Kontrast</string> + <string name="drawer_gamma">Gamma</string> + <string name="drawer_saturation">Sättigung</string> + <string name="drawer_fxaa">FXAA (Kantenglättung)</string> + <string name="drawer_toon">Toon-Konturen</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Texturfilterung</string> + <string name="drawer_texture_hint">DirectX-9–11-Spiele, ab ihrem nächsten Start.</string> + <string name="drawer_anisotropy">Anisotrope Filterung</string> + <string name="drawer_texture_sharpness">Texturschärfe</string> + <string name="drawer_texture_sharpness_note">Ein negativer Mip-Bias: schärfere Texturen, etwas Flimmern. Auto richtet sich danach, wie stark die Sitzung auf den Bildschirm vergrößert wird.</string> + <string name="drawer_touch_auto">Auto (%1$s)</string> + <string name="drawer_keyboard">Tastatur</string> + <string name="drawer_pc_keyboard">PC-Tastatur</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">Zweiter Bildschirm</string> + <string name="drawer_second_screen_mode">Modus</string> + <string name="drawer_second_screen_shows">Zeigt</string> + <string name="drawer_android_apps">Android-Apps</string> + <string name="drawer_launch_app">App starten</string> + <string name="common_hide">Ausblenden</string> + <string name="common_show">Anzeigen</string> + <string name="drawer_no_apps">Keine startbaren Apps</string> + <string name="drawer_session_behavior">Sitzungsverhalten</string> + <string name="drawer_suspend_hint">Gilt sofort und für künftige Sitzungen in diesem Modus.</string> + <string name="drawer_next_session">Nächste Sitzung</string> + <string name="drawer_support">Support</string> + <string name="drawer_logs">Sitzungslogs</string> + <string name="drawer_logs_hint">An einen Fehlerbericht anhängen</string> + <string name="drawer_share_logs">Logs teilen</string> + <string name="drawer_background">Hintergrund</string> + <string name="drawer_stop">Beenden</string> + <string name="drawer_stop_title">Sitzung beenden?</string> + <string name="drawer_running_now">Läuft gerade</string> + <string name="drawer_no_game">Es läuft kein Spiel auf einem Proton</string> + <string name="drawer_selected_profile">Spielprofil</string> + <string name="drawer_selected_profile_none">Profildetails sind noch nicht verfügbar.</string> + <string name="drawer_profile_refresh_failed_initial">Spielprofile konnten nicht geladen werden. Neuer Versuch erfolgt automatisch.</string> + <string name="drawer_profile_refresh_failed_stale">Spielprofile konnten nicht aktualisiert werden. Die letzten verfügbaren Daten werden angezeigt; neuer Versuch erfolgt automatisch.</string> + <string name="drawer_profile_inspect_hint">Gespeicherte Einstellungen für den nächsten Start ansehen und bearbeiten.</string> + <string name="drawer_profile_default">Standardprofil wird verwendet</string> + <string name="drawer_profile_custom">Eigenes Profil</string> + <string name="drawer_component_inherited">%1$s aus dem Standardprofil wird verwendet.</string> + <string name="drawer_game_components_note">Änderungen werden im Profil von %1$s gespeichert und gelten ab dem nächsten Spielstart.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d Spielüberschreibung</item> + <item quantity="other">%1$d Spielüberschreibungen</item> + </plurals> + <string name="drawer_no_proton">Es ist noch kein Proton installiert.</string> + <string name="drawer_game_running">Spiel läuft</string> + <string name="drawer_reapplied">beim Start erneut angewendet %1$s</string> + <string name="drawer_original">Original</string> + <string name="drawer_components_note">Importieren und Löschen auf der Seite „Komponenten“.</string> + <string name="setup_check_done">Fertig</string> + <string name="setup_check_warn">Handlungsbedarf</string> + <string name="setup_check_busy">In Arbeit</string> + <string name="setup_limit_adb_failed">Prüfe IP-Adresse und Port unter „Debugging über WLAN“ oder kopple erneut, falls Android dieses Gerät entfernt hat.</string> + <string name="setup_install">Installieren</string> + <string name="setup_manage">Verwalten</string> + <string name="setup_tab_overview">Übersicht</string> + <string name="setup_tab_controller">Controller</string> + <string name="setup_tab_session">Sitzung</string> + <string name="setup_tab_launcher">Launcher</string> + <string name="setup_title">Einrichtung</string> + <string name="setup_all_set">● Alles bereit</string> + <string name="setup_n_ready">%1$d von %2$d bereit</string> + <string name="setup_system_check">Systemprüfung</string> + <string name="setup_gpu_supported">Gerät unterstützt</string> + <string name="setup_gpu_untested">Ungetestete GPU</string> + <string name="setup_gpu_unsupported">GPU nicht unterstützt</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">Steam benötigt einen Adreno-Treiber; %1$s zeigt möglicherweise einen schwarzen Bildschirm</string> + <string name="setup_runtime">Linux-Laufzeit</string> + <string name="setup_runtime_progress">%1$s · %2$d %%</string> + <string name="setup_runtime_missing">Nicht installiert · ca. 3 GB, wird beim ersten „Spielen“ installiert</string> + <string name="setup_installed">Installiert</string> + <string name="setup_runtime_update">%1$s · Update verfügbar</string> + <string name="setup_runtime_current">%1$s · aktuell</string> + <string name="setup_limit">Limit für Kindprozesse</string> + <string name="setup_limit_on">An · Android kann Steam schließen. Das Abschalten dauert eine Minute.</string> + <string name="setup_limit_unset">Nicht gesetzt · Android kann Steam schließen. Das Abschalten dauert eine Minute.</string> + <string name="setup_limit_unknown">Konnte nicht geprüft werden · das Abschalten dauert eine Minute.</string> + <string name="setup_fix_it">Beheben</string> + <string name="setup_details">Details</string> + <string name="setup_dev_options">Entwickleroptionen</string> + <string name="setup_use_wireless">„Debugging über WLAN“ verwenden</string> + <string name="setup_check_again">Erneut prüfen</string> + <string name="setup_limit_turn_on">Limit aktivieren</string> + <string name="setup_account">Steam-Konto</string> + <string name="setup_signed_in_offline">Angemeldet als %1$s · Offlinemodus</string> + <string name="setup_signed_in">Angemeldet als %1$s</string> + <string name="setup_sign_in">Drücke „Spielen“ und melde dich bei Steam an</string> + <string name="setup_tools">Tools</string> + <string name="setup_controller">Controller</string> + <string name="setup_controller_unavailable">Controller-Einstellungen sind nicht verfügbar.</string> + <string name="setup_session">Sitzung</string> + <string name="setup_logs">Sitzungslogs</string> + <string name="setup_logs_hint">Nach jeder Sitzung gespeichert</string> + <string name="setup_agent_commands">Debugging-Tools</string> + <string name="setup_latest_logs">Neueste Sitzungslogs</string> + <string name="setup_saved_logs">Gespeicherte Sitzungslogs</string> + <string name="setup_saved_logs_hint">Behält die neuesten %1$d</string> + <string name="setup_clear_logs">Logs löschen</string> + <string name="setup_offline">Offlinemodus</string> + <string name="setup_sign_in_first">Melde dich zuerst bei Steam an</string> + <string name="setup_launcher">Launcher</string> + <string name="setup_language">Sprache</string> + <string name="setup_language_hint">Auch Steam nutzt sie, ab seinem nächsten Start.</string> + <string name="setup_language_system">Systemstandard (%1$s)</string> + <string name="setup_language_system_short">Systemstandard</string> + <string name="setup_app_scale">App-Skalierung</string> + <string name="setup_app_scale_hint">Text und Bedienelemente von DroidDeck</string> + <string name="setup_app_scale_default">%1$d %% (Standard)</string> + <string name="setup_theme">Design</string> + <string name="setup_theme_hint">Aussehen des Launchers</string> + <string name="setup_home">Als Startbildschirm verwenden</string> + <string name="setup_home_on">DroidDeck kann die Startbildschirm-App sein</string> + <string name="setup_home_off">DroidDeck wird nie als Startbildschirm-App angeboten</string> + <string name="setup_orientation">Ausrichtung</string> + <string name="setup_orientation_auto">Automatisch Querformat</string> + <string name="setup_orientation_landscape">Querformat</string> + <string name="setup_orientation_reverse">Umgekehrtes Querformat</string> + <string name="setup_fullscreen">Vollbild</string> + <string name="setup_fullscreen_on">Blendet die Status- und Navigationsleiste von Android aus</string> + <string name="setup_fullscreen_off">Zeigt die Status- und Navigationsleiste von Android an</string> + <string name="setup_default_home">Standard-Startbildschirm-App</string> + <string name="setup_choose_home">Startbildschirm-App auswählen</string> + <string name="setup_choose">Auswählen</string> + <string name="setup_linux_apps">Linux-Apps (Beta)</string> + <string name="setup_tool_files">Dateien</string> + <string name="setup_tool_files_hint">Dateien durchsuchen und verwalten</string> + <string name="setup_tool_protons">Proton-Versionen</string> + <string name="setup_tool_protons_hint">ARM64-Proton-Builds installieren</string> + <string name="setup_tool_performance">Leistung</string> + <string name="setup_tool_performance_hint">Zuweisung der CPU-Kerne</string> + <string name="setup_tool_roms">ROMs-Ordner</string> + <string name="setup_tool_roms_hint">Speicherort für Emulatorspiele</string> + <string name="setup_card_components">Komponenten</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Steuerung</string> + <string name="setup_card_controls_hint">Tastenbelegung</string> + <string name="store_tab_discover">Entdecken</string> + <string name="store_tab_search">Suchen</string> + <string name="store_tab_installed">Installiert</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Store</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak bereit</string> + <string name="store_needs_runtime">Installiere zuerst die Linux-Laufzeit unter „Einrichtung“.</string> + <string name="store_setup_title">Flatpak einrichten</string> + <string name="store_setup_text">Fügt Flatpak hinzu und verbindet es mit Flathub (ca. 20 MB). Die erste App lädt außerdem ihre Laufzeit herunter.</string> + <string name="store_setting_up">Wird eingerichtet…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Updates</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d %%</string> + <string name="store_starting">Wird gestartet…</string> + <string name="store_categories">Kategorien</string> + <string name="store_unreachable">Flathub nicht erreichbar. Prüfe deine Verbindung und versuch es noch einmal.</string> + <string name="store_try_again">Erneut versuchen</string> + <string name="store_search_label">Flathub durchsuchen</string> + <string name="store_search_placeholder">Firefox, Spiele, Emulatoren…</string> + <string name="store_search">Suchen</string> + <string name="store_all">Alle</string> + <string name="store_searching">Suche läuft…</string> + <string name="store_search_hint">Suche nach Namen oder wähle eine Kategorie. Nur ARM64-Apps werden aufgeführt.</string> + <string name="store_no_results">Keine ARM64-Apps auf Flathub passen dazu.</string> + <string name="store_results">Ergebnisse</string> + <string name="store_check_updates">Nach Updates suchen</string> + <string name="store_update_all">Alle aktualisieren (%1$d)</string> + <string name="store_all_current">● Alles ist aktuell</string> + <string name="store_no_apps">Noch keine Apps. Finde welche unter „Entdecken“ oder „Suchen“.</string> + <string name="store_setup_first_note">Richte Flatpak ein, um Apps zu installieren.</string> + <string name="store_update_available_prefix">Update verfügbar · %1$s</string> + <string name="store_open">Öffnen</string> + <string name="store_details">Details</string> + <string name="store_remove">Entfernen</string> + <string name="store_press_again">Zum Entfernen erneut drücken</string> + <string name="store_installing">Wird installiert…</string> + <string name="store_install">Installieren</string> + <string name="store_setup_first">Richte zuerst Flatpak ein</string> + <string name="store_download_size">%1$s Download</string> + <string name="store_installed_chip">● Installiert</string> + <string name="store_details_failed">Die Flathub-Seite dieser App konnte nicht geladen werden.</string> + <string name="store_loading">Wird von Flathub geladen…</string> + <string name="store_no_arm">Kein ARM64-Build auf Flathub, daher hier nicht installierbar.</string> + <string name="store_screenshots">Screenshots</string> + <string name="store_about">Über</string> + <string name="store_app_id">App-ID</string> + <string name="store_license">Lizenz</string> + <string name="store_runtime">Laufzeit</string> + <string name="store_installed_size">Installierte Größe</string> + <string name="store_installed_size_value">%1$s (plus gemeinsame Laufzeit)</string> + <string name="store_website">Website</string> + <string name="store_verified">Verifiziert</string> + <string name="upd_title">Updates</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Geprüft: %1$s</string> + <string name="upd_not_checked">Noch nicht geprüft</string> + <string name="upd_perm_title">Erlaube DroidDeck, sich selbst zu aktualisieren</string> + <string name="upd_perm_text">Aktiviere auf dem nächsten Bildschirm „Dieser Quelle vertrauen“ und kehre dann zurück. Das Update läuft von selbst weiter.</string> + <string name="upd_open_settings">Einstellungen öffnen</string> + <string name="upd_not_now">Nicht jetzt</string> + <string name="upd_signed_status">Anders signiert</string> + <string name="upd_signed_headline">Direktes Update nicht möglich</string> + <string name="upd_signed_detail">Diese Kopie ist nicht mit dem Release-Schlüssel von DroidDeck signiert, daher lassen sich Updates nicht darüber installieren. Deinstalliere sie, um zu wechseln.</string> + <string name="upd_available">Update verfügbar</string> + <string name="upd_blocked_status">Installation nicht möglich</string> + <string name="upd_switch">Bereit zum Wechseln</string> + <string name="upd_ahead_status">Neuer als Stabil</string> + <string name="upd_ahead_headline">Du bist dem Stabil-Kanal voraus</string> + <string name="upd_ahead_detail">Dieser Build ist neuer als die aktuelle Stabil-Version. Mit dem nächsten Release wechselst du zu Stabil.</string> + <string name="upd_gone_status">Test beendet</string> + <string name="upd_gone_headline">Dieser Test ist beendet</string> + <string name="upd_gone_detail">Sein Fix wurde übernommen oder verworfen. Folge „Vorschau“ für die neuesten Fixes.</string> + <string name="upd_current_status">Aktuell</string> + <string name="upd_current_headline">Du hast die neueste Version von %1$s</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d %%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Aktualisieren%1$s</string> + <string name="upd_install_button">Installieren%1$s</string> + <string name="upd_install_anyway">Stabil %1$s trotzdem installieren</string> + <string name="upd_follow_preview">Vorschau folgen</string> + <string name="upd_stop_session">Beende die laufende Sitzung, um zu aktualisieren.</string> + <string name="upd_less">Weniger</string> + <string name="upd_more">Mehr</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Neuer Vorschau-Build</string> + <string name="upd_new_test">Test-Build für PR #%1$d</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">UPDATE-KANAL</string> + <string name="upd_stable">Stabil</string> + <string name="upd_stable_hint">Getestete Releases, für die meisten</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Vorschau</string> + <string name="upd_preview_hint">Neueste Fixes, vor Stabil</string> + <string name="upd_preview_current">Aktuell</string> + <string name="upd_tests">Test-Builds</string> + <string name="upd_tests_hint">Einen Fix vor dem Release ausprobieren</string> + <string name="upd_tests_none">Derzeit keine</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Nicht für diese Kopie von DroidDeck gebaut</string> + <string name="upd_updated">Aktualisiert: %1$s</string> + <string name="upd_test_name">Test zu PR #%1$d</string> + <string name="upd_while_ago">vor einer Weile</string> + <string name="upd_just_now">gerade eben</string> + <plurals name="upd_tests_count"> + <item quantity="one">%1$d zum Ausprobieren</item> + <item quantity="other">%1$d zum Ausprobieren</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="one">Änderungen seit deinem Build · %1$d Build zurück</item> + <item quantity="other">Änderungen seit deinem Build · %1$d Builds zurück</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="one">Neueste Änderungen · %1$d Build</item> + <item quantity="other">Neueste Änderungen · %1$d Builds</item> + </plurals> + <string name="gate_title">Eine Android-Einstellung muss geändert werden</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Android stoppt Apps, die viele Hintergrundprozesse starten, und Steam startet Dutzende. Solange das so bleibt, werden Spiele ohne Fehlermeldung geschlossen.</string> + <string name="gate_fix_for_me">Für mich beheben</string> + <string name="gate_toggle_steps">Aktiviere „Einschränkungen für untergeordnete Prozesse deaktivieren“ und kehre dann zurück. Lass die Entwickleroptionen aktiviert, sonst geht der Schalter wieder aus.</string> + <string name="gate_open_dev">Entwickleroptionen öffnen</string> + <string name="gate_no_toggle">Diese Android-Version hat dafür keinen Schalter, deshalb ändert DroidDeck es über „Debugging über WLAN“. Dauert etwa eine Minute, kein Computer nötig.</string> + <string name="gate_needs_wifi">„Debugging über WLAN“ braucht eine Wi-Fi-Verbindung. Jedes Netzwerk geht; Internet ist nicht nötig.</string> + <string name="gate_needs_notifications">Der Kopplungscode wird in eine Benachrichtigung eingegeben. Erlaube Benachrichtigungen für DroidDeck oder nutze „Andere Wege“.</string> + <string name="gate_allow_notifications">Benachrichtigungen erlauben</string> + <string name="gate_step1">Öffne Einstellungen → Entwickleroptionen → Debugging über WLAN und aktiviere es</string> + <string name="gate_step2">Tippe auf „Gerät über einen Kopplungscode koppeln“</string> + <string name="gate_step3">Gib den Code in die DroidDeck-Benachrichtigung ein</string> + <string name="gate_waiting">Warte auf das Kopplungs-Pop-up…</string> + <string name="gate_code_needed">Pop-up gefunden. Gib seinen Code in der Benachrichtigung ein.</string> + <string name="gate_status">Status</string> + <string name="gate_polling">Prüft alle 2 Sekunden. Diese Seite schließt sich, sobald es aus ist.</string> + <string name="gate_other_ways">Andere Wege</string> + <string name="gate_other_toggle">Debugging über WLAN, manuelle Adresse, ADB am Computer</string> + <string name="gate_other_no_toggle">Manuelle Adresse, ADB am Computer, Entwickleroptionen</string> + <string name="gate_use_wireless_instead">Stattdessen „Debugging über WLAN“ verwenden</string> + <string name="gate_some_roms">Manche ROMs haben den Schalter trotzdem. Suche in den Entwickleroptionen nach „untergeordnete Prozesse“.</string> + <string name="gate_manual_hint">Falls die Benachrichtigung nicht funktioniert, gib Adresse und Code des Pop-ups selbst ein.</string> + <string name="gate_enter_manually">Adresse manuell eingeben</string> + <string name="gate_from_computer">Von einem Computer mit ADB:</string> + <string name="gate_copy_command">Befehl kopieren</string> + <string name="gate_choose_display">Wähle, auf welchem Display die Android-Einstellungen geöffnet werden.</string> + <string name="gate_main_screen">Hauptbildschirm</string> + <string name="wadb_finding_port">Gekoppelt. Suche den Port für Debugging über WLAN…</string> + <string name="wadb_enter_port">Gekoppelt. Gib den Port aus den Einstellungen ein.</string> + <string name="wadb_applying">Gerät gefunden. Einstellung wird angewendet…</string> + <string name="wadb_apply_failed">Automatisches Anwenden fehlgeschlagen. Prüfe die Adresse unten und versuch es erneut. %1$s</string> + <string name="wadb_title_wireless">Debugging über WLAN</string> + <string name="wadb_title_done">Limit für untergeordnete Prozesse aktualisiert</string> + <string name="wadb_title">Limit für untergeordnete Prozesse ändern</string> + <string name="wadb_lede_compact0">Fehlt die Option, kopple über „Debugging über WLAN“.</string> + <string name="wadb_lede_compact1">Gekoppelt. Prüfe IP-Adresse und Port unter „Debugging über WLAN“.</string> + <string name="wadb_lede_done">Das Limit für untergeordnete Prozesse wurde aktualisiert.</string> + <string name="wadb_lede0">Nutze Schritt 2, wenn die Entwickleroptionen keine Einstellung für untergeordnete Prozesse haben. Öffne Entwickleroptionen → Debugging über WLAN → Gerät über einen Kopplungscode koppeln.</string> + <string name="wadb_lede1">Gekoppelt. Nutze die Adresse unten oder aktualisiere sie über Debugging über WLAN → IP-Adresse und Port.</string> + <string name="wadb_lede_done_long">Das Limit für untergeordnete Prozesse wurde über „Debugging über WLAN“ aktualisiert.</string> + <string name="wadb_confirmed_on">Android hat bestätigt, dass das Limit aktiv ist.</string> + <string name="wadb_confirmed_off">Android hat bestätigt, dass das Limit aus ist.</string> + <string name="wadb_pair_group">2 · Für Debugging über WLAN koppeln</string> + <string name="wadb_pair_address">IP-Adresse und Port aus dem Kopplungs-Pop-up</string> + <string name="wadb_pin">Kopplungscode (PIN)</string> + <string name="wadb_pin_placeholder">6 Ziffern</string> + <string name="wadb_pair">Koppeln</string> + <string name="wadb_apply_group">2 · Per Debugging über WLAN anwenden</string> + <string name="wadb_connect_address">IP-Adresse und Port für die Verbindung</string> + <string name="wadb_pair_again">Erneut koppeln</string> + <string name="wadb_turn_off">Limit deaktivieren</string> + <string name="wadb_other_ways">Andere Wege, es zu ändern</string> + <string name="wadb_dev_try_first">1 · Entwickleroptionen (zuerst versuchen)</string> + <string name="wadb_restrict_on">Schalte „Untergeordnete Prozesse einschränken“ ein.</string> + <string name="wadb_restrict_off">Schalte „Untergeordnete Prozesse einschränken“ aus.</string> + <string name="wadb_computer_last">3 · ADB am Computer (letzter Ausweg)</string> + <string name="wadb_only_if">Nur wenn „Debugging über WLAN“ nicht verfügbar ist.</string> + <string name="wadb_computer_group">3 · ADB am Computer (letzter Ausweg)</string> + <string name="ctrl_osc_hint">Wann die Touch-Steuerung in einer Sitzung erscheint</string> + <string name="ctrl_color">Farbe</string> + <string name="ctrl_color_hint">Farbton der Bildschirmtasten</string> + <string name="ctrl_custom">Benutzerdefiniert</string> + <string name="ctrl_opacity">Deckkraft</string> + <string name="ctrl_percent">%1$d %%</string> + <string name="ctrl_size">Tastengröße</string> + <string name="ctrl_size_hint" formatted="false">Standardgröße: 100 %</string> + <string name="ctrl_stick_click">Stick-Klick</string> + <string name="ctrl_stick_click_hint">Doppeltippen und Stick halten für L3 oder R3</string> + <string name="ctrl_adaptive">Adaptive Sticks</string> + <string name="ctrl_rumble">Spielvibration</string> + <string name="ctrl_steam_button">Steam-Taste</string> + <string name="ctrl_qam_button">QAM-Taste</string> + <string name="ctrl_keyboard_button">Tastatur-Taste</string> + <string name="ctrl_select_steam">Mit Select das Steam-Menü öffnen</string> + <string name="ctrl_select_steam_hint">Statt View/Select.</string> + <string name="setup_animations">Animationen</string> + <string name="ctrl_adaptive_hint">Sticks erscheinen dort, wo du tippst, und verschwinden beim Loslassen</string> + <string name="ctrl_layout">Layout</string> + <string name="ctrl_layout_custom">Eigene Positionen gespeichert</string> + <string name="ctrl_layout_default">An diesen Bildschirm angepasst</string> + <string name="ctrl_edit">Bearbeiten</string> + <string name="ctrl_reset">Zurücksetzen</string> + <string name="ctrl_mapping">Tastenbelegung</string> + <string name="ctrl_mapping_default">Jede Taste sendet ihre eigene Eingabe</string> + <string name="ctrl_mapping_count">%1$d von %2$d Tasten neu belegt</string> + <string name="ctrl_configure">Konfigurieren</string> + <string name="ctrl_reset_all">Controller zurücksetzen</string> + <string name="ctrl_reset_all_hint">Stellt alle Controller-Einstellungen und das Layout wieder her</string> + <string name="ctrl_mapping_lede">Was jede Bildschirmtaste an das Spiel sendet. „Ausgeblendet“ entfernt die Taste.</string> + <string name="ctrl_onscreen_buttons">Bildschirmtasten</string> + <string name="ctrl_defaults">Standardwerte</string> + <string name="ctrl_reset_mapping">Belegung zurücksetzen</string> + <string name="content_android_apps">Android-Apps</string> + <string name="content_apps_count">%1$d Apps</string> + <string name="content_no_android_apps">Keine startbaren Android-Apps gefunden.</string> + <string name="content_desktop_installed">● Desktop installiert</string> + <string name="content_desktop_first_open">Wird beim ersten Öffnen installiert</string> + <string name="content_available">Zur Installation verfügbar</string> + <string name="content_not_installed">Nicht installiert.</string> + <string name="content_not_installed_suffix">%1$s · nicht installiert</string> + <string name="content_open_named">%1$s öffnen</string> + <string name="content_games">Spiele</string> + <string name="content_choose_roms">Wähle einen ROMs-Ordner.</string> + <string name="content_retroarch_roms">Navigiere in RetroArch zu /root/ROMs.</string> + <string name="content_add_roms">Füge %1$s-Spiele zu ROMs/%2$s hinzu.</string> + <string name="content_install_named">%1$s installieren</string> + <string name="content_stop_to_install">Zum Installieren Sitzung beenden</string> + <string name="content_runtime_required">Laufzeit erforderlich</string> + <string name="content_loading_details">Installationsdetails werden geladen…</string> + <string name="content_details_unavailable">Installationsdetails sind nicht verfügbar. Versuch es später noch einmal.</string> + <string name="content_needs_runtime">Installiere zuerst die Linux-Laufzeit unter „Einrichtung“.</string> + <string name="content_stop_first">Beende die laufende Sitzung, um Desktop-Apps zu installieren.</string> + <string name="content_install_into">Installiert %1$s in den Linux-Desktop.</string> + <string name="store_busy_percent_short">%1$s · %2$d %%</string> + <string name="content_hide_note">Ausblenden entfernt es vom Desktop. Seine Dateien bleiben in der Linux-Laufzeit.</string> + <string name="content_rom_gone">Dieses Spiel ist nicht mehr im ROMs-Ordner.</string> + <string name="content_desktop_system">Desktop · %1$s</string> + <string name="content_launch_in">In %1$s starten</string> + <string name="content_also_in">Auch in %1$s</string> + <string name="content_page_gone">Diese Seite gibt es nicht mehr. Wähle links einen Bereich.</string> + <string name="content_open_desktop">Desktop öffnen</string> + <string name="content_install_open_desktop">Desktop installieren & öffnen</string> + <string name="desktop_remove_title">Desktop deinstallieren</string> + <string name="desktop_remove_text">Entfernt den Desktop und seine mitgelieferten Apps. Spiele, hinzugefügte Apps, Einstellungen und gemeinsame Bibliotheken bleiben erhalten.</string> + <string name="desktop_removing">Desktop wird entfernt</string> + <string name="desktop_removed">Desktop entfernt</string> + <string name="desktop_cleaning_legacy">Alte LXQt-Dateien werden entfernt</string> + <string name="desktop_removal_unknown">Aktualisiere DroidDeck, bevor du diese Desktop-Version entfernst.</string> + <string name="content_linux_desktop">Linux-Desktop</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox und deine Emulatoren</string> + <string name="games_empty">Spiele, die du in Steam installierst, erscheinen hier nach der Sitzung.</string> + <string name="games_play_steam">Mit Steam spielen</string> + <string name="games_launch_settings">Starteinstellungen</string> + <string name="games_launch">Starten</string> + <string name="games_files">Spieldateien</string> + <string name="games_prefix">Proton-Präfix</string> + <string name="games_saves_looking">Suche nach Spielständen…</string> + <string name="games_saves_none">Kein Spielstandordner per Name gefunden. Der Export nimmt den ganzen Benutzerordner.</string> + <string name="games_saves_folders">%1$d Spielstandordner</string> + <string name="games_saves_summary">%1$s · %2$d Dateien · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">Spielstände verwalten</string> + <string name="games_saves">Spielstände</string> + <string name="games_import_saves">Spielstände importieren…</string> + <string name="games_export_gamehub">Als GameHub-ZIP exportieren…</string> + <string name="games_export_winlator">Als Winlator-ZIP exportieren…</string> + <string name="games_open_save_folder">Spielstandordner öffnen</string> + <string name="games_never_played">Nie gespielt</string> + <string name="games_runtime_busy_percent">Laufzeit beschäftigt · %1$d %%</string> + <string name="games_runtime_busy">Laufzeit beschäftigt</string> + <string name="games_runtime_first_play">Laufzeit wird beim ersten „Spielen“ installiert</string> + <string name="games_runtime_update">Laufzeit-Update verfügbar</string> + <string name="games_runtime_ready">● Laufzeit bereit</string> + <string name="games_last_played">Zuletzt gespielt: %1$s</string> + <string name="games_internal">Interner Speicher</string> + <string name="games_added">Hinzugefügtes Spiel</string> + <string name="games_custom">Eigenes Spiel</string> + <string name="games_store_storage">Spielspeicher</string> + <string name="gpu_auto_hint">Das beste Paar für diese GPU, stets aktuell</string> + <string name="gpu_manual_hint">Wähle selbst ein Paar</string> + <string name="gpu_checking">Suche nach Treibern…</string> + <string name="gpu_none_listed">Noch keine Treiber aufgeführt. Aktualisiere, um danach zu suchen.</string> + <string name="gpu_hide_other">Treiber für andere GPUs ausblenden</string> + <string name="gpu_show_other">Treiber für andere GPUs anzeigen</string> + <string name="gpu_advanced">Erweitert</string> + <string name="gpu_set_with_display">Zusammen mit dem Anzeigetreiber festgelegt: %1$s enthält beide.</string> + <string name="gpu_set_with_runtime">Zusammen mit dem Laufzeittreiber festgelegt: %1$s enthält beide.</string> + <string name="gpu_runtime_hint">Wird von Spielen und dem Desktop genutzt.</string> + <string name="gpu_runtime_default">Laufzeit-Standard</string> + <string name="gpu_display_hint">Bringt die Frames auf den Bildschirm. Gilt nach einem Neustart von DroidDeck.</string> + <string name="gpu_auto_by_gpu">Auto - nach GPU gewählt</string> + <string name="gpu_manage">Herunterladen, importieren oder löschen…</string> + <string name="gpu_in_use_caps">IN VERWENDUNG</string> + <string name="gpu_builtin">Die in DroidDeck integrierten Treiber</string> + <string name="gpu_matched_pair">Laufzeit- und Anzeigetreiber zusammen als passendes Paar festgelegt.</string> + <string name="gpu_recommended_for">Empfohlen für diese GPU: %1$s.</string> + <string name="gpu_refresh_hint">Aktualisiere, um nach Treibern für diese GPU zu suchen.</string> + <string name="gpu_recommended_caps">EMPFOHLEN</string> + <string name="gpu_unavailable">Nicht verfügbar</string> + <string name="load_runtime">Linux-Laufzeit</string> + <string name="load_runtime_detail">Linux-Laufzeit wird vorbereitet</string> + <string name="load_session">Sitzung</string> + <string name="load_session_detail">Anzeige und Audio werden gestartet</string> + <string name="load_steam">Steam-Client</string> + <string name="load_steam_detail">Steam und seine Kompatibilitätstools werden geprüft</string> + <string name="load_opening_steam">Steam wird geöffnet</string> + <string name="load_opening_steam_detail">Warte auf das erste Bild von Steam</string> + <string name="load_desktop">Desktop</string> + <string name="load_desktop_detail">Desktop wird vorbereitet</string> + <string name="load_opening">Wird geöffnet</string> + <string name="load_opening_detail">Warte auf das erste Bild</string> + <string name="load_starting_steam">Steam wird gestartet</string> + <string name="load_ended">Die Sitzung wurde beendet</string> + <string name="load_step">Schritt %1$d von %2$d</string> + <string name="load_steam_paused">Steam ist pausiert</string> + <string name="deck_tab_grips">Griffe</string> + <string name="deck_tab_left">Linkes Pad</string> + <string name="deck_tab_right">Rechtes Pad</string> + <string name="deck_tab_both">Beide</string> + <string name="deck_tab_gamepad">Gamepad</string> + <string name="deck_surface_stick">Stick</string> + <string name="deck_surface_trackpad">Trackpad</string> + <string name="deck_surface_switch">Zwischen Stick und Trackpad wechseln</string> + <string name="deck_more_controls">Weitere Deck-Steuerelemente</string> + <string name="deck_panels">BEREICHE</string> + <string name="deck_close">Steuerung auf dem Zweitbildschirm schließen</string> + <string name="deck_open_steam_menu">Steam-Menü öffnen</string> + <string name="deck_left_upper">Links · oben</string> + <string name="deck_left_lower">Links · unten</string> + <string name="deck_right_upper">Rechts · oben</string> + <string name="deck_right_lower">Rechts · unten</string> + <string name="deck_click">Klick</string> + <string name="deck_right_trackpad">Rechtes Trackpad</string> + <string name="deck_left_trackpad">Linkes Trackpad</string> + <string name="second_trackpad">Trackpad</string> + <string name="second_show_keyboard">Tastatur einblenden</string> + <string name="second_hide_keyboard">Tastatur ausblenden</string> + <string name="second_terminal">Linux-Terminal</string> + <string name="second_terminal_not_ready">Die Linux-Sitzung ist noch nicht bereit für ein Terminal.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Spiele</string> + <string name="rail_desktop">Desktop</string> + <string name="rail_store">Store</string> + <string name="rail_components">Komponenten</string> + <string name="rail_apps">Apps</string> + <string name="rail_setup">Einrichtung</string> + <string name="rail_updates">Updates</string> + <string name="rail_percent">%1$d %%</string> + <string name="rail_resume_named">%1$s fortsetzen</string> + <string name="roms_text">Erscheint in jeder Sitzung als /root/ROMs. Der interne Speicher liegt unter /root/Storage.</string> + <string name="roms_none">Kein Ordner ausgewählt</string> + <string name="roms_applies">Gilt ab der nächsten Sitzung. SD-Karten funktionieren.</string> + <string name="roms_choose">Ordner auswählen</string> + <string name="roms_change">Ordner ändern</string> + <string name="proton_lede">Installiere einen ARM64-Proton-Build und wähle ihn dann pro Spiel unter Steam → Eigenschaften → Kompatibilität aus.</string> + <string name="proton_stop_first">Beende die laufende Sitzung, um ein Kompatibilitätstool zu installieren oder zu entfernen.</string> + <string name="proton_available">Verfügbare Builds</string> + <string name="proton_installed">%1$s installiert</string> + <string name="proton_queued">Aus einer früheren Anfrage vorgemerkt</string> + <string name="proton_not_installed">Nicht installiert</string> + <string name="proton_update_available">Update verfügbar → %1$s</string> + <string name="proton_remove_older">Alte %1$s-Versionen entfernen</string> + <string name="proton_remove_older_body">Entfernt %1$s. Spiele, die diese Versionen nutzen, müssen auf ein anderes Proton umgestellt werden.</string> + <string name="proton_remove_body">Spiele, die diese Version nutzen, müssen auf ein anderes Proton umgestellt werden.</string> + <string name="proton_install_now">Jetzt installieren</string> + <string name="proton_cancel_queued">Vormerkung abbrechen</string> + <string name="pckb_android">Android-Tastatur</string> + <string name="pckb_hide">Ausblenden ✕</string> + <string name="main_launch_named">%1$s starten</string> + <string name="main_choose_screen">Wähle einen Bildschirm. Zweitbildschirm: %1$s.</string> + <string name="main_secondary_gone">Der Zweitbildschirm ist nicht mehr verfügbar.</string> + <string name="main_primary">Hauptbildschirm</string> + <string name="main_secondary">Zweitbildschirm</string> + <string name="widgets_prev_tab">Vorheriger Tab</string> + <string name="widgets_next_tab">Nächster Tab</string> + <string name="widgets_on">An</string> + <string name="widgets_all_n">Alle %1$d</string> + <string name="widgets_none">Keine</string> + <string name="widgets_n_of_m">%1$d von %2$d</string> + <string name="widgets_off">Aus</string> + <string name="screen_bottom">Unterer Bildschirm</string> + <string name="game_env_applies">Gilt ab dem nächsten Spielstart. Das Profil eines Spiels hat Vorrang vor dem gemeinsamen.</string> + <string name="game_env_other">Anderes Spiel per App-ID…</string> + <string name="game_env_done">Fertig</string> + <string name="game_env_default">Standard</string> + <string name="game_env_off">Aus</string> + <string name="game_env_change">Ändern…</string> + <string name="game_env_change_title">Variable ändern</string> + <string name="game_env_restore_default">Auf Standard zurücksetzen</string> + <string name="game_env_eyebrow">Proton-Spiele</string> + <string name="pip_title">Bild im Bild</string> + <string name="pip_auto">Automatisch zu Bild im Bild wechseln</string> + <string name="unfolded_controls">Steuerung auf dem aufgeklappten Bildschirm</string> + <string name="unfolded_controls_hint">Aufgeklappt liegt das Spiel über den Steam Deck-Trackpads und den hinteren Griffen. Zugeklappt läuft es im Vollbild.</string> + <string name="pip_suspended">Angehalten</string> + <string name="pip_not_ready">Bild im Bild ist verfügbar, sobald die Sitzung bereit ist.</string> + <string name="pip_permission">Erlaube Bild im Bild für DroidDeck in den Android-Einstellungen.</string> + <string name="pip_unavailable">Android konnte Bild im Bild nicht öffnen.</string> + <string name="game_shortcut">Verknüpfung</string> + <string name="game_shortcut_add">Zum Startbildschirm hinzufügen</string> + <string name="game_shortcut_choose">Spiel auswählen</string> + <string name="game_shortcut_scanning">Installierte Spiele werden gesucht…</string> + <string name="game_link_copy">Startlink kopieren</string> + <string name="game_link_copied">Startlink kopiert</string> + <string name="game_shortcut_unsupported">Dieser Launcher kann keine Verknüpfungen hinzufügen. Nutze stattdessen „Startlink kopieren“.</string> + <string name="game_link_invalid">Ungültiger Spiel-Startlink.</string> + <string name="game_link_missing">Dieses Spiel ist nicht mehr in deiner DroidDeck-Bibliothek.</string> + <string name="game_link_session_busy">Beende die aktuelle Sitzung, bevor du ein Steam-Spiel startest.</string> + <string name="game_link_failed">Das Spiel konnte nicht an Steam gesendet werden. Versuche erneut, es zu starten.</string> + <string name="setup_retry_removal">Entfernen wiederholen</string> + <string name="runtime_removing">Wird entfernt</string> + <string name="runtime_removal_incomplete">Entfernen unvollständig</string> + <string name="game_file_export">Spieldatei exportieren</string> + <string name="game_file_sync">Alle Spieldateien mit Ordner synchronisieren</string> + <string name="game_file_stop_sync">Synchronisierung der Spieldateien beenden</string> + <string name="game_file_folder">Ordner für Frontend-Spieldateien auswählen</string> + <string name="game_file_exported">Spieldateien gespeichert unter %1$s</string> + <string name="game_frontend_files">Frontend-Dateien</string> + + <string name="main_wait_runtime">Warte, bis der Vorgang an der Laufzeit abgeschlossen ist</string> + <string name="main_wait_proton_install">Warte, bis die Installation des Kompatibilitätstools abgeschlossen ist</string> + <string name="main_wait_desktop_install">Warte, bis die Installation der Desktop-App abgeschlossen ist</string> + <string name="main_wait_install_game">Warte, bis die Installation abgeschlossen ist, bevor du ein Spiel startest</string> + <string name="main_saves_close_session">Schließe zuerst die Steam-Sitzung, damit das Spiel den Import nicht überschreiben kann</string> + <string name="main_saves_importing">Import in %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="one">%1$d Datei aus %2$s in %3$s importiert</item> + <item quantity="other">%1$d Dateien aus %2$s in %3$s importiert</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">%1$d Datei aus %2$s in %3$s importiert. Alte Spielstände gesichert unter Download/DroidDeck/Saves/backups</item> + <item quantity="other">%1$d Dateien aus %2$s in %3$s importiert. Alte Spielstände gesichert unter Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Spielstand-ZIP für %1$s auswählen</string> + <string name="main_saves_exporting">Export von %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="one">%1$d Datei als %2$s exportiert: %3$s</item> + <item quantity="other">%1$d Dateien als %2$s exportiert: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Speicherort für %1$s auswählen (%2$s)</string> + <string name="main_action_failed">%1$s fehlgeschlagen: %2$s</string> + <string name="main_no_logs_yet">Noch keine Sitzungsprotokolle: Starte zuerst eine Sitzung.</string> + <string name="main_no_logs_to_clear">Keine Sitzungsprotokolle zum Löschen.</string> + <plurals name="main_logs_cleared"> + <item quantity="one">%1$d Ordner mit Sitzungsprotokollen gelöscht.</item> + <item quantity="other">%1$d Ordner mit Sitzungsprotokollen gelöscht.</item> + </plurals> + <string name="main_wadb_pair_failed">Kopplung für Debugging über WLAN fehlgeschlagen</string> + <string name="main_wadb_command_failed">Befehl für Debugging über WLAN fehlgeschlagen</string> + <string name="main_wadb_unavailable">Debugging über WLAN ist nicht verfügbar</string> + <string name="main_adb_copied">ADB-Befehl kopiert</string> + <string name="main_pick_roms">ROMs-Ordner auswählen</string> + <string name="main_non_adreno_title">Keine Adreno-GPU</string> + <string name="main_non_adreno_text">Turnip unterstützt Adreno-GPUs. Auf %1$s zeigt Steam eventuell einen schwarzen Bildschirm. Download: %2$.0f MB.</string> + <string name="main_install_anyway">Trotzdem installieren</string> + <string name="main_remove_runtime_title">Linux-Laufzeit entfernen</string> + <string name="main_remove_runtime_text">Dadurch werden die Laufzeit, der Steam-Client darin und alle dort installierten Spiele gelöscht.</string> + <string name="main_applied">Angewendet: %1$s</string> + <string name="android_app_open_failed_primary">%1$s konnte nicht auf dem Hauptbildschirm geöffnet werden</string> + <string name="android_app_open_failed_on">%1$s konnte nicht auf %2$s geöffnet werden</string> + <string name="android_app_open_failed_display">%1$s konnte nicht auf Bildschirm %2$d geöffnet werden</string> + <string name="comp_busy_importing">Importieren</string> + <string name="comp_busy_swapping">Austauschen</string> + <string name="comp_busy_restoring">Wiederherstellen</string> + <string name="comp_busy_cancelling">Abbrechen</string> + <string name="comp_busy_deleting">Löschen</string> + <string name="comp_cannot_read">Datei kann nicht gelesen werden</string> + <string name="comp_imported">%1$s importiert</string> + <string name="comp_swap_cancelled">Der wartende Austausch wurde abgebrochen.</string> + <string name="comp_pick_wcp">Komponentenpaket auswählen (-linux .wcp)</string> + <string name="comp_nightlies_unreachable">Die Nightlies waren nicht erreichbar</string> + <string name="comp_stored">%1$s gespeichert</string> + <string name="comp_download_failed">Download fehlgeschlagen: %1$s</string> + <string name="drivers_pick_linux">Laufzeittreiber auswählen (-Linux-ZIP oder Android + Linux-Bundle)</string> + <string name="drivers_pick_android">Anzeigetreiber auswählen (AdrenoTools-ZIP oder Android + Linux-Bundle)</string> + <string name="drivers_pick_any">Treiber-ZIP oder Android + Linux-Bundle auswählen</string> + <string name="main_pkg_problem">%1$s: %2$s</string> + <string name="main_removing_named">%1$s wird entfernt…</string> + <string name="main_forgetting_named">%1$s wird vergessen…</string> + <string name="main_pick_game_storage">Ordner für den Spielspeicher auswählen</string> + <string name="main_pick_decky_zip">Decky-Plugin-ZIP auswählen</string> + <string name="main_storage_not_usable">Nicht nutzbar: %1$s</string> + <string name="main_running_program">Programm</string> + <string name="main_removing_runtime">Linux-Laufzeit wird entfernt</string> + <string name="main_settings_main_screen">Die Einstellungen ließen sich nicht auf dem unteren Bildschirm öffnen und wurden auf dem Hauptbildschirm geöffnet.</string> + <string name="main_open_dev_options">Entwickleroptionen in den Einstellungen öffnen</string> + + <string name="upd_unknown_apps_failed">Die Android-Einstellung „Unbekannte Apps installieren“ konnte nicht geöffnet werden</string> + <string name="upd_couldnt_check">Prüfung nicht möglich: %1$s</string> + <string name="upd_no_download">Dieser Build hat keinen Download für diese DroidDeck-Installation</string> + <string name="upd_stop_first">Beende zuerst die laufende Sitzung und aktualisiere dann</string> + <string name="upd_stage_downloading">Herunterladen</string> + <string name="upd_stage_installing">Installieren</string> + <string name="upd_stage_confirm">Warte auf deine Bestätigung</string> + <string name="upd_failed">Das Update ist fehlgeschlagen</string> + <string name="upd_not_offered">Android hat die Installation des Updates nicht angeboten</string> + + <string name="decky_install_failed">Decky-Installation fehlgeschlagen: %1$s</string> + <string name="decky_plugin_starting">Plugin-Import wird gestartet…</string> + <string name="decky_plugin_failed">Plugin-Import fehlgeschlagen: %1$s</string> + <string name="decky_plugin_installed">Plugin installiert. Starte Steam neu, um es zu laden.</string> + + <string name="drivers_not_checked">Noch nicht geprüft - tippe auf Aktualisieren, um nach neuen Treibern zu suchen</string> + <string name="drivers_bundle_sets_display">Android + Linux: legt den Anzeigetreiber gleich mit fest</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: legt den Laufzeittreiber gleich mit fest</string> + <string name="drivers_runtime_builtin">das in die Laufzeit integrierte Turnip</string> + <string name="drivers_system_vulkan">System-Vulkan: kein mitgelieferter Build für diese GPU</string> + <string name="drivers_bundled">%1$s (mitgeliefert)</string> + <string name="drivers_manual">Manuell</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">Derzeit ist nur eine Hälfte veröffentlicht</string> + <string name="drivers_pair_size">%1$.0f MB für beide</string> + <string name="drivers_none_to_set">Keine Treiber zum Festlegen: %1$s</string> + <string name="drivers_checking_latest">Suche nach den neuesten Treibern…</string> + <string name="drivers_up_to_date_check_failed">Aktuell (Prüfung nicht möglich: %1$s)</string> + <string name="drivers_recommended_unpublished">Die empfohlenen Treiber sind derzeit nicht veröffentlicht</string> + <string name="drivers_downloading_pair">%1$s %2$s wird heruntergeladen…</string> + <string name="drivers_updated_to">Auf %1$s %2$s aktualisiert.</string> + <string name="drivers_updated_to_restart">Auf %1$s %2$s aktualisiert. Der Anzeigetreiber gilt nach einem Neustart von DroidDeck.</string> + <string name="drivers_switched_to">Zu %1$s %2$s gewechselt.</string> + <string name="drivers_switched_to_restart">Zu %1$s %2$s gewechselt. Der Anzeigetreiber gilt nach einem Neustart von DroidDeck.</string> + <string name="drivers_using">%1$s %2$s wird verwendet.</string> + <string name="drivers_using_restart">%1$s %2$s wird verwendet. Der Anzeigetreiber gilt nach einem Neustart von DroidDeck.</string> + <string name="drivers_not_a_driver">Keine Treiber-ZIP: weder ein AdrenoTools-Treiber noch ein -Linux-Turnip noch ein Android + Linux-Bundle</string> + <string name="drivers_import_failed">Import fehlgeschlagen: %1$s</string> + <string name="drivers_imported_unnamed">Importierter Treiber</string> + <string name="drivers_using_bundle">%1$s wird als Laufzeit- und Anzeigetreiber verwendet.</string> + <string name="drivers_using_bundle_restart">%1$s wird als Laufzeit- und Anzeigetreiber verwendet. Der Anzeigetreiber gilt nach einem Neustart von DroidDeck.</string> + <string name="drivers_deleted_bundle">%1$s gelöscht (beide Treiber)</string> + <string name="drivers_deleted">%1$s gelöscht</string> + <string name="drivers_release_status">Neueste: %1$s · geprüft %2$s</string> + <string name="drivers_release_status_failed">Neueste: %1$s · %2$s nicht erreichbar · geprüft %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">vor %1$d Min.</item> + <item quantity="other">vor %1$d Min.</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">vor %1$d Std.</item> + <item quantity="other">vor %1$d Std.</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">vor %1$d Tag</item> + <item quantity="other">vor %1$d Tagen</item> + </plurals> + <string name="drivers_checking_sources">Banners-Turnip, WinNative und DroidDeck werden geprüft…</string> + <string name="drivers_check_failed">Prüfung fehlgeschlagen</string> + <string name="drivers_installed_pick_either">%1$s installiert - wähle ihn in einer der beiden Listen, um beide Treiber festzulegen</string> + <string name="drivers_installed_pick">%1$s installiert - wähle ihn im Menü aus</string> + + <string name="session_wait_removal">Warte, bis die Laufzeit entfernt ist</string> + <string name="session_title_game">Spiel</string> + <string name="session_touch_auto_touchpad">Touchpad</string> + <string name="session_touch_auto_direct">direkt</string> + <string name="session_swap_failed">Austausch fehlgeschlagen: %1$s</string> + <string name="session_profile_save_failed">Profil konnte nicht gespeichert werden: %1$s</string> + <string name="session_desktop_paused">Der Desktop ist pausiert</string> + <string name="session_named_paused">%1$s ist pausiert</string> + <string name="session_program_paused">Das Programm ist pausiert</string> + <string name="session_starting_desktop">Desktop wird gestartet</string> + <string name="session_starting_named">%1$s wird gestartet</string> + <string name="session_starting_program">Programm wird gestartet</string> + <string name="session_no_logs">Keine Protokolle für diese Sitzung.</string> + <string name="session_logs_archive_failed">Das Archiv der Sitzungsprotokolle konnte nicht erstellt werden.</string> + <string name="session_logs_share_failed">Die Sitzungsprotokolle konnten nicht geteilt werden.</string> + <string name="session_starting_session">Sitzung wird gestartet…</string> + <string name="session_runtime_downloading">Linux-Laufzeit wird heruntergeladen</string> + <string name="session_runtime_downloading_mb">Linux-Laufzeit wird heruntergeladen · %1$d von %2$d MB</string> + <string name="session_runtime_checking">Linux-Laufzeit wird geprüft</string> + <string name="session_runtime_unpacking">Linux-Laufzeit wird entpackt</string> + <string name="session_desktop_downloading">Desktop wird heruntergeladen</string> + <string name="session_desktop_downloading_mb">Desktop wird heruntergeladen · %1$d von %2$d MB</string> + <string name="session_desktop_checking">Desktop wird geprüft</string> + <string name="session_desktop_unpacking">Desktop wird entpackt</string> + <string name="session_proton_downloading">Proton Experimental (ARM64) wird heruntergeladen</string> + <string name="session_proton_downloading_mb">Proton Experimental (ARM64) wird heruntergeladen · %1$d von %2$d MB</string> + <string name="session_proton_checking">Proton Experimental (ARM64) wird geprüft</string> + <string name="session_proton_unpacking">Proton Experimental (ARM64) wird entpackt</string> + <string name="session_runtime_catalog_unreachable">Der Katalog der Laufzeiten war nicht erreichbar. Prüfe die Verbindung und tippe erneut auf Spielen.</string> + <string name="session_runtime_install_failed">Die Linux-Laufzeit wurde nicht installiert. Prüfe sie unter Einrichtung und tippe erneut auf Spielen.</string> + <string name="session_desktop_install_failed">Der Desktop wurde nicht installiert (%1$s). Prüfe die Verbindung und tippe erneut auf Desktop.</string> + <string name="session_ended_named">%1$s wurde unerwartet beendet. Teile die Protokolle mit einem Fehlerbericht oder versuche es erneut.</string> + <string name="session_ended_program">Das Programm wurde unerwartet beendet. Teile die Protokolle mit einem Fehlerbericht oder versuche es erneut.</string> + <string name="session_ended_desktop">Der Desktop wurde unerwartet beendet. Teile die Protokolle mit einem Fehlerbericht oder versuche es erneut.</string> + <string name="session_ended_detail">Exit-Status %1$d · %2$s</string> + <string name="session_ended_no_log">kein Protokoll</string> + <plurals name="session_hint_enosys"> + <item quantity="one">Das Protokoll zeigt %1$d Fehler „Function not implemented“: Die seccomp-Beschleunigung von proot lässt auf diesem Gerät einen Helfer scheitern. Versuche %2$s → „%3$s“ (oder „%4$s“) und starte neu.</item> + <item quantity="other">Das Protokoll zeigt %1$d Fehler „Function not implemented“: Die seccomp-Beschleunigung von proot lässt auf diesem Gerät einen Helfer scheitern. Versuche %2$s → „%3$s“ (oder „%4$s“) und starte neu.</item> + </plurals> + + <string name="mode_decky_plugins">Plugins</string> + <string name="mode_decky_plugins_hint">Installiere eine Plugin-ZIP für die nächste Steam-Sitzung.</string> + <string name="mode_decky_plugin_downloading">Wird heruntergeladen…</string> + <string name="mode_decky_plugin_install_zip">Aus ZIP installieren</string> + <string name="store_cat_games">Spiele</string> + <string name="store_cat_audio_video">Audio & Video</string> + <string name="store_cat_graphics">Grafik</string> + <string name="store_cat_internet">Internet</string> + <string name="store_cat_office">Büro</string> + <string name="store_cat_developer">Entwicklung</string> + <string name="store_cat_education">Bildung</string> + <string name="store_cat_science">Wissenschaft</string> + <string name="store_cat_system">System</string> + <string name="store_cat_utilities">Dienstprogramme</string> + <string name="store_section_popular">Beliebt</string> + <string name="store_section_trending">Im Trend</string> + <string name="store_section_new">Neu</string> + <string name="store_section_updated">Aktualisiert</string> + <string name="store_section_loading">wird geladen…</string> + <string name="store_flatpak_setup">Flatpak-Einrichtung</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Updates konnten nicht auf Flathub geprüft werden</string> + <string name="content_rom_installed">installiert</string> + <string name="content_rom_folder">Ordner</string> + <string name="theme_graphite">Graphit</string> + <string name="theme_graphite_detail">Graphitflächen, mit dem Kugelblau des Logos für alles Ausgewählte, Fokussierte oder Gedrückte.</string> + <string name="theme_paper">Papier auf Schwarz</string> + <string name="theme_paper_detail">Weiß ist die Hauptfarbe, das Blau des Symbols markiert Linien, Ringe und Punkte.</string> + <string name="theme_icon_blue">Symbolblau</string> + <string name="theme_icon_blue_detail">Schwarzer Grund, weiße Schrift, das Blau für alles Ausgewählte, Fokussierte oder Gedrückte.</string> + <string name="theme_electric">Elektrisches Marineblau</string> + <string name="theme_electric_detail">Das Blau heller, auf Flächen, die eher zu Marineblau als zu Grau tendieren.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Einfg</string> + <string name="pckb_key_del">Entf</string> + <string name="pckb_key_home">Pos1</string> + <string name="pckb_key_end">Ende</string> + <string name="pckb_key_pgup">Bild↑</string> + <string name="pckb_key_pgdn">Bild↓</string> + <string name="pckb_key_backspace">⌫ Rücktaste</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Feststell</string> + <string name="pckb_key_enter">Eingabe ⏎</string> + <string name="pckb_key_shift">⇧ Umschalt</string> + <string name="pckb_key_ctrl">Strg</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Leertaste</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <string name="second_key_on">%1$s ✓</string> + <string name="second_key_ctrl_c">Strg+C</string> + <string name="second_key_ctrl_d">Strg+D</string> + <string name="game_sync_err_runtime">Installiere zuerst die Laufzeit</string> + <string name="game_sync_err_steam_library">Steam-Bibliothek ist nicht verfügbar</string> + <string name="game_sync_err_previous_sd">Vorherige SD-Bibliothek ist nicht verfügbar</string> + <string name="game_sync_err_game_storage">Spielspeicher ist nicht verfügbar</string> + <string name="game_sync_err_added_games">Ordner für hinzugefügte Spiele ist nicht verfügbar</string> + <string name="game_sync_err_manifests">Steam-Manifeste sind nicht verfügbar</string> + <string name="content_system_many">Viele Systeme</string> + <string name="fm_drive_internal">Intern</string> + <string name="fm_drive_linux">Linux-System</string> + <string name="fm_drive_sd">SD-Karte</string> + <string name="fm_drive_storage">Speicher</string> + <string name="fm_select">Auswählen</string> + <string name="fm_properties">Eigenschaften</string> + <string name="fm_favorite_add">Zu Favoriten hinzufügen</string> + <string name="fm_favorite_remove">Aus Favoriten entfernen</string> + <string name="fm_rename">Umbenennen</string> + <string name="fm_copy">Kopieren</string> + <string name="fm_cut">Ausschneiden</string> + <string name="fm_actions">Aktionen</string> + <string name="fm_favorites">Favoriten</string> + <string name="fm_pin_current">Aktuellen Ordner anheften</string> + <string name="fm_no_favorites">Noch keine Favoriten - hefte einen Ordner über sein ⋮-Menü an, um schnell hierher zurückzukehren.</string> + <string name="fm_unpin_favorite">Aus Favoriten entfernen</string> + <string name="fm_size_zero">0 Byte</string> + <string name="fm_unit_bytes">Byte</string> + <string name="fm_prop_location">Speicherort</string> + <string name="fm_prop_type">Typ</string> + <string name="fm_prop_size">Größe</string> + <string name="fm_prop_modified">Geändert</string> + <string name="fm_type_folder">Ordner</string> + <string name="fm_type_file">Datei</string> + <string name="fm_type_ext_file">%1$s-Datei</string> + <string name="fm_read_only">Schreibgeschützt</string> + <string name="fm_read_only_hint">Verhindert, dass Spiele (und Wine) diese Datei überschreiben oder löschen.</string> + <string name="fm_read_only_failed">„Schreibgeschützt“ konnte nicht geändert werden</string> + <string name="fm_hidden">Versteckt</string> + <string name="fm_hidden_unsupported">Dieser Speicher kann das Versteckt-Attribut nicht speichern.</string> + <string name="fm_hidden_hint">Markiert die Datei in Windows als versteckt (Versteckt-Attribut von Wine).</string> + <string name="fm_hidden_failed">„Versteckt“ konnte auf diesem Speicher nicht geändert werden</string> + <string name="fm_done">Fertig</string> + <string name="fm_op_deleting">Wird gelöscht...</string> + <string name="fm_delete_failed">Löschen fehlgeschlagen</string> + <string name="fm_op_moving">Wird verschoben...</string> + <string name="fm_op_copying">Wird kopiert...</string> + <string name="fm_op_moving_item">Verschieben - %1$s</string> + <string name="fm_op_copying_item">Kopieren - %1$s</string> + <string name="fm_op_moving_batch">Verschieben %1$d/%2$d - %3$s</string> + <string name="fm_op_copying_batch">Kopieren %1$d/%2$d - %3$s</string> + <string name="fm_paste_failed">%1$d erledigt, %2$d fehlgeschlagen</string> + <string name="fm_paste_skipped">%1$d erledigt, %2$d übersprungen</string> + <plurals name="fm_paste_moved"> + <item quantity="one">%1$d Element verschoben</item> + <item quantity="other">%1$d Elemente verschoben</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">%1$d Element kopiert</item> + <item quantity="other">%1$d Elemente kopiert</item> + </plurals> + <string name="fm_already_exists">„%1$s“ ist bereits vorhanden</string> + <string name="fm_op_renaming">Wird umbenannt...</string> + <string name="fm_rename_failed">Umbenennen fehlgeschlagen</string> + <string name="fm_op_creating_folder">Ordner wird erstellt...</string> + <string name="fm_create_folder_failed">Ordner konnte nicht erstellt werden</string> + <string name="fm_new_folder">Neuer Ordner</string> + <string name="fm_folder_name">Ordnername</string> + <string name="fm_create">Erstellen</string> + <string name="fm_new_name">Neuer Name</string> + <string name="fm_delete_title">Löschen?</string> + <string name="fm_delete_text">„%1$s“ endgültig löschen?</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">%1$d Element löschen?</item> + <item quantity="other">%1$d Elemente löschen?</item> + </plurals> + <string name="fm_cannot_undo">Dies kann nicht rückgängig gemacht werden.</string> + <plurals name="fm_and_more"> + <item quantity="one">…und %1$d weiteres</item> + <item quantity="other">…und %1$d weitere</item> + </plurals> + <string name="fm_op_deleting_batch">Löschen %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">%1$d konnte nicht gelöscht werden</item> + <item quantity="other">%1$d konnten nicht gelöscht werden</item> + </plurals> + <string name="fm_conflict_merge_hint">Zusammenführen fügt Dateien im vorhandenen Ordner hinzu und ersetzt sie.</string> + <string name="fm_conflict_overwrite_hint">Überschreiben ersetzt die vorhandene Datei.</string> + <string name="fm_conflict_apply_all">Auf alle Konflikte anwenden</string> + <string name="fm_conflict_merge">Zusammenführen</string> + <string name="fm_conflict_overwrite">Überschreiben</string> + <string name="fm_conflict_keep_both">Beide behalten</string> + <string name="fm_conflict_skip">Überspringen</string> + <string name="fm_select_this_folder">Diesen Ordner auswählen</string> + <string name="fm_back">Zurück</string> + <string name="fm_drive_not_readable">%1$s ist eingebunden, aber derzeit nicht lesbar</string> + <string name="fm_list_view">Listenansicht</string> + <string name="fm_grid_view">Rasteransicht</string> + <string name="fm_search">Suchen</string> + <string name="fm_sort">Sortieren</string> + <string name="fm_sort_name">Name</string> + <string name="fm_sort_date">Änderungsdatum</string> + <string name="fm_rows_comfortable">Großzügige Zeilen</string> + <string name="fm_rows_compact">Kompakte Zeilen</string> + <string name="fm_hide_hidden">Versteckte Dateien ausblenden</string> + <string name="fm_show_hidden">Versteckte Dateien anzeigen</string> + <string name="fm_hide_favorites">Favoriten ausblenden</string> + <string name="fm_show_favorites">Favoriten anzeigen</string> + <string name="fm_filter_folder">Diesen Ordner filtern</string> + <string name="fm_free_space">%1$s frei</string> + <string name="fm_selected_count">%1$d ausgewählt</string> + <string name="fm_select_none">Keine</string> + <string name="fm_select_all">Alle</string> + <string name="fm_paste">Einfügen</string> + <plurals name="fm_items"> + <item quantity="one">%1$d Element</item> + <item quantity="other">%1$d Elemente</item> + </plurals> + <string name="fm_paste_here">%1$s hier einfügen</string> + <string name="fm_paste_move_here">%1$s hier einfügen (verschieben)</string> + <string name="fm_rail_title">Dateien</string> + <string name="fm_rail_storage">SPEICHER</string> + <string name="fm_rail_quick">SCHNELLZUGRIFF</string> + <string name="fm_rail_favorites">FAVORITEN</string> + <string name="fm_rail_downloads">Downloads</string> + <string name="fm_rail_downloads_short">Download</string> + <string name="fm_rail_roms">ROMs</string> + <string name="fm_rail_session_logs">Sitzungsprotokolle</string> + <string name="fm_rail_pictures">Bilder</string> + <string name="fm_favorite_added">„%1$s“ zu Favoriten hinzugefügt</string> + <string name="fm_favorite_removed">„%1$s“ aus Favoriten entfernt</string> + <string name="fm_empty_directory">Leeres Verzeichnis</string> + <string name="fm_up_one_level">Eine Ebene nach oben</string> + <string name="fm_choose_folder">Ordner auswählen</string> + <string name="fm_choose_file">Datei auswählen</string> + <string name="ctrl_tint_steam_blue">Steam-Blau</string> + <string name="ctrl_tint_sky">Himmelblau</string> + <string name="ctrl_tint_white">Weiß</string> + <string name="ctrl_tint_violet">Violett</string> + <string name="ctrl_tint_green">Grün</string> + <string name="ctrl_tint_amber">Bernstein</string> + <string name="ctrl_tint_red">Rot</string> + <string name="ctrl_tint_pink">Pink</string> + <string name="ctrl_button_a">A-Taste</string> + <string name="ctrl_button_b">B-Taste</string> + <string name="ctrl_button_x">X-Taste</string> + <string name="ctrl_button_y">Y-Taste</string> + <string name="ctrl_button_lb">Linke Schultertaste</string> + <string name="ctrl_button_rb">Rechte Schultertaste</string> + <string name="ctrl_button_lt">Linker Trigger</string> + <string name="ctrl_button_rt">Rechter Trigger</string> + <string name="ctrl_button_view">Ansichtstaste</string> + <string name="ctrl_button_menu">Menütaste</string> + <string name="ctrl_target_view">Ansicht</string> + <string name="ctrl_target_menu">Menü</string> + <string name="ctrl_target_up">Steuerkreuz oben</string> + <string name="ctrl_target_down">Steuerkreuz unten</string> + <string name="ctrl_target_left">Steuerkreuz links</string> + <string name="ctrl_target_right">Steuerkreuz rechts</string> + <string name="ctrl_target_hidden">Ausgeblendet</string> + <string name="ctrl_editor_drag">Zum Verschieben ziehen</string> + <string name="ctrl_editor_snap">Einrasten</string> + <string name="ctrl_editor_save">Speichern</string> + <string name="second_mode_none">Nichts</string> + <string name="second_mode_keyboard_trackpad">Tastatur + Trackpad</string> + <string name="second_mode_terminal">Terminal</string> + <string name="second_mode_deck_controls">Deck-Griffe + Trackpads</string> + + <string name="rtinst_removing">Linux-Laufzeit wird entfernt</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">Linux-Laufzeit wird entfernt · %1$d Eintrag</item> + <item quantity="other">Linux-Laufzeit wird entfernt · %1$d Einträge</item> + </plurals> + <string name="rtinst_removed">Linux-Laufzeit entfernt</string> + <string name="rtinst_removal_failed">Die Linux-Laufzeit konnte nicht vollständig entfernt werden. Wiederhole das Entfernen unter Einrichtung.</string> + <string name="rtinst_removing_leftovers">Reste der Laufzeit werden entfernt</string> + <string name="rtinst_downloading">Herunterladen</string> + <string name="rtinst_verifying">Überprüfen</string> + <string name="rtinst_extracting">Entpacken</string> + <string name="rtinst_channel_description">Wird angezeigt, während die Linux-Laufzeit installiert oder entfernt wird</string> + <string name="rtinst_title_installing">Linux-Laufzeit wird installiert</string> + <string name="rtinst_title_removing">Linux-Laufzeit wird entfernt</string> + + <string name="deskpkg_runtime_missing">Die Linux-Laufzeit ist nicht installiert</string> + <string name="deskpkg_no_checksum">Der Katalog hat keine Prüfsumme für %1$s</string> + <string name="deskpkg_checksum_mismatch">Prüfsumme stimmt nicht überein - es wurde nichts geändert</string> + <string name="deskpkg_installing">%1$s wird installiert</string> + <string name="deskpkg_place_failed">Das AppImage konnte nicht abgelegt werden</string> + <string name="deskpkg_extract_failed">Entpacken fehlgeschlagen</string> + <string name="deskpkg_install_failed">Installation fehlgeschlagen</string> + + <string name="flatpakmgr_setup_failed">Flatpak-Einrichtung fehlgeschlagen (Exit-Code %1$d)</string> + <string name="flatpakmgr_setup_unfinished">Flatpak-Einrichtung wurde nicht abgeschlossen</string> + <string name="flatpakmgr_busy">Eine andere Store-Aufgabe läuft</string> + <string name="flatpakmgr_not_ready">Richte zuerst Flatpak ein</string> + <string name="flatpakmgr_starting">Wird gestartet</string> + <string name="flatpakmgr_exit_status">Flatpak wurde mit Status %1$d beendet</string> + <string name="flatpakmgr_installing">%1$s wird installiert</string> + <string name="flatpakmgr_updating">%1$s wird aktualisiert</string> + <string name="flatpakmgr_removing">%1$s wird entfernt</string> + <string name="flatpakmgr_installing_step">%1$s wird installiert (%2$d/%3$d)</string> + <string name="flatpakmgr_updating_step">%1$s wird aktualisiert (%2$d/%3$d)</string> + <string name="flatpakmgr_removing_step">%1$s wird entfernt (%2$d/%3$d)</string> + <string name="flatpakmgr_ref_translations">Übersetzungen</string> + <string name="flatpakmgr_ref_graphics_drivers">Grafiktreiber</string> + <string name="flatpakmgr_ref_media_codecs">Mediencodecs</string> + <string name="flatpakmgr_ref_runtime">%1$s-Laufzeit</string> + <string name="flatpakmgr_ref_runtime_branch">%1$s-Laufzeit %2$s</string> + + <string name="appimg_too_small">Die Datei ist zu klein für ein AppImage</string> + <string name="appimg_unreadable">Die Datei kann nicht gelesen werden: %1$s</string> + <string name="appimg_not_linux">Dies ist kein AppImage (es ist kein Linux-Programm)</string> + <string name="appimg_wrong_arch">Dieses AppImage ist weder für ARM64 (aarch64) noch für x86-PCs gebaut</string> + <string name="appimg_not_appimage">Dies ist ein Linux-Programm, aber kein AppImage</string> + <string name="appimg_wrong_type">Nur AppImages vom Typ 2 können importiert werden (dieses ist Typ %1$d)</string> + <string name="appimg_no_space_import">Nicht genug freier Speicherplatz: Der Import benötigt etwa %1$s</string> + <string name="appimg_no_space_update">Nicht genug freier Speicherplatz: Das Update benötigt etwa %1$s</string> + <string name="appimg_import_failed">Import fehlgeschlagen</string> + <string name="appimg_gone">Dieses AppImage ist nicht mehr vorhanden</string> + <string name="appimg_update_failed">Update fehlgeschlagen</string> + <string name="appimg_extracting">%1$s wird entpackt</string> + <string name="appimg_extract_failed">Das AppImage konnte nicht entpackt werden</string> + <string name="appimg_extract_failed_detail">Das AppImage konnte nicht entpackt werden: %1$s</string> + + <string name="deckymgr_stop_session_loader">Beende die aktive Sitzung, bevor du Decky Loader installierst</string> + <string name="deckymgr_download_failed">Download von Decky Loader fehlgeschlagen</string> + <string name="deckymgr_size_mismatch">Die Größe von Decky Loader stimmt nicht mit den Release-Metadaten überein</string> + <string name="deckymgr_checksum_download_failed">Die Release-Prüfsumme konnte nicht heruntergeladen werden</string> + <string name="deckymgr_checksum_missing">Release-Prüfsumme fehlt</string> + <string name="deckymgr_checksum_invalid">Release-Prüfsumme ist ungültig</string> + <string name="deckymgr_checksum_mismatch">Prüfsumme von Decky Loader stimmt nicht überein</string> + <string name="deckymgr_not_elf">Die Release-Datei ist keine kompatible 64-Bit-Linux-Programmdatei</string> + <string name="deckymgr_session_started_loader">Während des Downloads wurde eine Sitzung gestartet; beende sie, bevor du Decky Loader installierst</string> + <string name="deckymgr_loader_not_executable">PluginLoader konnte nicht als ausführbar markiert werden</string> + <string name="deckymgr_cef_failed">Steam-CEF-Remote-Debugging konnte nicht aktiviert werden</string> + <string name="deckymgr_stage_failed">PluginLoader konnte nicht bereitgestellt werden</string> + <string name="deckymgr_loader_preserve_failed">Der vorhandene PluginLoader konnte nicht gesichert werden</string> + <string name="deckymgr_loader_activate_failed">Der neue PluginLoader konnte nicht aktiviert werden</string> + <string name="deckymgr_stop_session_plugin">Beende die aktive Sitzung, bevor du ein Plugin installierst</string> + <string name="deckymgr_loader_required">Installiere Decky Loader, bevor du ein Plugin importierst</string> + <string name="deckymgr_zip_unreadable">Die ausgewählte ZIP-Datei konnte nicht gelesen werden</string> + <string name="deckymgr_plugins_dir_failed">Das Decky-Plugin-Verzeichnis konnte nicht erstellt werden</string> + <string name="deckymgr_extracting_zip">Plugin-ZIP wird entpackt</string> + <string name="deckymgr_zip_one_folder">Die Plugin-ZIP muss genau einen Plugin-Ordner auf oberster Ebene enthalten</string> + <string name="deckymgr_zip_missing_files">In der Plugin-ZIP fehlt plugin.json, package.json oder dist/index.js</string> + <string name="deckymgr_plugin_name_missing">Plugin-Name fehlt</string> + <string name="deckymgr_session_started_plugin">Während der Installation wurde eine Sitzung gestartet; beende sie, bevor du ein Plugin installierst</string> + <string name="deckymgr_plugin_installed">Plugin installiert · starte Steam neu, um es zu laden</string> + <string name="deckymgr_plugin_install_failed">Installation der Plugin-ZIP fehlgeschlagen</string> + <string name="deckymgr_zip_too_many">Die Plugin-ZIP enthält zu viele Einträge</string> + <string name="deckymgr_zip_invalid_path">Die Plugin-ZIP enthält einen ungültigen Pfad</string> + <string name="deckymgr_zip_duplicate_paths">Die Plugin-ZIP enthält doppelte Pfade</string> + <string name="deckymgr_zip_unsafe_path">Die Plugin-ZIP enthält einen unsicheren Pfad</string> + <string name="deckymgr_folder_failed">Plugin-Ordner konnte nicht erstellt werden</string> + <string name="deckymgr_zip_too_large">Die Plugin-ZIP überschreitet entpackt das Limit von 1 GiB</string> + <string name="deckymgr_bundled_not_executable">Mitgelieferte Plugin-Binärdatei konnte nicht als ausführbar markiert werden</string> + <string name="deckymgr_remote_entry_invalid">Ungültiger remote_binary-Eintrag</string> + <string name="deckymgr_remote_name_invalid">Ungültiger Name der Remote-Binärdatei</string> + <string name="deckymgr_remote_url_https">Die URL der Remote-Binärdatei muss HTTPS verwenden</string> + <string name="deckymgr_remote_hash_invalid">Remote-Binärdatei %1$s hat einen ungültigen SHA-256-Hash</string> + <string name="deckymgr_bin_dir_failed">bin-Verzeichnis des Plugins konnte nicht erstellt werden</string> + <string name="deckymgr_downloading_binary">Plugin-Binärdatei wird heruntergeladen · %1$s</string> + <string name="deckymgr_binary_download_failed">Plugin-Binärdatei %1$s konnte nicht heruntergeladen werden</string> + <string name="deckymgr_binary_checksum_mismatch">Prüfsumme der Plugin-Binärdatei stimmt nicht überein · %1$s</string> + <string name="deckymgr_binary_not_executable">Plugin-Binärdatei konnte nicht als ausführbar markiert werden · %1$s</string> + <string name="deckymgr_binary_replace_failed">Plugin-Binärdatei konnte nicht ersetzt werden · %1$s</string> + <string name="deckymgr_binary_install_failed">Plugin-Binärdatei konnte nicht installiert werden · %1$s</string> + <string name="deckymgr_folder_taken">Der Plugin-Ordner gehört zu einem anderen Plugin</string> + <string name="deckymgr_preserve_failed">Das vorhandene Plugin konnte nicht gesichert werden</string> + <string name="deckymgr_activate_failed">Das importierte Plugin konnte nicht aktiviert werden</string> + <string name="deckymgr_settings_failed">Decky-Plugin-Einstellungen konnten nicht aktualisiert werden</string> + <string name="deckymgr_restore_failed">Das vorhandene Plugin konnte nicht wiederhergestellt werden</string> + + <string name="adbfix_save_failed">Identität für Debugging über WLAN konnte nicht gespeichert werden</string> + <string name="adbfix_command_failed">ADB-Befehl fehlgeschlagen (%1$d)</string> + <string name="adbfix_not_confirmed">Android hat nicht bestätigt, dass das Limit für untergeordnete Prozesse geändert wurde: %1$s</string> + <string name="adbfix_no_pairing">Keine Kopplung für Debugging über WLAN gespeichert</string> + <string name="adbfix_enter_address">Aktuelle IP-Adresse & Port von Debugging über WLAN eingeben</string> + + <string name="adbpair_timed_out">Zeitüberschreitung beim Warten auf das Kopplungs-Pop-up. Versuche es erneut über DroidDeck.</string> + <string name="adbpair_discovery_failed_code">Suche nach dem Kopplungs-Pop-up nicht möglich (%1$d). Prüfe, ob WLAN aktiviert ist.</string> + <string name="adbpair_discovery_failed">Suche nach dem Kopplungs-Pop-up nicht möglich: %1$s</string> + <string name="adbpair_six_digits">Gib alle 6 Ziffern des Codes ein</string> + <string name="adbpair_pairing">Wird gekoppelt…</string> + <string name="adbpair_code_rejected">Dieser Code hat nicht funktioniert. Öffne das Kopplungs-Pop-up erneut und gib den neuen Code ein.</string> + <string name="adbpair_connecting">Gekoppelt. Verbindung wird hergestellt…</string> + <string name="adbpair_port_not_found">Gekoppelt, aber der Port für Debugging über WLAN wurde nicht gefunden. Lass Debugging über WLAN aktiviert und versuche es erneut.</string> + <string name="adbpair_applying">Einstellung wird angewendet…</string> + <string name="adbpair_command_failed">Befehl für Debugging über WLAN fehlgeschlagen</string> + <string name="adbpair_channel_name">Einrichtung von Debugging über WLAN</string> + <string name="adbpair_channel_description">Fragt nach dem Kopplungscode für Debugging über WLAN, während die Einstellungen geöffnet sind</string> + <string name="adbpair_title_pair">Debugging über WLAN koppeln</string> + <string name="adbpair_waiting_text">Aktiviere Debugging über WLAN und tippe dann auf „Gerät mit Kopplungscode koppeln“.</string> + <string name="adbpair_waiting_long">Aktiviere Debugging über WLAN und tippe dann auf „Gerät mit Kopplungscode koppeln“. Bleib in den Einstellungen: Der Code wird in dieser Benachrichtigung eingegeben.</string> + <string name="adbpair_code_label">6-stelliger Kopplungscode</string> + <string name="adbpair_code_hint">Tippe auf „Code eingeben“ und gib die 6 Ziffern aus den Einstellungen ein.</string> + <string name="adbpair_title_code">WLAN-Kopplungscode eingeben</string> + <string name="adbpair_title_retry">Code noch einmal eingeben</string> + <string name="adbpair_enter_code">Code eingeben</string> + <string name="adbpair_title_working">Steam wird eingerichtet</string> + <string name="adbpair_title_done">Steam ist bereit</string> + <string name="adbpair_done_text">Tippe, um zu DroidDeck zurückzukehren. Du kannst Debugging über WLAN jetzt deaktivieren.</string> + <string name="adbpair_title_failed">Einrichtung von Debugging über WLAN abgebrochen</string> + + <string name="selfinst_http">Der Download antwortete mit HTTP %1$d</string> + <string name="selfinst_size_mismatch">Der Download hatte nicht die Größe der veröffentlichten APK und wurde verworfen - versuche es erneut</string> + <string name="selfinst_checksum_mismatch">Der Download stimmte nicht mit seiner Prüfsumme überein und wurde verworfen - versuche es erneut</string> + <string name="selfinst_wrong_package">Das Update ist für das Android-Paket %1$s, nicht für %2$s</string> + <string name="selfinst_metadata_signer">Die Update-Metadaten nennen nicht den Release-Signaturschlüssel von DroidDeck</string> + <string name="selfinst_unreadable">Android konnte die heruntergeladene APK nicht lesen</string> + <string name="selfinst_apk_wrong_package">Die heruntergeladene APK ist für %1$s, nicht für %2$s</string> + <string name="selfinst_version_mismatch">Die heruntergeladene APK hat versionCode %1$d statt des veröffentlichten %2$d</string> + <string name="selfinst_downgrade">Android installiert versionCode %1$d nicht über den installierten %2$d</string> + <string name="selfinst_no_signing">Die heruntergeladene APK hat keine Signaturinformationen</string> + <string name="selfinst_wrong_signer">Die heruntergeladene APK ist nicht mit dem Release-Schlüssel von DroidDeck signiert</string> + <string name="appupd_http">Der Update-Katalog antwortete mit HTTP %1$d</string> + + <string name="loadstate_starting">Sitzung wird gestartet…</string> + <string name="loadstate_elapsed">%1$d:%2$02d vergangen · läuft noch</string> + <string name="loadstate_steam_update">Update des Steam-Clients wird heruntergeladen · %1$d %%</string> + <string name="loadstate_steam_client">Steam-Client wird heruntergeladen · %1$s</string> + + <string name="pextra_busy">Es läuft bereits die Installation eines Kompatibilitätstools</string> + <string name="pextra_stop_session">Beende die aktive Sitzung, bevor du Kompatibilitätstools installierst</string> + <string name="pextra_no_release">Kein ARM64-Release für %1$s gefunden</string> + <string name="pextra_no_space">Zum Installieren von %1$s werden mindestens 4 GB freier Speicherplatz plus die Downloadgröße benötigt</string> + <string name="pextra_downloading">%1$s %2$s wird heruntergeladen</string> + <string name="pextra_retrying">Download wird wiederholt · Versuch %1$d von %2$d</string> + <string name="pextra_interrupted">Download unterbrochen</string> + <string name="pextra_download_failed">Download fehlgeschlagen; die Teildatei bleibt erhalten, damit ein neuer Versuch fortsetzen kann</string> + <string name="pextra_too_big">Der Download war größer als das Release und wurde gelöscht</string> + <string name="pextra_incomplete">Der Download war unvollständig; die Teildatei bleibt für einen fortsetzbaren neuen Versuch erhalten</string> + <string name="pextra_verifying">Download wird überprüft</string> + <string name="pextra_checksum_unreadable">Die Release-Prüfsumme konnte nicht gelesen werden; versuche es erneut</string> + <string name="pextra_no_checksum">%1$s %2$s veröffentlicht keine Prüfsumme; es wurde nichts installiert</string> + <string name="pextra_checksum_mismatch">Prüfsumme stimmt nicht überein; der Download wurde gelöscht</string> + <string name="pextra_session_started">Während des Downloads wurde eine Sitzung gestartet; beende sie, bevor du Kompatibilitätstools installierst</string> + <string name="pextra_installing">%1$s wird installiert</string> + <string name="pextra_install_failed">Installation von %1$s fehlgeschlagen (Exit-Code %2$d)</string> + <string name="pextra_could_not_install">%1$s konnte nicht installiert werden</string> + <string name="esync_fetching">droiddeck-esync-Paket wird abgerufen</string> + + <string name="cmgr_fex_linux_helpers">mit Linux-Helfern</string> + <string name="cmgr_fex_dlls_only">nur DLLs</string> + <string name="cmgr_original_proton_updated">Original (Proton seit dem letzten Austausch aktualisiert)</string> + <string name="cmgr_changed_outside">Außerhalb von Komponenten geändert</string> + <string name="cmgr_not_present">nicht vorhanden</string> + <string name="cmgr_original_version">Original %1$s</string> + <string name="cmgr_original_from">Original aus %1$s</string> + <string name="cmgr_swap_queued">%1$s %2$s kommt in %3$s, sobald das darauf laufende Spiel beendet wird.</string> + <string name="cmgr_swap_done">%1$s %2$s ist jetzt in %3$s. Es gilt ab dem nächsten Spielstart.</string> + <string name="cmgr_original_missing">Dieses Original-Bundle ist nicht gespeichert</string> + <string name="cmgr_restore_queued">%1$s von %2$s wird auf das Original zurückgesetzt, sobald das darauf laufende Spiel beendet wird.</string> + <string name="cmgr_restore_done">%1$s von %2$s aus dem Original von %3$s wiederhergestellt.</string> + <string name="cmgr_package_in_use">%1$s wird in %2$s verwendet. Wechsle in diesem Proton zuerst zu etwas anderem.</string> + <string name="cmgr_package_not_stored">%1$s ist nicht gespeichert</string> + <string name="cmgr_deleted">%1$s gelöscht.</string> + <string name="cmgr_original_kept">Das Original des installierten Builds bleibt erhalten: Es ist der Weg zurück.</string> + <string name="cmgr_not_stored">Nicht gespeichert</string> + <string name="cmgr_deleted_original">Das %1$s-Original von %2$s wurde gelöscht.</string> + <string name="cmgr_nightlies_only">Downloads kommen nur aus den Nightlies-Releases</string> + <string name="cmgr_list_predates_checksums">Diese Paketliste stammt aus der Zeit vor Prüfsummen - aktualisiere sie und versuche es erneut</string> + <string name="cmgr_checksum_mismatch">Prüfsumme stimmt nicht überein</string> + + <string name="sprefs_upscaler_linear">Linear</string> + <string name="sprefs_upscaler_nearest">Nächster Nachbar</string> + <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (Qualität)</string> + <string name="sprefs_upscaler_sharpen">Nur schärfen</string> + <string name="sprefs_lod_bias_auto">Auto (an Skalierung anpassen)</string> + + <string name="look_off_desc">Nichts angewendet.</string> + <string name="look_game_clarity">Spielklarheit</string> + <string name="look_game_clarity_desc">Schärfung mit etwas Kontrast - für jeden Tag.</string> + <string name="look_vivid">Lebendig</string> + <string name="look_vivid_desc">Farben kräftig verstärkt. Debanding an, damit der Himmel keine Streifen zeigt.</string> + <string name="look_cinematic">Kino</string> + <string name="look_cinematic_desc">Tieferer Kontrast, kühlere Farben, geglättete Kanten.</string> + <string name="look_competitive">Wettkampf</string> + <string name="look_competitive_desc">Aufgehellte Schatten und starke Schärfung - Gegner zuerst entdecken.</string> + <string name="look_adaptive_sharpen">Adaptive Schärfe</string> + <string name="look_adaptive_sharpen_desc">Nur Schärfung, Farben unverändert.</string> + <string name="look_filmic">Filmisch</string> + <string name="look_filmic_desc">Filmähnliche Kurve, leicht gedämpft.</string> + <string name="look_arcade">Arcade</string> + <string name="look_arcade_desc">Laut und knackig.</string> + <string name="look_retro_crt">Retro-Röhre</string> + <string name="look_retro_crt_desc">Röhrenbildschirm und analoges Signal zusammen.</string> + <string name="look_upscale_sharp">Scharf hochskaliert</string> + <string name="look_upscale_sharp_desc">Für Auflösungen unterhalb der Panel-Auflösung.</string> + <string name="look_pixel_clean">Saubere Pixel</string> + <string name="look_pixel_clean_desc">Scharfe Pixel für 2D und alte Titel.</string> + <string name="look_anime_edge">Anime-Kanten</string> + <string name="look_anime_edge_desc">Cartoon-Konturen plus Schärfung.</string> + + <string name="gpuinfo_not_adreno">Keine Adreno-GPU</string> + <string name="gpuinfo_supported">Unterstützt</string> + <string name="gpuinfo_experimental">Experimentell: Die Treiber sind Test-Builds</string> + <string name="gpuinfo_below_tested">Unter der getesteten Hardware (Adreno 725 und neuer): läuft eventuell nicht</string> + <string name="gpuinfo_unsupported">Nicht unterstützt: DroidDeck braucht eine Adreno-GPU (Snapdragon)</string> + + <string name="turnip_rate_limited">Das Rate-Limit von GitHub wurde erreicht - versuche es später erneut</string> + <string name="turnip_github_http">GitHub antwortete mit HTTP %1$d</string> + <string name="turnip_download_http">Download antwortete mit HTTP %1$d</string> + <string name="turnip_list_predates_checksums">Diese Treiberliste stammt aus der Zeit vor Prüfsummen - aktualisiere sie und versuche es erneut</string> + <string name="turnip_checksum_mismatch">Prüfsumme stimmt nicht überein - der Download wurde verworfen</string> + <string name="turnip_not_adrenotools">Kein AdrenoTools-Treiber: %1$s</string> + <string name="turnip_reject_no_meta">keine meta.json</string> + <string name="turnip_reject_meta_unreadable">meta.json ist nicht lesbar (%1$s)</string> + <string name="turnip_reject_linux_driver">dies ist ein Laufzeittreiber für Linux (importiere ihn unter „Linux-Laufzeittreiber“)</string> + <string name="turnip_reject_wayland">dies ist ein Wayland-Spieltreiber, mit dem diese App nichts anfangen kann</string> + <string name="turnip_reject_no_library_name">meta.json nennt keinen libraryName, daher konnte nichts an AdrenoTools übergeben werden</string> + <string name="turnip_reject_library_missing">meta.json nennt %1$s, das nicht in der ZIP enthalten ist</string> + <string name="turnip_reject_not_aarch64">%1$s ist keine gemeinsam genutzte 64-Bit-AArch64-ELF-Bibliothek</string> + <string name="turnip_reject_glibc">%1$s linkt glibc - ein „-Linux“-Turnip, der unter „Linux-Laufzeittreiber“ gehört</string> + <string name="lvd_no_library">Keine libvulkan_freedreno*.so in dieser ZIP. Eine Android- (AdrenoTools) oder -Wayland-Turnip-ZIP ist kein Linux-Laufzeittreiber.</string> + <string name="lvd_not_aarch64">%1$s ist keine gemeinsam genutzte 64-Bit-AArch64-ELF-Bibliothek.</string> + <string name="lvd_not_glibc">%1$s ist kein glibc-Treiber - er linkt die libc von Android. Die Linux-Laufzeit braucht eine „-Linux“-ZIP; ein einfacher oder „-Wayland“-Turnip kann vom Steam-Client gar nicht geladen werden.</string> + <string name="bundle_no_manifest">Kein Treiber-Bundle: keine manifest.json, die einen Android- und einen Linux-Treiber nennt</string> + <string name="bundle_entry_outside">Kein Treiber-Bundle: %1$s zeigt nach außerhalb</string> + <string name="bundle_too_large">Kein Treiber-Bundle: entpackt mehr als %1$d MB</string> + + <string name="gstore_sd_card">SD-Karte</string> + <string name="gstore_folder">Ordner</string> + + <string name="logshare_chooser">Sitzungsprotokolle teilen</string> + + <string name="hdr_needs_android_10">erfordert Android 10 oder neuer (Anzeigeebenen mit Farbraum)</string> + <string name="hdr_none">dieses Display meldet keine HDR-Unterstützung</string> + <string name="hdr_not_hdr10">dieses Display unterstützt %1$s, aber nicht HDR10</string> + + <string name="gpu_this_gpu">diese GPU</string> + <string name="phantom_title_on">Limit für untergeordnete Prozesse ist aktiv</string> + <string name="phantom_title_unset">Limit für untergeordnete Prozesse ist nicht festgelegt</string> + <string name="phantom_title_unreadable">Limit für untergeordnete Prozesse konnte nicht geprüft werden</string> + <string name="phantom_title_off">Limit für untergeordnete Prozesse ist aus</string> + <string name="phantom_title_not_required">Limit für untergeordnete Prozesse nicht erforderlich</string> + <string name="phantom_instructions_fix">%1$s Falls das nicht möglich ist, verbinde dieses Gerät per ADB mit einem Computer und führe den Befehl unten aus. Kehre dann hierher zurück und prüfe erneut.</string> + <string name="phantom_instructions_off">Steam-Sitzungen können starten.</string> + <string name="phantom_instructions_not_applicable">Diese Android-Version nutzt dieses Limit nicht.</string> + <string name="phantom_fix_toggle">Aktiviere „Einschränkungen für untergeordnete Prozesse deaktivieren“ in den Entwickleroptionen und lass die Entwickleroptionen danach aktiviert.</string> + <string name="phantom_fix_no_toggle">Diese Android-Version hat dafür keinen Schalter. Nutze Debugging über WLAN, um es auf diesem Gerät zu ändern.</string> + <string name="upd_block_no_apk">Dieser Build hat keinen Download für diese DroidDeck-Installation.</string> + <string name="upd_block_version_code">Android installiert ihn nicht über diesen Build, weil sein versionCode %1$d unter dem installierten %2$d liegt.</string> + <string name="upd_fail_downgrade">Android installiert keine ältere Version über eine neuere. Bleib bei diesem Build, bis der Kanal aufholt.</string> + <string name="upd_fail_signature">Diese DroidDeck-Installation ist anders signiert (ein lokaler Build?), daher kann Android sie nicht direkt aktualisieren.</string> + <string name="upd_fail_storage">Für das Update ist nicht genug freier Speicherplatz vorhanden.</string> + <string name="upd_fail_aborted">Das Update wurde abgebrochen.</string> + <string name="upd_fail_with_message">Das Update wurde nicht installiert: %1$s</string> + <string name="upd_fail_generic">Das Update wurde nicht installiert.</string> + <string name="saves_layout_zip">%1$s-ZIP</string> + <string name="saves_zip">ZIP</string> + <string name="saves_not_launched">%1$s wurde noch nicht gestartet</string> + <string name="saves_none_found">Keine Spielstände für %1$s gefunden</string> + <string name="saves_not_save_zip">%1$s hat keinen drive_c-Ordner und ist daher keine GameHub- oder Winlator-Spielstand-ZIP</string> + <string name="gstore_cannot_create">in %1$s können keine Ordner erstellt werden</string> + <string name="gstore_cannot_write">in %1$s kann nicht geschrieben werden</string> + <string name="gstore_cannot_write_detail">in %1$s kann nicht geschrieben werden (%2$s)</string> + <string name="gfiles_folder_unavailable">Ordner ist nicht verfügbar</string> + <string name="gfiles_name_taken">Eine andere Datei verwendet bereits %1$s</string> + <string name="gfiles_art_unavailable">Grafikordner ist nicht verfügbar</string> + <string name="gfiles_unrelated_inventory">Der Ordner enthält ein fremdes Sync-Inventar</string> + <string name="gfiles_remove_failed">%1$s konnte nicht entfernt werden</string> + <string name="gfiles_outside_folder">Datei liegt außerhalb des Exportordners</string> + <string name="cmgr_fex_unnamed">FEX (Build ohne Versionsnamen)</string> + <string name="cmgr_no_profile">keine profile.json in %1$s</string> + <string name="cmgr_not_component_package">%1$s ist kein Linux-Komponentenpaket (Typ %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s frei</string> + <string name="mode_storage_auto_short">Automatisch</string> + <string name="appupd_catalog_rejected">Der Update-Katalog hat seine Prüfungen nicht bestanden (%1$s)</string> + <string name="main_adb_clip_label">DroidDeck-Einstellung für untergeordnete Prozesse</string> + <string name="setup_signed_in_offline_unnamed">Angemeldet · Offlinemodus</string> + <string name="setup_signed_in_unnamed">Angemeldet</string> + <string name="driver_tag_bundled">MITGELIEFERT</string> + <string name="driver_tag_downloaded">HERUNTERGELADEN</string> + <string name="driver_tag_imported">IMPORTIERT</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Sitzungsanzeige</string> + <string name="display_resolution">Auflösung</string> + <string name="display_game_fit">16:9-Spielanpassung</string> + <string name="display_game_fit_hint">Wie 16:9-Spiele auf dieses Display passen. Ab der nächsten Sitzung.</string> + <string name="display_game_fit_unavailable">Wähle „An Bildschirm anpassen“ oder eine eigene Größe schmaler als 16:9.</string> + <string name="display_preserve">Seitenverhältnis beibehalten</string> + <string name="display_stretch">Auf Display strecken</string> + <string name="display_image_scaling">Bildskalierung</string> + <string name="display_filter">Skalierungsfilter</string> + <string name="display_filter_hint">Gemeinsam für Steam und Desktop.</string> + <string name="display_filter_live">Wirkt sofort. Gemeinsam für Steam und Desktop.</string> + <string name="display_filter_note">FSR, GSR und NIS wirken beim Vergrößern des Sitzungsbilds. „Nur schärfen“ funktioniert auch ohne Vergrößerung.</string> + <string name="display_sharpness">Filterschärfe</string> + <string name="display_frame_rate">Bildrate</string> + <string name="display_look_filter">%1$s Stellt die Skalierung auf %2$s.</string> + <string name="display_match_screen">An Bildschirm anpassen (%1$d×%2$d)</string> + <string name="display_follow_screen">Bildschirm folgen (passt sich beim Auf- und Zuklappen an)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Gerätedetails anzeigen</string> + <string name="device_info_hide">Gerätedetails ausblenden</string> + <string name="device_info_loading">Gerät wird ausgelesen…</string> + <string name="device_info_copy">Kopieren</string> + <string name="device_info_copy_hint">Kopiere diese Details in einen Fehlerbericht.</string> + <string name="device_info_copied">Kopiert.</string> + <string name="force_ssbs_title">SSBS erzwingen</string> + <string name="force_ssbs_hint">Hält eine CPU-Geschwindigkeitseinstellung aktiv, die Wine immer wieder abschaltet. Kann Windows-Spiele auf dem Snapdragon 8 Elite beschleunigen; auf älteren Chips kaum Unterschied. Gilt ab dem nächsten Spielstart.</string> + + <string name="rail_stores">Stores</string> + <string name="source_steam">Steam</string> + <string name="source_added">Eigene</string> + <string name="setup_stores">Stores</string> + <string name="setup_stores_open_on">Store öffnen auf</string> + <string name="setup_stores_speed">Download-Tempo</string> + <string name="setup_stores_engine">Store-Engine</string> + <string name="setup_stores_engine_chip_ready">● Engine bereit</string> + <string name="setup_stores_engine_chip_missing">Nicht gebaut</string> + <string name="stores_tier_balanced">Ausgewogen</string> + <string name="stores_tier_fast">Schnell</string> + <string name="stores_tier_max">Max.</string> + <string name="stores_title">Stores</string> + <string name="stores_settings">Store-Einstellungen</string> + <string name="stores_settings_eyebrow">Einstellungen</string> + <string name="stores_chip_signed_out">(nicht angemeldet)</string> + <string name="stores_tab_store">Store</string> + <string name="stores_tab_installed">Installiert</string> + <string name="stores_tab_library">Bibliothek</string> + <string name="stores_tab_all">Alle</string> + <string name="stores_tab_downloads">Downloads</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Anmelden</string> + <string name="stores_signout_named">%1$s · abmelden</string> + <string name="stores_signout_title">Von %1$s abmelden?</string> + <string name="stores_signout_note">Installierte Spiele bleiben erhalten. Die Bibliothek wird bei der nächsten Anmeldung erneut abgerufen.</string> + <string name="stores_signout_stay">Angemeldet bleiben</string> + <string name="stores_signout_go">Abmelden</string> + <string name="stores_search_catalog">Katalog von %1$s durchsuchen…</string> + <string name="stores_search_installed">Installierte Spiele durchsuchen…</string> + <string name="stores_search_library">Deine Bibliothek durchsuchen…</string> + <string name="stores_search_open">Suchen</string> + <string name="stores_installed_empty">Nichts installiert</string> + <string name="stores_library_loading">Wird geladen</string> + <string name="stores_nothing_matches">Keine Spiele</string> + <string name="stores_shelves_loading">Store wird geladen…</string> + <string name="stores_amazon_no_catalog">Kein Katalog</string> + <string name="stores_shelf_new">Neuheiten</string> + <string name="stores_shelf_deals">Angebote</string> + <string name="stores_shelf_free">Kostenlose Spiele</string> + <string name="stores_shelf_trending">Im Trend</string> + <string name="stores_shelf_library">Deine Bibliothek</string> + <string name="stores_play">Spielen</string> + <string name="stores_install">Installieren</string> + <string name="stores_resume_install">Installation fortsetzen</string> + <string name="stores_dl_interrupted">Unterbrochen</string> + <string name="stores_install_size">Installieren · %1$s</string> + <string name="stores_get_free">Kostenlos erhalten</string> + <string name="stores_view_on">Auf %1$s ansehen</string> + <string name="stores_buy_price">Kaufen · %1$s</string> + <string name="stores_installed_chip">Installiert</string> + <string name="stores_in_library">In der Bibliothek</string> + <string name="stores_free">Kostenlos</string> + <string name="stores_resume">Fortsetzen</string> + <string name="stores_refresh">Aktualisieren</string> + <string name="stores_show_mature">Inhalte für Erwachsene anzeigen</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Offline</string> + <string name="epic_card_none">Aus</string> + <string name="epic_eos">Epic-Anmeldung (EOS)</string> + <string name="epic_offline">Offline starten</string> + <string name="epic_resolve">Epic-Anmeldung klären</string> + <string name="store_cloud_saves">Cloud-Spielstände</string> + <string name="cloud_upload">In die Cloud hochladen</string> + <string name="cloud_download">Aus der Cloud herunterladen</string> + <string name="cloud_none">Keine Cloud-Spielstände</string> + <string name="cloud_conflict">Cloud-Konflikt</string> + <string name="cloud_conflict_files">Cloud-Konflikt · %1$d Dateien</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Cloud behalten</string> + <string name="cloud_keep_local">Lokal behalten</string> + <string name="cloud_checking">Cloud wird geprüft…</string> + <string name="cloud_last_sync">Zuletzt synchronisiert %1$s</string> + <string name="cloud_never">Noch nicht synchronisiert</string> + <string name="cloud_done">%1$d Dateien</string> + <string name="stores_eyebrow_installed">installiert</string> + <string name="stores_eyebrow_library">in deiner Bibliothek</string> + <string name="stores_eyebrow_store">Store</string> + <string name="achievements_title">Erfolge</string> + <string name="achievements_count">%1$d von %2$d freigeschaltet</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Freigeschaltet · %1$d</string> + <string name="achievements_group_locked">Gesperrt · %1$d</string> + <string name="achievements_group_hidden">Verborgen · %1$d</string> + <string name="achievements_hidden">Verborgener Erfolg</string> + <string name="achievements_hidden_detail">Details werden angezeigt, sobald er freigeschaltet ist.</string> + <string name="achievements_all">Alle Erfolge freigeschaltet</string> + <string name="achievements_reading">Wird geladen…</string> + <string name="achievements_signed_out">Melde dich bei %1$s an, um deine Erfolge zu sehen.</string> + <string name="achievements_unreachable">%1$s konnte nicht erreicht werden. Schau später noch einmal vorbei, wenn du online bist.</string> + <string name="achievements_none">%1$s listet keine Erfolge für dieses Spiel.</string> + <string name="achievements_show_all">Alle %1$d anzeigen</string> + <string name="achievements_dialog_title">Erfolge · %1$s</string> + <string name="stores_in_steam">✓ In Steam</string> + <string name="stores_uninstall">Deinstallieren</string> + <string name="stores_uninstalling">Wird deinstalliert…</string> + <string name="stores_uninstall_title">%1$s deinstallieren?</string> + <string name="stores_uninstall_body_size">Gibt %1$s frei und entfernt das Spiel aus Steam.</string> + <string name="stores_uninstall_body">Entfernt seine Dateien und seine Steam-Verknüpfung.</string> + <string name="stores_launch_not_registered">Steam übernimmt das Spiel beim nächsten Start; starte es von dort oder starte Steam jetzt.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Von Steam pro Spiel gewählt</string> + <string name="stores_card_audio">Audio</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Komponenten</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Engine</string> + <string name="stores_card_engine_value">%1$s · nativ</string> + <string name="stores_card_engine_java">%1$s · integriert</string> + <string name="stores_how_it_runs">So läuft es</string> + <string name="stores_kv_fetched">Abgerufen von</string> + <string name="stores_kv_fetched_value">%1$s, mit deinem Konto</string> + <string name="stores_kv_installed_to">Installiert in</string> + <string name="stores_kv_installed_to_value">Spielspeicher (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Gestartet über</string> + <string name="stores_kv_launched_by_value">Steam und Proton, derselbe Weg wie bei Steam-Spielen</string> + <string name="stores_kv_launched_by_epic">Steam und Proton · Epic-Anmeldung bei jedem Start, EOS-Overlay aus</string> + <string name="stores_kv_launched_by_amazon">Steam und Proton · Variablen für Amazon Games Services gesetzt</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">bei der Installation zu Steam hinzugefügt (Deck-UI + Steam Input)</string> + <string name="stores_downloads_empty">Keine Downloads</string> + <string name="stores_dl_paused">Pausiert</string> + <string name="stores_dl_queued">In Warteschlange</string> + <string name="stores_dl_queued_at">In Warteschlange · #%1$d</string> + <string name="stores_dl_cancelled">Abgebrochen</string> + <string name="stores_dl_failed">Fehlgeschlagen</string> + <string name="stores_dl_failed_reason">Fehlgeschlagen · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d %%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d %%</string> + <string name="stores_dl_pause">Pausieren</string> + <string name="stores_dl_resume">Fortsetzen</string> + <string name="stores_dl_cancel">Abbrechen</string> + <string name="stores_dl_retry">Wiederholen</string> + <string name="stores_dl_clear">Entfernen</string> + <string name="stores_dl_clear_all">Abgeschlossene entfernen</string> + <string name="stores_stage_manifest">Manifest</string> + <string name="stores_stage_download">Download</string> + <string name="stores_stage_verify">Prüfen</string> + <string name="stores_stage_install">Installation</string> + <string name="stores_stage_done">Fertig</string> + <string name="stores_stage_checking">Wird geprüft</string> + <string name="stores_stage_downloading">Wird heruntergeladen</string> + <string name="stores_dl_cancel_confirm">Download löschen?</string> + <string name="stores_dl_failed_kept">Fehlgeschlagen · %1$s behalten</string> + <string name="stores_stage_verifying">Wird überprüft</string> + <string name="stores_stage_installing">Wird installiert</string> + <string name="stores_dl_active_percent">%1$s %2$d %%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d Dateien</string> + <string name="stores_download_manager">Download-Manager</string> + <string name="stores_parallel">Gleichzeitige Downloads</string> + <string name="stores_install_where">Installieren in</string> + <string name="stores_install_title">%1$s installieren:</string> + <string name="stores_target_internal">Intern</string> + <string name="stores_target_free">%1$s frei</string> + <string name="stores_notification_channel">Store-Downloads</string> + <string name="stores_notification_channel_hint">Zeigt den Fortschritt von Spiel-Downloads und lässt Downloads im Hintergrund weiterlaufen</string> + <string name="stores_notification_one">Wird heruntergeladen</string> + <string name="stores_notification_many">%1$d Downloads</string> + <string name="stores_notification_waiting">Wird vorbereitet…</string> + <string name="stores_notification_done_channel">Abgeschlossene Store-Downloads</string> + <string name="stores_notification_installed">%1$s installiert</string> + <string name="stores_notification_failed">Download von %1$s fehlgeschlagen</string> + <string name="stores_notification_delete">Löschen</string> + <string name="stores_notification_keep">Behalten</string> + <string name="stores_login_title">Bei %1$s anmelden</string> + <string name="stores_login_provider_title">Mit deinem Anbieter anmelden</string> + <string name="stores_login_reload">Neu laden</string> + <string name="stores_login_close">Schließen</string> + <string name="stores_login_retry">Erneut versuchen</string> + <string name="stores_login_finishing">Anmeldung wird abgeschlossen…</string> + <string name="stores_login_error_network">Die Anmeldeseite konnte nicht geladen werden: %1$s</string> + <string name="stores_login_error_idp_blocked">Dieser Anbieter lässt keine Anmeldung über eine eingebettete Seite zu. Melde dich stattdessen mit deinem Store-Passwort an.</string> + <string name="stores_login_error_http">Die Anmeldeseite antwortete mit einem Fehler (HTTP %1$d).</string> + <string name="stores_login_error_ssl">Die Verbindung zur Anmeldeseite ist nicht sicher.</string> + <string name="stores_login_error_verification">Die Anmeldung konnte nicht bestätigt werden. Versuche es erneut.</string> + <string name="stores_login_error_generic">Anmeldung fehlgeschlagen. Versuche es erneut.</string> + <string name="common_close">Schließen</string> + <string name="common_back">Zurück</string> + <string name="common_edit">Bearbeiten</string> + <string name="games_add_all">Alle Spiele in diesem Ordner hinzufügen</string> + <string name="games_added_none">Nichts Neues in diesem Ordner</string> + <string name="edit_name">Name</string> + <string name="edit_target">Ziel</string> + <string name="edit_start_in">Ausführen in</string> + <string name="edit_launch_options">Startoptionen</string> + <string name="edit_artwork">Grafiken</string> + <string name="edit_other_exe">Andere EXE…</string> + <string name="edit_remove">Entfernen</string> + <string name="art_cover">Cover</string> + <string name="art_background">Hintergrund</string> + <string name="art_logo">Logo</string> + <string name="art_icon">Symbol</string> + <string name="art_src_folder">Spielordner</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">Deine Datei</string> + <string name="art_src_exe">Aus der EXE</string> + <string name="art_src_auto">Automatisch</string> + <string name="art_pick_image">Bild auswählen…</string> + <string name="art_pick_title">Bild auswählen: %1$s</string> + <string name="art_reset">Auf automatisch zurücksetzen</string> + <plurals name="games_added_n"> + <item quantity="one">%1$d Spiel hinzugefügt</item> + <item quantity="other">%1$d Spiele hinzugefügt</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="one">%1$d bereits unter „Spiele“</item> + <item quantity="other">%1$d bereits unter „Spiele“</item> + </plurals> + <string name="hop_back">B · Zurück: %1$s</string> + <string name="hop_from_games">Spiel</string> + <string name="hop_from_play">Spieleinstellungen</string> + <string name="sheet_graphics_drivers">Grafiktreiber</string> + <string name="comp_this_proton">Dieses Proton</string> + <string name="comp_every_proton">Jedes Proton</string> + <string name="comp_for_every_proton">Für jedes Proton</string> + <string name="comp_available_count">Verfügbar · %1$d</string> + <string name="comp_checked">Geprüft %1$s</string> + <string name="comp_check_all">Nach neuen Komponenten und Treibern suchen</string> + <string name="comp_get">Holen</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton und FEX unten gehören zu diesem Proton</string> + <string name="comp_device_tested">%1$s · getestet</string> + <string name="comp_device_untested">%1$s · ungetestet</string> + <string name="comp_device_unsupported">%1$s · nicht unterstützt</string> + <string name="gpu_auto_keeps">Hält das empfohlene Paar aktuell</string> + <string name="gpu_bar_auto">Auto · %1$s</string> + <string name="gpu_separately">Laufzeit- und Anzeigetreiber getrennt</string> + <string name="gpu_use_title">%1$s verwenden?</string> + <string name="proton_remove_title">%1$s entfernen?</string> + <string name="setup_tab_diagnostics">Diagnose</string> + <string name="setup_ready">Bereit zum Spielen</string> + <string name="setup_not_ready">Noch nicht bereit zum Spielen</string> + <string name="setup_ready_runtime">Laufzeit %1$s</string> + <string name="setup_ready_limit_off">Prozesslimit aus</string> + <string name="setup_ready_signed_in">angemeldet als %1$s</string> + <string name="setup_needs_you">Wenn etwas deine Aufmerksamkeit braucht</string> + <string name="setup_group_look">Aussehen</string> + <string name="setup_group_device">Dieses Gerät</string> + <string name="setup_group_art">Bibliotheksgrafiken</string> + <string name="flathub_eyebrow">LINUX-APPS</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Desktop-Apps für den Linux-Desktop, geöffnet über Desktop.</string> + <string name="flathub_setup">Flathub einrichten</string> + <string name="flathub_remove_title">%1$s entfernen?</string> + <string name="flathub_remove_note">Einstellungen und Dateien bleiben erhalten.</string> + <string name="flathub_keep">Behalten</string> + <string name="desktop_size">Speicherbedarf</string> + <string name="desktop_uninstall_title">Desktop deinstallieren?</string> + <string name="desktop_all_systems">Alle</string> + <string name="gog_achievements">GOG-Erfolge</string> + <string name="gog_achievements_hint">Synchronisiert Erfolge für unterstützte GOG-Spiele. Gilt ab dem nächsten Start.</string> + <string name="gog_achievements_off">Erfolge aus</string> + <string name="gog_achievements_on">Erfolge aktiviert</string> + <string name="gog_achievements_signin">Melde dich erneut bei GOG an</string> + <string name="gog_achievements_uploaded">Erfolge hochgeladen</string> + <string name="gog_achievements_pending">Hochladen des Erfolgs ausstehend</string> + <string name="gog_achievements_enable_store">Aktiviere GOG-Erfolge in den Store-Einstellungen</string> + <string name="gog_achievements_unavailable">Erfolge nicht verfügbar; Sitzungsprotokolle prüfen</string> + <string name="achievement_unlocked">Erfolg freigeschaltet</string> + <string name="touch_paste">Einfügen</string> + <string name="touch_dpad">Richtungstasten</string> + <string name="touch_left_stick">Linker Stick</string> + <string name="touch_right_stick">Rechter Stick</string> + <string name="touch_menu">Menü</string> + <string name="touch_keyboard">Tastatur</string> + <string name="touch_dpad_up">Richtungstasten hoch</string> + <string name="touch_dpad_down">Richtungstasten runter</string> + <string name="touch_dpad_left">Richtungstasten links</string> + <string name="touch_dpad_right">Richtungstasten rechts</string> + <string name="touch_title">Touch-Steuerung</string> + <string name="touch_controller">Touch-Controller</string> + <string name="touch_mouse">Maus</string> + <string name="touch_on">An</string> + <string name="touch_off">Aus</string> + <string name="touch_speed_slow">Langsam</string> + <string name="touch_speed_normal">Normal</string> + <string name="touch_speed_fast">Schnell</string> + <string name="touch_edit_layout">Layout bearbeiten</string> + <string name="touch_droiddeck_menu">DroidDeck-Menü</string> + <string name="touch_drag_hint">Ziehe Steuerelemente auf den Bildschirm</string> + <string name="touch_arrange">Anordnen</string> + <string name="touch_discard">Verwerfen</string> + <string name="touch_saving">Wird gespeichert…</string> + <string name="touch_icon">Symbol</string> + <string name="touch_narrower">B−</string> + <string name="touch_wider">B+</string> + <string name="touch_shorter">H−</string> + <string name="touch_taller">H+</string> + <string name="touch_hide">Ausblenden</string> + <string name="touch_color_opacity">Farbe und Deckkraft…</string> + <string name="touch_copy_layout">Dieses Layout kopieren</string> + <string name="touch_layout_copied">Layout kopiert; füge es in das Touch-Layout eines beliebigen Spiels ein</string> + <string name="touch_paste_layout">Kopiertes Layout einfügen</string> + <string name="touch_reset_layout">Auf Steams Standard zurücksetzen</string> + <string name="touch_no_binding">Dieses Steuerelement hat keine Zuordnung in Steam, um ihm ein Symbol zu geben</string> + <string name="touch_icon_for">Symbol für %1$s</string> + <string name="touch_layout_color">Touch-Layout-Farbe</string> + <string name="touch_done">Fertig</string> + <string name="touch_save_failed">Das Touch-Layout konnte nicht gespeichert werden</string> + <string name="touch_saved">Touch-Layout in Steam gespeichert</string> + <string name="touch_app_id">App %1$d</string> + <string name="touch_label_hint">Bezeichnung (optional)</string> + <string name="touch_icons_missing">Steams Symbole wurden nicht gefunden</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d Symbol von Steam</item> + <item quantity="other">%1$d Symbole von Steam</item> + </plurals> + <string name="touch_icon_color">Symbolfarbe</string> + <string name="touch_button_color">Schaltflächenfarbe</string> + <string name="touch_no_icon">Kein Symbol</string> + + <string name="stores_sync_sign_in">Anmeldung wird geprüft…</string> + <string name="stores_sync_library">Bibliothek wird abgerufen…</string> + <string name="stores_sync_game_list">Spieleliste wird abgerufen…</string> + <string name="stores_sync_details">Spieledetails werden geladen… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">%1$d Spiel wird synchronisiert…</item> + <item quantity="other">%1$d Spiele werden synchronisiert…</item> + </plurals> + <string name="stores_sync_progress">Wird synchronisiert… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Cover werden abgerufen…</string> + <string name="stores_sync_failed">Synchronisierung fehlgeschlagen</string> + <string name="stores_sync_no_games">Keine Spiele in deiner %1$s-Bibliothek gefunden</string> + <string name="stores_sync_amazon_no_answer">Amazon Games hat für deine Bibliothek nicht geantwortet: aktualisiere die Seite oder melde dich erneut an</string> + <string name="stores_sync_gog_unreachable">GOG konnte nicht erreicht werden</string> + <string name="stores_sync_gog_unreadable">Deine GOG-Bibliothek konnte nicht gelesen werden</string> + <string name="stores_registering">Wird bei Steam registriert…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Cardboard-Modus</string> + <string name="cardboard_mode_hint">Bilder für linkes und rechtes Auge für eine Smartphone-VR-Brille.</string> + <string name="cardboard_source">Bild</string> + <string name="cardboard_source_screen">Virtueller Bildschirm</string> + <string name="cardboard_source_sbs">Side-by-Side-3D</string> + <string name="cardboard_viewer">Brille</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Ohne Linse</string> + <string name="cardboard_ipd">Augenabstand</string> + <string name="cardboard_screen_width">Bildschirmbreite</string> + <string name="cardboard_screen_distance">Bildschirmabstand</string> + <string name="cardboard_head_tracking">Kopf-Tracking</string> + <string name="cardboard_no_sensor">Dieses Gerät hat keinen Drehsensor.</string> + <string name="cardboard_recenter">Ansicht zentrieren</string> + <string name="cardboard_recenter_button">Zentrieren</string> + <string name="cardboard_steamvr">SteamVR (experimentell)</string> + <string name="cardboard_steamvr_install">Installieren</string> + <string name="cardboard_steamvr_install_hint">Lädt Valves SteamVR für ARM64 herunter (571 MB).</string> + <string name="cardboard_steamvr_installing">SteamVR wird heruntergeladen und entpackt …</string> + <string name="cardboard_steamvr_start">Starten</string> + <string name="cardboard_steamvr_ready_hint">Startet SteamVR mit deinem Kopf als Headset; das Bild wechselt zu Side-by-Side-3D.</string> + <string name="cardboard_steamvr_stop">Beenden</string> + <string name="cardboard_steamvr_running_hint">SteamVR läuft.</string> + <string name="cardboard_steamvr_unavailable">In dieser Sitzung nicht verfügbar.</string> + <string name="cardboard_steamvr_failed">SteamVR hat nicht reagiert; Sitzungsprotokoll prüfen.</string> + <string name="drawer_component_downloads">Komponenten herunterladen</string> + <string name="drawer_component_downloads_hint">Downloads werden mit der Seite „Komponenten“ geteilt. Wähle oben eine heruntergeladene Version, um sie beim nächsten Spielstart zu verwenden.</string> + <string name="drawer_component_choose_download">Version zum Herunterladen wählen</string> + <string name="stores_fact_developer">Entwickler</string> + <string name="stores_fact_publisher">Publisher</string> + <string name="stores_fact_released">Erschienen</string> + <string name="stores_hltb_main">Hauptspiel</string> + <string name="stores_hltb_extras">+ Extras</string> + <string name="stores_hltb_completionist">Komplettierer</string> + <string name="stores_hltb_hours">%1$s Std.</string> + <string name="stores_hltb_minutes">%1$d Min.</string> +</resources> diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml new file mode 100644 index 000000000..efc1c59bd --- /dev/null +++ b/app/src/main/res/values-el/strings.xml @@ -0,0 +1,2235 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>Η πρώτη εκτέλεση κάνει λήψη του Steam. Αυτό μπορεί να διαρκέσει μερικά λεπτά.</item> + <item>Από προεπιλογή, η έξοδος από την εφαρμογή θέτει το Steam σε παύση· συνεχίζει από εκεί που είχε μείνει όταν επιστρέψετε.</item> + <item>Το χειριστήριό σας εμφανίζεται ως χειριστήριο Xbox 360. Διαφορετικά, χρησιμοποιήστε τα χειριστήρια στην οθόνη.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>Η πρώτη εκτέλεση ρυθμίζει την επιφάνεια εργασίας. Αυτό μπορεί να διαρκέσει μερικά λεπτά.</item> + <item>Από προεπιλογή, η έξοδος από την εφαρμογή θέτει τη συνεδρία σε παύση· συνεχίζει από εκεί που είχε μείνει όταν επιστρέψετε.</item> + <item>Το άγγιγμα λειτουργεί ως πινακίδα αφής: σύρετε για να μετακινήσετε τον δείκτη, πατήστε για κλικ.</item> + <item>Το χειριστήριό σας εμφανίζεται ως χειριστήριο Xbox 360. Διαφορετικά, χρησιμοποιήστε τα χειριστήρια στην οθόνη.</item> + </string-array> + <string name="session_channel">Συνεδρία</string> + <string name="session_channel_description">Εμφανίζεται όσο μια συνεδρία Steam ή επιφάνειας εργασίας είναι ανοιχτή</string> + <string name="session_notification">Το Steam εκτελείται</string> + <string name="session_notification_downloads">Λήψη Steam σε εξέλιξη</string> + <string name="session_notification_paused">Η συνεδρία παύθηκε - πατήστε Συνέχιση όταν είστε έτοιμοι</string> + <string name="resume_session">Συνέχιση</string> + <string name="suspend_now">Αναστολή τώρα</string> + <string name="stop_session">Διακοπή συνεδρίας</string> + <string name="frame_gen_title">Δημιουργία καρέ</string> + <string name="frame_gen_hint">Επιλέξτε τη λειτουργία δημιουργίας καρέ</string> + <string name="frame_gen_off">Ανενεργό</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG προσαρμοστικό %1$d</string> + <string name="lsfg_needs_steam">Απαιτεί το Lossless Scaling στο Steam</string> + <string name="lsfg_needs_install">Εγκαταστήστε το Lossless Scaling στο Steam ή εισαγάγετε το Lossless.dll</string> + <string name="lsfg_import">Εισαγωγή Lossless.dll…</string> + <string name="lsfg_using_steam">Χρήση του αντιγράφου από το Steam</string> + <string name="lsfg_using_import">Χρήση ενός εισαγμένου αντιγράφου</string> + <string name="lsfg_pick_title">Επιλογή Lossless.dll</string> + <string name="lsfg_imported">Το Lossless.dll εισήχθη</string> + <string name="lsfg_import_unchanged">Αυτό το Lossless.dll χρησιμοποιείται ήδη</string> + <string name="lsfg_import_not_owned">Αυτός ο λογαριασμός Steam δεν κατέχει το Lossless Scaling</string> + <string name="lsfg_import_invalid">Αυτό το αρχείο δεν είναι το Lossless.dll του Lossless Scaling</string> + <string name="lsfg_import_failed">Δεν ήταν δυνατή η προετοιμασία του LSFG από αυτό το Lossless.dll</string> + <string name="lsfg_problem_missing">Το LSFG απαιτεί το Lossless Scaling από το Steam ή ένα εισαγμένο Lossless.dll</string> + <string name="lsfg_problem_failed">Το LSFG δεν μπόρεσε να προετοιμάσει τα shaders του από το Lossless.dll</string> + <string name="hud_fg_unsupported">%1$s: μη υποστηριζόμενο</string> + <string name="hud_fg_failed">%1$s: αποτυχία μηχανής</string> + <string name="hud_fg_needs_lossless">%1$s: απαιτεί το Lossless Scaling</string> + <string name="hud_fg_starting">%1$s %2$s · ξεκινά</string> + <string name="second_library_import_hint">Προσθέτει μια βιβλιοθήκη Steam και τη σαρώνει για παιχνίδια Windows. Τα παιχνίδια που κατέχετε χρησιμοποιούν τις καταχωρίσεις τους στο Steam· τα υπόλοιπα γίνονται συντομεύσεις εκτός Steam.</string> + <string name="game_env_title">Περιβάλλον παιχνιδιού</string> + <string name="game_env_hint">Μεταβλητές περιβάλλοντος για παιχνίδια Proton.</string> + <string name="game_env_edit">Επεξεργασία</string> + <string name="game_env_shared">Όλα τα παιχνίδια Proton</string> + <string name="game_env_scope">Προφίλ</string> + <string name="game_env_app_id">ID εφαρμογής Steam</string> + <string name="game_env_open">Άνοιγμα</string> + <string name="game_env_profile">Παιχνίδι %1$s</string> + <string name="game_env_add">Προσθήκη μεταβλητής</string> + <string name="game_env_name">Όνομα μεταβλητής</string> + <string name="game_env_value">Τιμή</string> + <string name="game_env_save">Αποθήκευση</string> + <string name="game_env_remove">Αφαίρεση</string> + <string name="game_env_restore">Κληρονόμηση ξανά</string> + <string name="game_env_reset">Εκκαθάριση παρακάμψεων περιβάλλοντος</string> + <string name="game_env_unset">Κατάργηση ορισμού για αυτό το προφίλ</string> + <string name="game_env_empty">Δεν έχουν ορισθεί μεταβλητές.</string> + <string name="game_env_inherited">Κληρονομημένη</string> + <string name="game_env_custom">Προσαρμοσμένη μεταβλητή</string> + <string name="game_env_error">Δεν ήταν δυνατή η φόρτωση ή η αποθήκευση. Ελέγξτε τον ελεύθερο χώρο αποθήκευσης και δοκιμάστε ξανά.</string> + <string name="game_env_invalid">Χρησιμοποιήστε ένα έγκυρο όνομα και μια τιμή έως 8192 χαρακτήρων.</string> + <string name="game_env_unused">Το τυπικό runtime του Proton δεν διαβάζει αυτή τη μεταβλητή.</string> + <string name="game_env_loading">Φόρτωση…</string> + <string name="game_env_feature">Παράκαμψη επιπέδου λειτουργιών D3D12. Το 12_2 αναφέρει δυνατότητες DirectX 12 Ultimate· μη υποστηριζόμενες λειτουργίες μπορεί να αποτύχουν. Αφήστε το χωρίς ορισμό για αυτόματη ανίχνευση.</string> + <string name="game_env_shader">Παράκαμψη μοντέλου shader D3D12. Το 6_9 απαιτεί αρκετά πρόσφατο VKD3D και αντίστοιχες δυνατότητες GPU. Αφήστε το χωρίς ορισμό για αυτόματη ανίχνευση.</string> + <string name="game_env_cache">Διατηρήστε την προσωρινή αποθήκευση shader της Mesa ενεργοποιημένη για να μειώσετε την επανειλημμένη μεταγλώττιση. Η τιμή false ενεργοποιεί την προσωρινή αποθήκευση.</string> + <string name="game_env_cache_size">Μέγιστο μέγεθος προσωρινής μνήμης shader της Mesa, όπως 1G. Μεγαλύτερες προσωρινές μνήμες χρησιμοποιούν περισσότερο χώρο αποθήκευσης.</string> + <string name="game_env_vkd3d_config">Επιλογές VKD3D για D3D12. Το nodxr απενεργοποιεί το ray tracing· χρησιμοποιήστε το μόνο για παιχνίδι που το χρειάζεται.</string> + <string name="game_env_proton_log">Ορίστε σε 1 για διαγνωστικό αρχείο καταγραφής του Proton. Η καταγραφή μπορεί να μειώσει την απόδοση και να χρησιμοποιήσει χώρο αποθήκευσης.</string> + <string name="game_env_wined3d">Ορίστε σε 1 για χρήση του WineD3D με βάση OpenGL αντί του DXVK, για δοκιμές συμβατότητας D3D9–11.</string> + <string name="game_env_no_ntsync">Ορίστε σε 1 για να κρατήσετε αυτό το παιχνίδι εκτός ntsync (droiddeck-ntsync ή οδηγός πυρήνα)· χρησιμοποιεί το droiddeck-esync όπου υπάρχει πακέτο, διαφορετικά το droiddeck-fsync.</string> + <string name="game_env_no_esync">Ορίστε σε 1 για να απενεργοποιήσετε το droiddeck-esync για αυτό το παιχνίδι· χρησιμοποιεί αντ’ αυτού το droiddeck-fsync, ή το droiddeck-ntsync όταν είναι απενεργοποιημένο και το fsync.</string> + <string name="game_env_no_fsync">Ορίστε σε 1 για να κρατήσετε αυτό το παιχνίδι εκτός droiddeck-fsync· χρησιμοποιεί το droiddeck-esync όπου υπάρχει πακέτο, διαφορετικά το droiddeck-ntsync.</string> + <string name="game_env_fsync_first">Ορίστε σε 1 για εκτέλεση αυτού του παιχνιδιού με droiddeck-fsync, ή σε 0 για να προτάξετε το droiddeck-esync όπου ένα πακέτο ταιριάζει με το Proton του.</string> + <string name="game_env_xalia">Ορίστε σε 0 για να απενεργοποιήσετε τον βοηθό πλοήγησης με χειριστήριο του Proton για αυτό το παιχνίδι.</string> + <string name="game_env_hud">Επικάλυψη DXVK για D3D9–11. Το fps εμφανίζει τον ρυθμό καρέ· χωρίς ορισμό το αποκρύπτει.</string> + <string name="game_env_dxvk_config">Ρυθμίσεις DXVK χωρισμένες με ερωτηματικά. Οι πρόσφατες εκδόσεις χρησιμοποιούν το dxvk.maxFrameRate για τον περιορισμό καρέ· προτιμήστε τον περιοριστή καρέ της συνεδρίας για συνήθη χρήση.</string> + <string name="game_env_vkd3d_limit">Όριο ρυθμού καρέ D3D12. Το 0 απενεργοποιεί το όριο.</string> + <string name="game_env_glthread">Πολυνημάτωση OpenGL της Mesa. Το true μπορεί να βοηθήσει παιχνίδια OpenGL περιορισμένα από CPU· δοκιμάστε το ανά παιχνίδι.</string> + <string name="sync_backend_title">Συγχρονισμός Wine</string> + <string name="sync_backend_hint">Πώς τα παιχνίδια κρατούν τα νήματά τους συγχρονισμένα. Εφαρμόζεται στην επόμενη συνεδρία.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">Προεπιλογή FEX</string> + <string name="fex_next_launch">Εφαρμόζεται στην επόμενη εκκίνηση παιχνιδιού.</string> + <string name="game_settings_title">Παιχνίδια</string> + <string name="fex_default_label">Προεπιλογές FEX</string> + <string name="fex_default_detail">Χρησιμοποιεί τις προεπιλογές του FEX.</string> + <string name="fex_stability_label">Σταθερότητα</string> + <string name="fex_stability_detail">Οι πιο ασφαλείς, πιο αργές ρυθμίσεις.</string> + <string name="fex_compatibility_label">Συμβατότητα</string> + <string name="fex_compatibility_detail">Πλήρες TSO με multiblock.</string> + <string name="fex_intermediate_label">Ενδιάμεσο</string> + <string name="fex_intermediate_detail">Ισορροπημένο TSO με μειωμένη ακρίβεια x87.</string> + <string name="fex_performance_label">Απόδοση</string> + <string name="fex_performance_detail">Όλο το TSO απενεργοποιημένο· multiblock.</string> + <string name="fex_performance_tso_label">Απόδοση + TSO</string> + <string name="fex_performance_tso_detail">Ρυθμίσεις απόδοσης με το κύριο TSO ενεργοποιημένο.</string> + <string name="fex_extreme_label">Ακραίο</string> + <string name="fex_extreme_detail">Απενεργοποιεί τους ελέγχους αυτο-τροποποιούμενου κώδικα. Παιχνίδια με JIT και DRM μπορεί να πάθουν βλάβη.</string> + <string name="fex_extreme_tso_label">Ακραίο + TSO</string> + <string name="fex_extreme_tso_detail">Ρυθμίσεις Ακραίο με το κύριο TSO ενεργοποιημένο.</string> + <string name="fex_extreme_gn_label">Extreme-gn</string> + <string name="fex_extreme_gn_detail">Απόδοση με υποδείξεις TSC και μεταδεδομένων. Δοκιμάστε το πριν το Ακραίο.</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">Απόδοση με πλήρεις ελέγχους κώδικα και το bit υπερεπόπτη κρυμμένο.</string> + <string name="game_env_custom_value">Προσαρμοσμένη τιμή</string> + <string name="game_env_select_value">Επιλογή τιμής</string> + <string name="user_apps_section">Προσομοιωτές & εφαρμογές</string> + <string name="user_apps_add">Προσθήκη</string> + <string name="user_apps_add_detail">Η δική σας εφαρμογή ή παιχνίδι</string> + <string name="user_apps_kind_script">Σενάριο</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d%%</string> + <string name="user_apps_starting">Εκκίνηση…</string> + <string name="user_apps_added">Το %1$s προστέθηκε</string> + <string name="user_apps_removed">Το %1$s αφαιρέθηκε</string> + <string name="user_apps_back">Επιφάνεια εργασίας</string> + <string name="user_apps_gone">Αυτή η εφαρμογή δεν υπάρχει πια. Επιλέξτε την ξανά από την Επιφάνεια εργασίας.</string> + <string name="user_apps_open">Άνοιγμα %1$s</string> + <string name="user_apps_remove">Αφαίρεση</string> + <string name="user_apps_remove_confirm">Πατήστε ξανά για αφαίρεση</string> + <string name="user_apps_stop_session">Διακόψτε τη συνεδρία για αφαίρεση</string> + <string name="user_apps_script_linked">Εκτελεί το %1$s από τον φάκελό του, συνδεδεμένο στη συνεδρία όπου βρίσκεται. Η αφαίρεση της εφαρμογής αφήνει αυτόν τον φάκελο ανέγγιχτο.</string> + <string name="user_apps_script_copied">Εκτελεί το %1$s από ένα αντίγραφο του φακέλου του μέσα στο DroidDeck. Η αφαίρεση της εφαρμογής διαγράφει το αντίγραφο, όχι το πρωτότυπο.</string> + <string name="user_apps_runtime_required">Εγκαταστήστε πρώτα το runtime Linux</string> + <string name="user_apps_file_gone">Το αρχείο δεν υπάρχει πια</string> + <string name="user_apps_no_space">Δεν υπάρχει αρκετός ελεύθερος χώρος: η αντιγραφή του φακέλου απαιτεί %1$s</string> + <string name="user_apps_copying">Αντιγραφή %1$s</string> + <string name="user_apps_failed">Η προσθήκη της εφαρμογής απέτυχε</string> + <string name="user_apps_github_checking">Αναζήτηση έκδοσης του %1$s</string> + <string name="user_apps_github_unreachable">Δεν ήταν δυνατή η ανάγνωση των εκδόσεων του %1$s</string> + <string name="user_apps_github_no_appimage">Καμία έκδοση του %1$s δεν διαθέτει AppImage για ARM64 ή x86_64</string> + <string name="user_apps_downloading">Λήψη %1$s</string> + <string name="user_apps_download_failed">Η λήψη απέτυχε</string> + <string name="user_apps_update_failed">Η ενημέρωση της εφαρμογής απέτυχε</string> + <string name="user_apps_icon_failed">Δεν ήταν δυνατή η ανάγνωση αυτού του εικονιδίου</string> + <string name="user_apps_saved">Το %1$s αποθηκεύτηκε</string> + <string name="user_apps_edit">Επεξεργασία</string> + <string name="user_apps_check_updates">Έλεγχος για ενημερώσεις</string> + <string name="user_apps_checking_updates">Έλεγχος για ενημερώσεις…</string> + <string name="user_apps_update_to">Ενημέρωση σε %1$s</string> + <string name="user_apps_up_to_date">Ενημερωμένο · %1$s</string> + <string name="user_apps_updated">Το %1$s ενημερώθηκε</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">Προσθήκη εφαρμογής</string> + <string name="add_app_tab_script">Σενάριο</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">Ένα αρχείο .sh. Εκτελείται από τον δικό του φάκελο, σε πλήρη οθόνη μέσω gamescope.</string> + <string name="add_app_appimage_hint">Ένα AppImage για ARM64 (aarch64), ή ένα για x86_64 που εκτελείται μέσω FEX. Αποσυμπιέζεται μέσα στο runtime Linux μία φορά.</string> + <string name="add_app_github_hint">Εγκαθιστά το AppImage για ARM64 από την πιο πρόσφατη έκδοση του αποθετηρίου, ή το x86_64 μέσω FEX.</string> + <string name="app_fex_title">Χρήση FEX</string> + <string name="app_fex_auto">Αυτόματο</string> + <string name="app_fex_on">Ενεργό</string> + <string name="app_fex_off">Ανενεργό</string> + <string name="app_fex_hint">Το FEX εκτελεί προγράμματα x86 Linux σε αυτή τη συσκευή ARM64. Το Αυτόματο το χρησιμοποιεί όταν το πρόγραμμα είναι χτισμένο για x86.</string> + <string name="app_fex_x86">%1$s μέσω FEX</string> + <string name="app_fex_not_ready">Αυτό το πρόγραμμα είναι χτισμένο για υπολογιστές %1$s και εκτελείται μέσω FEX, το οποίο δεν έχει ρυθμιστεί ακόμα. Ανοίξτε το Steam μία φορά ενώ είστε συνδεδεμένοι: το DroidDeck κάνει λήψη του FEX και του Steam Linux Runtime που χρειάζεται, και μετά αυτό ανοίγει.</string> + <string name="session_end_program">Αυτό το πρόγραμμα</string> + <string name="session_end_fex_missing">Το %1$s είναι χτισμένο για υπολογιστές x86 και εκτελείται μέσω FEX, το οποίο δεν έχει εγκατασταθεί ακόμα. Εκκινήστε το Steam μία φορά ενώ είστε συνδεδεμένοι και αφήστε το να κάνει λήψη του FEX και του Steam Linux Runtime, μετά εκκινήστε αυτό ξανά.</string> + <string name="add_app_flatpak_hint">Ένα ID εφαρμογής Flathub ή η σελίδα της στο Flathub. Το Flatpak ρυθμίζεται πρώτα, αν δεν έχει ρυθμιστεί ακόμα.</string> + <string name="add_app_file">Αρχείο</string> + <string name="add_app_no_file">Δεν επιλέχθηκε αρχείο</string> + <string name="add_app_choose_file">Επιλογή αρχείου</string> + <string name="add_app_change_file">Αλλαγή</string> + <string name="add_app_pick_script">Επιλογή σεναρίου</string> + <string name="add_app_pick_appimage">Επιλογή AppImage</string> + <string name="add_app_pick_icon">Επιλογή εικονιδίου</string> + <string name="add_app_repository">Αποθετήριο</string> + <string name="add_app_repository_placeholder">owner/repo ή σύνδεσμος github.com</string> + <string name="add_app_repository_invalid">Δεν είναι αποθετήριο GitHub</string> + <string name="add_app_flatpak_id">ID εφαρμογής</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">Δεν είναι ID εφαρμογής Flathub</string> + <string name="add_app_flatpak_bundle_hint">Ή ένα αρχείο .flatpak, για εφαρμογή που δεν υπάρχει στο Flathub.</string> + <string name="add_app_pick_flatpak">Επιλογή αρχείου .flatpak</string> + <string name="add_app_source">Πηγή</string> + <string name="add_app_appearance">Στην Επιφάνεια εργασίας σας</string> + <string name="add_app_preview_name">Νέα εφαρμογή</string> + <string name="add_app_name">Όνομα</string> + <string name="add_app_name_placeholder">Από την εφαρμογή</string> + <string name="add_app_icon">Εικονίδιο</string> + <string name="add_app_icon_default">Προεπιλεγμένο εικονίδιο</string> + <string name="add_app_choose_icon">Επιλογή εικονιδίου</string> + <string name="add_app_clear_icon">Εκκαθάριση</string> + <string name="add_app_icon_suggestions">Από το αποθετήριο</string> + <string name="add_app_icon_searching">Αναζήτηση εικονιδίων…</string> + <string name="add_app_icon_reset">Προεπιλογή</string> + <string name="edit_app_title">Επεξεργασία %1$s</string> + <string name="edit_app_save">Αποθήκευση</string> + <string name="add_app_checking_folder">Έλεγχος φακέλου…</string> + <string name="add_app_folder_linked">Ο φάκελός του είναι συνδεδεμένος στη συνεδρία: %1$s</string> + <string name="add_app_folder_copied">Ο φάκελός του περιέχει προγράμματα Linux, τα οποία ο κοινός χώρος αποθήκευσης δεν μπορεί να εκτελέσει, οπότε αντιγράφεται μέσα στο DroidDeck (%1$s).</string> + <string name="add_app_confirm">Προσθήκη</string> + <string name="add_app_cancel">Ακύρωση</string> + <string name="add_app_runtime_required">Εγκαταστήστε πρώτα το runtime Linux από τη Ρύθμιση.</string> + <string name="add_app_busy">Περιμένετε να τελειώσει το %1$s.</string> + <string name="steam_compat_label">Συμβατό</string> + <string name="proton_compatible_note">Τα builds που φέρουν τη σήμανση «%1$s» στο μενού Συμβατότητα του Steam έχουν τις διορθώσεις prefix του DroidDeck και εκκινούν παιχνίδια εδώ. Η προεπιλογή είναι το Proton Experimental ARM64 (%1$s), ή το Proton 11.0 ARM64 (%1$s) μόλις εγκατασταθεί αυτό το νεότερο build. Οι δικές της καταχωρίσεις Proton της Valve χωρίς τη σήμανση δεν μπορούν να εκκινήσουν παιχνίδι σε αυτή τη συσκευή.</string> + <string name="proton_installed_compatible">Εγκατεστημένο %1$s · %2$s</string> + <string name="common_cancel">Ακύρωση</string> + <string name="common_auto">Αυτόματο</string> + <string name="common_never">Ποτέ</string> + <string name="common_always">Πάντα</string> + <string name="common_uninstall">Κατάργηση εγκατάστασης</string> + <string name="common_check">Έλεγχος</string> + <string name="common_update">Ενημέρωση</string> + <string name="common_checking">Έλεγχος…</string> + <string name="common_working">Σε εξέλιξη…</string> + <string name="common_forget">Διαγραφή</string> + <string name="common_add_ellipsis">Προσθήκη…</string> + <string name="common_use">Χρήση</string> + <string name="common_applies_next_session">Εφαρμόζεται στην επόμενη συνεδρία.</string> + <string name="mode_steam_title">Συνεδρία Steam</string> + <string name="mode_desktop_title">Συνεδρία επιφάνειας εργασίας</string> + <string name="mode_display">Οθόνη</string> + <string name="mode_resolution">Ανάλυση</string> + <string name="mode_res_720">Έως 720p</string> + <string name="mode_res_900">Έως 900p</string> + <string name="mode_res_1080">Έως 1080p</string> + <string name="mode_res_native">Εγγενής</string> + <string name="mode_res_default">%1$s (προεπιλογή)</string> + <string name="mode_res_custom_value">Προσαρμοσμένη · %1$d×%2$d</string> + <string name="mode_res_custom">Προσαρμοσμένη…</string> + <string name="mode_res_note">Το 720p μπορεί να κάνει τα μενού πιο αποκρίσιμα.</string> + <string name="mode_ratio">Αναλογία οθόνης</string> + <string name="mode_ratio_custom">Καθορίζεται από την προσαρμοσμένη ανάλυση.</string> + <string name="mode_ratio_auto">Το Αυτόματο χρησιμοποιεί τουλάχιστον 16:9.</string> + <string name="mode_fps">Όριο καρέ</string> + <string name="mode_fps_note">Περιορίζει ολόκληρη τη συνεδρία. Η οθόνη μεταβαίνει σε ρυθμό ανανέωσης που διαιρείται ακριβώς με το όριο.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">Έξοδος HDR10</string> + <string name="mode_hdr_unavailable">Μη διαθέσιμο: %1$s.</string> + <string name="mode_hdr_restart">Κάντε επανεκκίνηση της εφαρμογής για να εφαρμοστεί.</string> + <string name="mode_drivers">Προγράμματα οδήγησης</string> + <string name="mode_gpu_drivers">Προγράμματα οδήγησης GPU</string> + <string name="mode_gpu_drivers_hint">Κοινά για το Steam και την Επιφάνεια εργασίας. Ορίζονται στη σελίδα Στοιχεία.</string> + <string name="mode_touch_controls">Αφή & χειριστήρια</string> + <string name="mode_touch">Αφή</string> + <string name="mode_touch_touchpad">Πινακίδα αφής</string> + <string name="mode_touch_direct">Απευθείας</string> + <string name="mode_touch_note">Αυτόματο: πινακίδα αφής στην Επιφάνεια εργασίας, απευθείας στο Steam.</string> + <string name="mode_osc">Χειριστήρια στην οθόνη</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">Το Αυτόματο εμφανίζει τα χειριστήρια όταν δεν είναι συνδεδεμένο χειριστήριο.</string> + <string name="osc_steam_touch">Αφή Steam (beta)</string> + <string name="osc_style_next_session">Τα χειριστήρια αφής του Steam ξεκινούν από την επόμενη συνεδρία Steam</string> + <string name="osc_steam_layout">Διάταξη αφής Steam</string> + <string name="osc_steam_layout_hint">Μετακινήστε, αλλάξτε μέγεθος ή αποκρύψτε τα χειριστήρια αφής αυτού του παιχνιδιού· αποθηκεύεται στο Steam</string> + <string name="mode_controller">Χειριστήριο</string> + <string name="mode_controller_hint">Πώς βλέπει το Steam το χειριστήριό σας. Εφαρμόζεται στην επόμενη συνεδρία.</string> + <string name="mode_controller_deck">Χειριστήριο Steam Deck</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Χειριστήριο Xbox 360</string> + <string name="mode_controller_note">Deck: κουμπί Quick Access και γυροσκόπιο. Steam Controller: το ίδιο, με αίσθηση λαβής στα πίσω κουμπιά. DualShock 4 και DualSense: γυροσκόπιο, και τα trackpad ως πινακίδα αφής τους. Xbox 360: ανοίξτε το Quick Access με Guide+A.</string> + <string name="mode_back">Πίσω</string> + <string name="back_menu_then_qam">Μενού → QAM</string> + <string name="back_qam_then_menu">QAM → Μενού</string> + <string name="mode_session">Συνεδρία</string> + <string name="mode_suspend">Συμπεριφορά παρασκηνίου</string> + <string name="mode_suspend_hint">Όταν η εφαρμογή είναι κρυφή ή η οθόνη κλείνει.</string> + <string name="mode_suspend_manual">Μη αυτόματο</string> + <string name="mode_suspend_native">Εγγενής</string> + <string name="mode_suspend_steam_note">Το Αυτόματο θέτει σε παύση και συνεχίζει όταν εμφανίζεται. Το Εγγενής προετοιμάζει πρώτα το Steam για αδρανοποίηση. Το Μη αυτόματο περιμένει το Συνέχιση. Το Ποτέ συνεχίζει να εκτελείται.</string> + <string name="mode_background_downloads">Διατήρηση των λήψεων σε εξέλιξη</string> + <string name="mode_background_downloads_hint">Καθυστερεί την αναστολή παρασκηνίου όσο το Steam κάνει ενεργά λήψη.</string> + <string name="mode_suspend_note">Το Αυτόματο θέτει σε παύση στο παρασκήνιο και συνεχίζει όταν εμφανίζεται. Το Μη αυτόματο περιμένει το Συνέχιση. Το Ποτέ συνεχίζει να εκτελείται.</string> + <string name="mode_startup">Εκκίνηση</string> + <string name="mode_steam_startup">Εκκίνηση του Steam με το DroidDeck</string> + <string name="mode_steam_startup_hint">Ανοίγει τη συνεδρία Steam όταν εκκινείται το DroidDeck.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">Έλεγχος για εκδόσεις…</string> + <string name="mode_decky_ready">Έτοιμο για εγκατάσταση %1$s.</string> + <string name="mode_decky_none">Δεν βρέθηκε συμβατό build. Δοκιμάστε ξανά αργότερα.</string> + <string name="mode_decky_installed">Εγκατεστημένο · %1$s</string> + <string name="mode_decky_update">Διαθέσιμη ενημέρωση · %1$s</string> + <string name="mode_decky_current">Ενημερωμένο · %1$s</string> + <string name="mode_decky_loader">Loader</string> + <string name="mode_decky_install_latest">Εγκατάσταση τελευταίας έκδοσης</string> + <string name="mode_decky_on">Ξεκινά με το Steam. Όσο είναι ενεργό, άλλες εφαρμογές σε αυτή τη συσκευή μπορεί να είναι σε θέση να ελέγξουν το Steam.</string> + <string name="mode_decky_off">Ανενεργό. Η θύρα απομακρυσμένου εντοπισμού σφαλμάτων του Steam παραμένει κλειστή.</string> + <string name="mode_decky_remove_title">Κατάργηση εγκατάστασης του Decky Loader;</string> + <string name="mode_decky_remove_text">Καταργεί τον loader. Τα plugins και οι ρυθμίσεις σας διατηρούνται.</string> + <string name="mode_client">Πελάτης</string> + <string name="mode_mangoapp">Επικάλυψη απόδοσης</string> + <string name="mode_mangoapp_hint">Απενεργοποιήστε το αν τα παιχνίδια κολλάνε ή μαυρίζει η οθόνη σε λειτουργία Deck. Εφαρμόζεται στην επόμενη συνεδρία.</string> + <string name="mode_steam_repair">Επιδιόρθωση πελάτη Steam</string> + <string name="mode_steam_repair_hint">Κάνει ξανά λήψη του πελάτη Steam την επόμενη φορά που θα ξεκινήσει το Steam. Χρησιμοποιήστε το αν το Steam παραμένει στην οθόνη φόρτωσης. Τα παιχνίδια, η σύνδεση και οι ρυθμίσεις διατηρούνται.</string> + <string name="mode_steam_repair_queued">Ο πελάτης Steam θα ληφθεί ξανά την επόμενη φορά που θα ξεκινήσει το Steam.</string> + <string name="mode_steam_repair_button">Επιδιόρθωση</string> + <string name="mode_added_art">Εικαστικά</string> + <string name="mode_sgdb_key">Κλειδί API SteamGridDB</string> + <string name="mode_sgdb_key_yours">Το κλειδί σας</string> + <string name="mode_sgdb_edit">Επεξεργασία…</string> + <string name="mode_added_art_hint">Συμπληρώνει τα εικαστικά που λείπουν από το Steam, έπειτα από το SteamGridDB. Τα δικά σας cover.jpg, hero.jpg, logo.png ή icon.png στον φάκελο του παιχνιδιού έχουν προτεραιότητα.</string> + <string name="summary_remove_game">Αφαίρεση %1$s</string> + <string name="mode_added_remove_confirm">Αφαίρεση;</string> + <string name="games_add">Προσθήκη παιχνιδιού</string> + <string name="games_add_title">Προσθήκη παιχνιδιού:</string> + <string name="games_pick_exe_title">Επιλέξτε το exe:</string> + <string name="games_add_unreachable">Αυτή η τοποθεσία δεν είναι διαθέσιμη στη συνεδρία</string> + <string name="mode_added_exe_check">Έλεγχος αυτού του exe</string> + <string name="wincomp_title">Στοιχεία Windows</string> + <string name="wincomp_pad_hint">Τα LB / RB μεταπηδούν ανάμεσα στις ενότητες.</string> + <string name="wincomp_card_none">Κανένα</string> + <string name="wincomp_applies">DLL των Windows που το Proton δεν περιλαμβάνει, από τη λίστα στοιχείων. Ενεργοποιήστε όσα χρειάζεται αυτό το παιχνίδι: τοποθετούνται στον δικό του φάκελο Windows, ποτέ στα αρχεία του, και παραμένουν ανεξάρτητα από το Proton με το οποίο εκτελείται.</string> + <string name="wincomp_offline">Δεν ήταν δυνατή η πρόσβαση στη λίστα στοιχείων. Τα στοιχεία που έχουν ληφθεί ήδη μπορούν ακόμα να αλλάξουν.</string> + <string name="wincomp_recommended">Προτείνεται για αυτό το παιχνίδι</string> + <string name="wincomp_checking">Έλεγχος τι χρειάζεται αυτό το παιχνίδι…</string> + <string name="wincomp_when_online">Διαθέσιμο με σύνδεση στο διαδίκτυο</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">Ενεργοποίηση %1$d στοιχείου…</item> + <item quantity="other">Ενεργοποίηση %1$d στοιχείων…</item> + </plurals> + <string name="wincomp_auto_section">Ενεργό για αυτό το παιχνίδι</string> + <string name="wincomp_auto_steam">Το Steam εγκαθιστά το %1$s με το %2$s (εφαρμογή %3$s)</string> + <string name="wincomp_auto_gog">Από τη λίστα του GOG για αυτό το παιχνίδι</string> + <string name="wincomp_auto_epic">Από τις προαπαιτήσεις του Epic για αυτό το παιχνίδι</string> + <string name="wincomp_auto_amazon">Από τα βήματα εγκατάστασης του Amazon για αυτό το παιχνίδι</string> + <string name="wincomp_steam_by_name">Το Steam εγκαθιστά το %1$s με την εφαρμογή %2$s, με αντιστοίχιση ονόματος</string> + <string name="wincomp_suggestions">Αναφέρονται επίσης</string> + <string name="wincomp_suggestion_reason">Αναφέρεται από %1$s· δεν είναι στοιχείο εδώ</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">Το παιχνίδι διανέμει το %1$s</string> + <string name="wincomp_found_shipped">Το παιχνίδι διαθέτει το %1$s (συχνά λειτουργεί ως έχει)</string> + <string name="wincomp_already_here">Υπάρχει ήδη σε αυτό το παιχνίδι</string> + <string name="wincomp_no_recommendation">Τίποτα στον φάκελο του παιχνιδιού δεν παραπέμπει σε στοιχείο.</string> + <string name="wincomp_active">Εγκατεστημένα για αυτό το παιχνίδι (%1$d)</string> + <string name="wincomp_all">Όλα τα στοιχεία (%1$d)</string> + <string name="wincomp_installers">Απαιτούν πρόγραμμα εγκατάστασης Windows (%1$d)</string> + <string name="wincomp_installers_note">Αυτά είναι δικά τους προγράμματα εγκατάστασης χωρίς πακέτο μέσα για αποσυμπίεση, και χωρίς καταγραφή ακόμα για ό,τι εγκαθιστούν. Τα περισσότερα είναι παλιά runtime που το Mono μέσα στο Proton ήδη αντικαθιστά· καταγραφές προστίθενται όσο γίνονται.</string> + <string name="wincomp_show">Εμφάνιση</string> + <string name="wincomp_hide">Απόκρυψη</string> + <string name="wincomp_needs_installer">Εκτελεί το δικό του πρόγραμμα εγκατάστασης (χωρίς καταγραφή ακόμα)</string> + <string name="wincomp_unsupported">Δεν υποστηρίζεται ακόμα εδώ</string> + <string name="wincomp_loading">Φόρτωση των στοιχείων…</string> + <string name="wincomp_failed">Δεν ήταν δυνατή η εγκατάσταση: %1$s</string> + <string name="wincomp_downloaded">Λήφθηκε</string> + <string name="wincomp_downloading">Λήψη %1$s</string> + <string name="wincomp_installing">Εγκατάσταση: %1$s</string> + <string name="wincomp_reverting">Απενεργοποίηση…</string> + <string name="wincomp_download_size">Λήψη %1$s</string> + <string name="wincomp_next_launch">Εφαρμόζεται την επόμενη φορά που θα ξεκινήσει το παιχνίδι.</string> + <string name="mode_stretch169">Τέντωμα παιχνιδιών 16:9</string> + <string name="mode_stretch169_hint">Σε οθόνες πιο τετράγωνες από 16:9, τεντώνει τα παιχνίδια για να καλύψουν την οθόνη αντί να εμφανίζονται μαύρες λωρίδες. Εφαρμόζεται στην επόμενη συνεδρία.</string> + <string name="mode_audio">Ήχος</string> + <string name="mode_directaudio">DirectAudio για παιχνίδια</string> + <string name="mode_directaudio_hint">Παρακάμπτει το PulseAudio για μικρότερη καθυστέρηση.</string> + <string name="mode_mic">Μικρόφωνο</string> + <string name="mode_mic_hint">Για φωνητική συνομιλία.</string> + <string name="mode_network">Δίκτυο</string> + <string name="mode_wifi_discovery">Εντοπισμός Wi-Fi</string> + <string name="mode_wifi_discovery_hint">Εμφανίζει ονόματα Wi-Fi και κοντινά δίκτυα στο Steam.</string> + <string name="mode_wifi_location_off">Οι υπηρεσίες τοποθεσίας είναι απενεργοποιημένες.</string> + <string name="mode_wifi_permission_denied">Δεν έχει δοθεί άδεια τοποθεσίας.</string> + <string name="mode_wifi_open_settings">Άνοιγμα ρυθμίσεων</string> + <string name="mode_wifi_continue">Συνέχεια</string> + <string name="mode_wifi_explain_title">Ενεργοποίηση εντοπισμού Wi-Fi;</string> + <string name="mode_wifi_explain_text">Το Android απαιτεί άδεια τοποθεσίας για να διαβάσει ονόματα Wi-Fi και να εντοπίσει κοντινά δίκτυα. Το DroidDeck χρησιμοποιεί αυτή την πρόσβαση για να εμφανίσει αυτά τα δίκτυα στο Steam. Δεν παρακολουθούμε την τοποθεσία σας. Η πρόσβαση στο διαδίκτυο λειτουργεί χωρίς αυτή την άδεια.</string> + <string name="mode_storage">Αποθηκευτικός χώρος παιχνιδιών</string> + <string name="mode_storage_auto">Αυτόματο - η κάρτα SD όταν υπάρχει, καθώς και οι φάκελοι Games σας</string> + <string name="mode_storage_auto_none">Αυτόματο - οι φάκελοι Games σας, καθώς και η κάρτα SD όταν υπάρχει (καμία τώρα)</string> + <string name="mode_storage_internal">Μόνο εσωτερική αποθήκευση</string> + <string name="mode_storage_folder">Φάκελος: %1$s</string> + <string name="mode_second_library">Δεύτερη βιβλιοθήκη</string> + <string name="mode_storage_note">Παιχνίδια που μεταδίδουν δεδομένα από κάρτα SD ή κοινόχρηστο χώρο αποθήκευσης μπορεί να κολλάνε.</string> + <string name="mode_storage_diagnostics">Διαγνωστικά αποθηκευτικού χώρου</string> + <string name="mode_storage_diagnostics_hint">Καταγράφει αποτελέσματα κατανομής και δειγματοληπτικούς χρόνους αποθήκευσης στην Κοινή χρήση αρχείων καταγραφής. Μπορεί να επιβραδύνει τις εγκαταστάσεις όσο είναι ενεργό. Εφαρμόζεται στην επόμενη συνεδρία.</string> + <string name="mode_choose_folder">Επιλογή φακέλου…</string> + <string name="mode_custom_res_title">Προσαρμοσμένη ανάλυση</string> + <string name="mode_custom_res_text">Αντικαθιστά το όριο και την αναλογία. Ένα μέγεθος που δεν ταιριάζει με την οθόνη εμφανίζει μαύρες λωρίδες.</string> + <string name="mode_width">Πλάτος</string> + <string name="mode_height">Ύψος</string> + <string name="mode_custom_res_range">Μεταξύ 320×240 και 3840×2160.</string> + <plurals name="mode_added_count"> + <item quantity="one">%1$d παιχνίδι</item> + <item quantity="other">%1$d παιχνίδια</item> + </plurals> + <string name="common_ok">OK</string> + <string name="common_delete">Διαγραφή</string> + <string name="common_restore">Επαναφορά</string> + <string name="common_download">Λήψη</string> + <string name="common_delete_named">Διαγραφή %1$s</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Πρόγραμμα οδήγησης runtime</string> + <string name="comp_runtime_driver_hint">Χρησιμοποιείται από το Steam, τα παιχνίδια και την επιφάνεια εργασίας. Εφαρμόζεται στην επόμενη συνεδρία.</string> + <string name="comp_import_turnip">Εισαγωγή zip Turnip…</string> + <string name="comp_display_driver">Πρόγραμμα οδήγησης οθόνης</string> + <string name="comp_display_driver_hint">Σχεδιάζει τα καρέ στην οθόνη και στις δύο λειτουργίες. Κάντε επανεκκίνηση της εφαρμογής για εφαρμογή.</string> + <string name="comp_import_adrenotools">Εισαγωγή zip AdrenoTools…</string> + <string name="comp_never">ποτέ</string> + <string name="comp_nightlies_checked">Έλεγχος Nightlies %1$s</string> + <string name="comp_title">Στοιχεία</string> + <string name="comp_about">Σχετικά με τα Στοιχεία</string> + <string name="comp_check_drivers">Έλεγχος για νέα προγράμματα οδήγησης</string> + <string name="comp_check_nightlies">Έλεγχος των Nightlies για νέα πακέτα</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">Το Proton που επιλέγεται εδώ εκτελεί κάθε παιχνίδι που δεν έχει επιλέξει το δικό του στο Steam, και μια αλλαγή που γίνεται στο Steam εμφανίζεται και εδώ. Το Steam αντικαθιστά το Proton της Valve όταν ενημερώνεται· τα πρωτότυπα διατηρούνται ανά build.</string> + <string name="comp_scope">Διαμόρφωση</string> + <string name="comp_profile_configure">Διαμόρφωση προφίλ</string> + <string name="comp_profile">Προφίλ</string> + <string name="comp_scope_general">Προεπιλεγμένο προφίλ</string> + <string name="comp_scope_general_detail">Χρησιμοποιείται αυτόματα από παιχνίδια χωρίς παρακάμψεις.</string> + <string name="comp_profile_game_detail">Αποθηκευμένες ρυθμίσεις που χρησιμοποιούνται αυτόματα όταν εκκινείται το %1$s.</string> + <string name="comp_game_hint">Επιλέξτε μόνο τις ρυθμίσεις που χρειάζεται αυτό το παιχνίδι. Όλα τα υπόλοιπα συνεχίζουν να χρησιμοποιούν το Προεπιλεγμένο προφίλ.</string> + <string name="comp_game_fex">Προεπιλογή FEX</string> + <string name="comp_game_fex_inherited">Χρησιμοποιεί την προεπιλογή του Προεπιλεγμένου προφίλ.</string> + <string name="comp_game_fex_override">Αποθηκεύτηκε προσαρμοσμένη προεπιλογή για αυτό το παιχνίδι.</string> + <string name="comp_game_fex_custom">Προσαρμοσμένες μεταβλητές FEX</string> + <string name="comp_game_use_general">Χρήση Προεπιλεγμένου</string> + <string name="comp_game_compatibility">Συμβατότητα παιχνιδιού</string> + <string name="comp_game_environment_hint">Επιλογές περιβάλλοντος Proton, DXVK, VKD3D και συγχρονισμού για αυτό το παιχνίδι.</string> + <string name="comp_game_proton_inherited">Χρησιμοποιεί το Proton του Προεπιλεγμένου προφίλ.</string> + <string name="comp_game_proton_override">Αποθηκεύτηκε προσαρμοσμένο Proton για αυτό το παιχνίδι μέσω της αντιστοίχισης συμβατότητας του Steam.</string> + <string name="comp_no_proton_short">Χωρίς Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Χρησιμοποιεί το πακέτο του Προεπιλεγμένου προφίλ από %1$s, εκτός αν αυτό το παιχνίδι επιλέξει άλλο πακέτο.</string> + <string name="comp_game_component_general">Χρησιμοποιεί το πακέτο στοιχείων του Προεπιλεγμένου προφίλ.</string> + <string name="comp_game_component_override">Χρησιμοποιεί το %1$s μόνο για αυτό το παιχνίδι.</string> + <string name="comp_game_runtime_options">Επιλογές runtime</string> + <string name="comp_game_environment_other_hint">Άλλες επιλογές περιβάλλοντος Proton, DXVK, VKD3D-Proton και συγχρονισμού ανά παιχνίδι.</string> + <string name="comp_game_saved_next">Το προφίλ του %1$s αποθηκεύτηκε για την επόμενη εκκίνηση του παιχνιδιού.</string> + <string name="game_no_steam_title">Εκκίνηση χωρίς το Steam</string> + <string name="game_no_steam_detail">Για παιχνίδι που κλείνει αμέσως: το εκκινεί χωρίς τη ρύθμιση του Steam στο Proton. Λειτουργίες του Steam, όπως το overlay, είναι απενεργοποιημένες για αυτό.</string> + <string name="game_no_steam_on">Ενεργό</string> + <string name="comp_game_proton_live">Η επιλογή Proton του %1$s αποθηκεύτηκε και είναι διαθέσιμη στο Steam τώρα.</string> + <string name="comp_game_proton_next">Η επιλογή Proton του %1$s αποθηκεύτηκε. Κάντε επανεκκίνηση του Steam πριν εκκινήσετε το παιχνίδι.</string> + <string name="comp_game_proton_not_runnable">Το %1$s δεν μπορεί να χρησιμοποιήσει το επιλεγμένο Proton σε αυτή τη συσκευή.</string> + <string name="comp_game_load_failed">Δεν ήταν δυνατή η φόρτωση του προφίλ του %1$s: %2$s</string> + <string name="comp_game_save_failed">Δεν ήταν δυνατή η αποθήκευση του προφίλ παιχνιδιού: %1$s</string> + <string name="comp_game_reset">Επαναφορά προφίλ παιχνιδιού</string> + <string name="comp_game_reset_title">Επαναφορά του προφίλ του %1$s;</string> + <string name="comp_game_reset_body">Αφαίρεση του Proton, του FEX, του πακέτου στοιχείων, του περιβάλλοντος, του φιλτραρίσματος υφών και των παρακάμψεων στοιχείων Windows αυτού του παιχνιδιού; Θα χρησιμοποιήσει το Προεπιλεγμένο προφίλ.</string> + <string name="comp_game_reset_done">Το %1$s χρησιμοποιεί τώρα το Προεπιλεγμένο προφίλ.</string> + <string name="comp_game_reset_failed">Δεν ήταν δυνατή η πλήρης επαναφορά του προφίλ παιχνιδιού: %1$s</string> + <string name="comp_default_live">Το %1$s εκτελεί τώρα κάθε παιχνίδι που δεν έχει επιλέξει το δικό του Proton στο Steam.</string> + <string name="comp_default_next">Το %1$s θα εκτελεί κάθε παιχνίδι που δεν έχει επιλέξει το δικό του Proton, από την επόμενη φορά που θα ξεκινήσει το Steam.</string> + <string name="comp_default_not_runnable">Το %1$s δεν μπορεί να εκκινήσει παιχνίδια εδώ, οπότε δεν μπορεί να είναι η προεπιλογή.</string> + <string name="comp_default_failed">Δεν ήταν δυνατή η αποθήκευση του προεπιλεγμένου Proton: %1$s</string> + <string name="comp_swapped">%1$s · %2$d αντικαταστάθηκαν</string> + <string name="comp_all_original">%1$s · όλα πρωτότυπα</string> + <string name="comp_game_running_suffix">%1$s · το παιχνίδι εκτελείται</string> + <string name="comp_esync_pack">%1$s · πακέτο droiddeck-esync</string> + <string name="comp_esync_wanted">%1$s · χωρίς πακέτο droiddeck-esync ακόμα</string> + <string name="comp_esync_builtin">%1$s · ενσωματωμένο esync, δεν χρειάζεται πακέτο</string> + <string name="comp_gpu_drivers">Προγράμματα οδήγησης GPU</string> + <string name="comp_reapplied">%1$s · επαναφέρθηκε στην εκκίνηση %2$s</string> + <string name="comp_next_queued">Επόμενο: %1$s · μετά το κλείσιμο του παιχνιδιού</string> + <string name="comp_in_use">%1$s σε χρήση: %2$s</string> + <string name="comp_waits">Περιμένει μέχρι να κλείσει το παιχνίδι</string> + <string name="comp_applies_next_game">Εφαρμόζεται την επόμενη φορά που θα ξεκινήσει ένα παιχνίδι</string> + <string name="comp_import_zip">Εισαγωγή .zip</string> + <string name="comp_reading">Ανάγνωση Proton…</string> + <string name="comp_no_proton">Δεν έχει εγκατασταθεί Proton ακόμα. Εκκινήστε το Steam μία φορά για λήψη του ARM64 Proton του, ή προσθέστε GE-Proton, CachyOS ή DroidDeck-Proton από τη Ρύθμιση.</string> + <string name="comp_original">Πρωτότυπο · %1$s</string> + <string name="comp_this_build">%1$s · αυτό το build</string> + <string name="comp_earlier_build">%1$s · προηγούμενο build</string> + <string name="comp_installed_count">Εγκατεστημένα · %1$d</string> + <string name="comp_nothing_stored">Δεν έχει αποθηκευτεί τίποτα για το %1$s ακόμα. Τα δικά του αρχεία αυτού του Proton αποθηκεύονται ως το πρωτότυπό του.</string> + <string name="comp_waits_long">Ένα παιχνίδι εκτελείται σε αυτό το Proton, οπότε αυτό περιμένει μέχρι να κλείσει.</string> + <string name="comp_applies_long">Εφαρμόζεται την επόμενη φορά που θα ξεκινήσει ένα παιχνίδι.</string> + <string name="comp_swap_title">Αντικατάσταση %1$s;</string> + <string name="comp_swap_body">Τοποθέτηση του %1$s στο %2$s; Τα πρωτότυπα αρχεία του παραμένουν αποθηκευμένα. %3$s</string> + <string name="comp_restore_title">Επαναφορά %1$s;</string> + <string name="comp_restore_body">Επαναφορά του πρωτότυπου %1$s στο %2$s; %3$s</string> + <string name="comp_delete_package_body">Αφαιρεί το πακέτο από την εφαρμογή. Μπορείτε να το λάβετε ξανά.</string> + <string name="comp_delete_original_title">Διαγραφή αυτού του πρωτοτύπου;</string> + <string name="comp_delete_original_body">Αυτό το πρωτότυπο %1$s είναι από προηγούμενο build του %2$s. Το πρωτότυπο του τρέχοντος build διατηρείται πάντα.</string> + <string name="comp_nightlies_count">Στα Nightlies · %1$d</string> + <string name="comp_refresh_to_list">Ανανεώστε για να εμφανιστούν τα πακέτα στα Nightlies.</string> + <string name="comp_nothing_new">Τίποτα νέο από τον τελευταίο έλεγχο.</string> + <string name="comp_import_wcp">Εισαγωγή .wcp</string> + <string name="comp_restore_original">Επαναφορά του πρωτοτύπου αυτού του Proton</string> + <string name="comp_restore_original_body">Επαναφορά του πρωτότυπου %2$s του %1$s;</string> + <string name="comp_swap">Αντικατάσταση</string> + <string name="comp_about_text">FEX, DXVK και VKD3D-Proton για κάθε Proton με το οποίο το Steam εκτελεί παιχνίδια.\n\nΕπιλέξτε μια εγκατεστημένη γραμμή για να την αντικαταστήσετε. Εφαρμόζεται την επόμενη φορά που θα ξεκινήσει ένα παιχνίδι, ή μετά το κλείσιμο του τρέχοντος παιχνιδιού.\n\nΟι επιλογές σας επανελέγχονται πριν από κάθε εκκίνηση και επανέρχονται αν κάτι τις άλλαξε. Τα πρωτότυπα αρχεία κάθε Proton διατηρούνται, έτσι οι ενημερώσεις του Steam δεν τα χάνουν ποτέ.</string> + <string name="comp_tag_original">Πρωτότυπο</string> + <string name="comp_tag_in_use">Σε χρήση</string> + <string name="comp_tag_new">Νέο</string> + <string name="comp_tag_stored">Αποθηκευμένο</string> + <string name="perf_title">Απόδοση</string> + <string name="perf_lede">Πυρήνες CPU, διορθώσεις συνεδρίας και το όνομα κεντρικού υπολογιστή. Εφαρμόζεται στην επόμενη συνεδρία.</string> + <string name="perf_client_cores_group">Πυρήνες πελάτη Steam</string> + <string name="perf_override">Παράκαμψη της επιλογής πυρήνων του Steam</string> + <string name="perf_override_hint">Καθηλώνει το Steam, τον βοηθό διεπαφής του και το gamescope στους παρακάτω πυρήνες.</string> + <string name="perf_client_cores">Πυρήνες πελάτη</string> + <string name="perf_client_cores_on">Πυρήνες για το Steam.</string> + <string name="perf_client_cores_off">Ενεργοποιήστε την παράκαμψη για να επιλέξετε.</string> + <string name="perf_game_cores_group">Πυρήνες παιχνιδιού</string> + <string name="perf_game_cores">Πυρήνες παιχνιδιού</string> + <string name="perf_game_cores_hint">Πυρήνες για παιχνίδια. Με όλους επιλεγμένους, το Android τους προγραμματίζει ελεύθερα.</string> + <string name="perf_client_ui">Διεπαφή πελάτη</string> + <string name="perf_glthread">Νηματοποιημένο GL</string> + <string name="perf_glthread_hint">Μπορεί να κάνει τα μενού του Steam πιο αποκρίσιμα.</string> + <string name="perf_zink">Zink: lazy descriptors</string> + <string name="perf_zink_hint">Προτείνεται για προγράμματα οδήγησης χωρίς descriptor buffers.</string> + <string name="perf_noglerror">Παράβλεψη ελέγχων σφαλμάτων GL</string> + <string name="perf_noglerror_hint">Απενεργοποιεί την επικύρωση GL ανά κλήση.</string> + <string name="perf_gsrealtime">gamescope: ουρά GPU πραγματικού χρόνου</string> + <string name="perf_gsrealtime_hint">Δίνει προτεραιότητα στην εργασία GPU του compositor. Μπορεί να εξομαλύνει τον ρυθμό καρέ με κάποιο κόστος για τα παιχνίδια.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">Διατήρηση της GPU στη μέγιστη συχνότητά της</string> + <string name="perf_gpuclock_hint">Μόνο Adreno. Λιγότερα κολλήματα, μεγαλύτερη χρήση μπαταρίας και θερμότητα.</string> + <string name="perf_fixes">Διορθώσεις συνεδρίας</string> + <string name="perf_sysmem">Turnip: απόδοση sysmem</string> + <string name="perf_sysmem_hint">Ταχύτερο για το Steam και τα περισσότερα παιχνίδια. Απαιτείται σε Adreno 710/720/722.</string> + <string name="perf_xalia">Παράβλεψη του βοηθού xalia του Steam</string> + <string name="perf_fastsync">droiddeck-ntsync (πειραματικό)</string> + <string name="perf_fastsync_hint">Ένα υποκατάστατο σε επίπεδο χρήστη για το πρόγραμμα οδήγησης ntsync του Linux, για δοκιμές. Όσο είναι ενεργό, το Proton το χρησιμοποιεί αντί για το droiddeck-esync και το droiddeck-fsync. Λειτουργεί με Proton Experimental ARM64, GE-Proton και proton-cachyos. Εφαρμόζεται στην επόμενη συνεδρία.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync πρώτα</string> + <string name="perf_fsync_hint">Εκτελεί το δικό του fsync του Proton στα απλά αρχεία Proton, με το futex_waitv παρεχόμενο σε επίπεδο χρήστη, οπότε δεν χρειάζεται πακέτο και συνεχίζει να λειτουργεί τη στιγμή που το Steam ενημερώνει το Proton. Όσο είναι ενεργό, τα παιχνίδια το χρησιμοποιούν πριν από το droiddeck-esync, εκτός από τα λίγα που το Proton κρατά εκτός fsync. Εφαρμόζεται στην επόμενη συνεδρία.</string> + <string name="perf_esync_hint">Τα νήματα Windows των παιχνιδιών περιμένουν το ένα το άλλο μέσω eventfd και κοινόχρηστης μνήμης αντί για διαδρομές μέσω wineserver. Χρησιμοποιεί ένα πακέτο droiddeck-esync φτιαγμένο ειδικά για αυτό το build του Proton· τα πακέτα λαμβάνονται αυτόματα. Μέχρι να υπάρξει ένα για νέο build του Proton, το droiddeck-fsync το αναπληρώνει. Εφαρμόζεται στην επόμενη συνεδρία.</string> + <string name="perf_xalia_hint">Απενεργοποιεί τον βοηθό πλοήγησης με χειριστήριο του Proton, που κοστίζει CPU σε κάθε παιχνίδι.</string> + <string name="perf_seccomp">Εκτέλεση του proot χωρίς seccomp</string> + <string name="perf_seccomp_hint">Μπορεί να διορθώσει σφάλματα απόντων syscall σε ορισμένους πυρήνες. Μπορεί να μειώσει την απόδοση.</string> + <string name="perf_fastpath">Ταχεία διαδρομή proot</string> + <string name="perf_fastpath_needs_seccomp">Απαιτεί το φίλτρο seccomp του proot.</string> + <string name="perf_fastpath_hint">Ταχύτερη αναζήτηση αρχείων και λιγότερα κολλήματα. Απενεργοποιήστε το αν ένα πρόγραμμα δεν μπορεί να βρει τα αρχεία του.</string> + <string name="perf_identity">Ταυτότητα συνεδρίας</string> + <string name="perf_hostname">Όνομα κεντρικού υπολογιστή</string> + <string name="perf_hostname_hint">Το όνομα που βλέπει το Steam για αυτή τη συσκευή. Αν το αφήσετε κενό, επανέρχεται σε %1$s.</string> + <string name="perf_hostname_invalid">Μόνο γράμματα, ψηφία και εσωτερικές παύλες, έως 63 χαρακτήρες.</string> + <string name="perf_phantom_warning">Το Android είναι ρυθμισμένο να διακόπτει αυτή τη συνεδρία</string> + <string name="driver_installed">Εγκατεστημένο</string> + <string name="driver_available">Διαθέσιμο για λήψη</string> + <string name="driver_nothing_new">Τίποτα νέο από τον τελευταίο έλεγχο.</string> + <string name="driver_restore_builtin">Επαναφορά ενσωματωμένων προγραμμάτων οδήγησης</string> + <string name="driver_delete_builtin">Ενσωματωμένο στην εφαρμογή: αυτό αφαιρεί τα αποσυμπιεσμένα αρχεία του και το αποκρύπτει. Η Επαναφορά ενσωματωμένων προγραμμάτων οδήγησης το επαναφέρει.</string> + <string name="driver_delete_bundle">Το ένα μισό ενός πακέτου Android + Linux: αφαιρούνται και τα δύο μισά. Ένα πρόγραμμα οδήγησης σε χρήση επανέρχεται στο προεπιλεγμένο.</string> + <string name="driver_delete_plain">Τα αρχεία του αφαιρούνται από την εφαρμογή.</string> + <string name="driver_delete_in_use">Τα αρχεία του αφαιρούνται. Είναι σε χρήση, οπότε το προεπιλεγμένο παίρνει τη θέση του.</string> + <string name="drawer_page_display">Οθόνη</string> + <string name="drawer_page_effects">Εφέ</string> + <string name="drawer_page_controls">Χειριστήρια</string> + <string name="drawer_page_components">Στοιχεία</string> + <string name="drawer_page_controller">Χειριστήριο</string> + <string name="drawer_page_settings">Ρυθμίσεις</string> + <string name="drawer_page_desc">Σελίδα %1$s</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Επιφάνεια εργασίας</string> + <string name="drawer_steam_menu">Μενού Steam</string> + <string name="drawer_quick_access">Quick access</string> + <string name="drawer_open_qam">Άνοιγμα Quick Access Menu</string> + <string name="drawer_prev_page">Προηγούμενη σελίδα</string> + <string name="drawer_next_page">Επόμενη σελίδα</string> + <string name="drawer_hud">HUD απόδοσης</string> + <string name="drawer_scaling">Λειτουργία κλιμάκωσης</string> + <string name="drawer_scaling_sharpness">Οξύτητα κλιμάκωσης</string> + <string name="drawer_effects">Εφέ οθόνης</string> + <string name="drawer_effects_hint">Σε πραγματικό χρόνο. Κοστίζει λίγο χρόνο GPU όσο κάποιο εφέ είναι ενεργό.</string> + <string name="drawer_look">Προεπιλογές</string> + <string name="drawer_look_custom">Προσαρμοσμένο</string> + <string name="drawer_effects_advanced">Για προχωρημένους</string> + <string name="widgets_expanded">Αναπτυγμένο</string> + <string name="widgets_collapsed">Συμπτυγμένο</string> + <string name="drawer_cas">Όξυνση (CAS)</string> + <string name="drawer_cas_level">Επίπεδο όξυνσης</string> + <string name="drawer_fake_hdr">Ψεύτικο HDR</string> + <string name="drawer_deband">Αφαίρεση λωρίδων</string> + <string name="drawer_deband_strength">Ένταση αφαίρεσης λωρίδων</string> + <string name="drawer_brightness">Φωτεινότητα</string> + <string name="drawer_contrast">Αντίθεση</string> + <string name="drawer_gamma">Γάμα</string> + <string name="drawer_saturation">Κορεσμός</string> + <string name="drawer_fxaa">FXAA (εξομάλυνση ακμών)</string> + <string name="drawer_toon">Περιγράμματα Toon</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Φιλτράρισμα υφών</string> + <string name="drawer_texture_hint">Παιχνίδια DirectX 9–11, από την επόμενη εκκίνησή τους.</string> + <string name="drawer_anisotropy">Ανισοτροπικό φιλτράρισμα</string> + <string name="drawer_texture_sharpness">Οξύτητα υφών</string> + <string name="drawer_texture_sharpness_note">Μια αρνητική απόκλιση mip: πιο έντονες υφές, κάποιο τρεμόπαιγμα. Το Αυτόματο ταιριάζει με το πόσο μεγεθύνεται η συνεδρία στην οθόνη.</string> + <string name="drawer_touch_auto">Αυτόματο (%1$s)</string> + <string name="drawer_keyboard">Πληκτρολόγιο</string> + <string name="drawer_pc_keyboard">Πληκτρολόγιο PC</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">Δεύτερη οθόνη</string> + <string name="drawer_second_screen_mode">Λειτουργία</string> + <string name="drawer_second_screen_shows">Εμφανίζει</string> + <string name="drawer_android_apps">Εφαρμογές Android</string> + <string name="drawer_launch_app">Εκκίνηση εφαρμογής</string> + <string name="common_hide">Απόκρυψη</string> + <string name="common_show">Εμφάνιση</string> + <string name="drawer_no_apps">Δεν υπάρχουν εφαρμογές για εκκίνηση</string> + <string name="drawer_session_behavior">Συμπεριφορά συνεδρίας</string> + <string name="drawer_suspend_hint">Εφαρμόζεται τώρα και σε επόμενες συνεδρίες σε αυτή τη λειτουργία.</string> + <string name="drawer_next_session">Επόμενη συνεδρία</string> + <string name="drawer_support">Υποστήριξη</string> + <string name="drawer_logs">Αρχεία καταγραφής συνεδρίας</string> + <string name="drawer_logs_hint">Επισυνάψτε σε αναφορά σφάλματος</string> + <string name="drawer_share_logs">Κοινή χρήση αρχείων καταγραφής</string> + <string name="drawer_background">Παρασκήνιο</string> + <string name="drawer_stop">Διακοπή</string> + <string name="drawer_stop_title">Διακοπή συνεδρίας;</string> + <string name="drawer_running_now">Εκτελείται τώρα</string> + <string name="drawer_no_game">Κανένα παιχνίδι δεν εκτελείται σε Proton</string> + <string name="drawer_selected_profile">Προφίλ παιχνιδιού</string> + <string name="drawer_selected_profile_none">Οι λεπτομέρειες του προφίλ δεν είναι διαθέσιμες ακόμα.</string> + <string name="drawer_profile_refresh_failed_initial">Δεν ήταν δυνατή η φόρτωση προφίλ παιχνιδιών. Νέα προσπάθεια αυτόματα.</string> + <string name="drawer_profile_refresh_failed_stale">Δεν ήταν δυνατή η ανανέωση προφίλ παιχνιδιών. Εμφανίζονται τα τελευταία διαθέσιμα δεδομένα· νέα προσπάθεια αυτόματα.</string> + <string name="drawer_profile_inspect_hint">Προβολή και επεξεργασία των αποθηκευμένων ρυθμίσεων που θα χρησιμοποιηθούν στην επόμενη εκκίνηση.</string> + <string name="drawer_profile_default">Χρήση Προεπιλεγμένου προφίλ</string> + <string name="drawer_profile_custom">Προσαρμοσμένο προφίλ</string> + <string name="drawer_component_inherited">Χρήση %1$s από το Προεπιλεγμένο προφίλ.</string> + <string name="drawer_game_components_note">Οι αλλαγές αποθηκεύονται στο προφίλ του %1$s και εφαρμόζονται την επόμενη φορά που θα εκκινήσει το παιχνίδι.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d παράκαμψη παιχνιδιού</item> + <item quantity="other">%1$d παρακάμψεις παιχνιδιού</item> + </plurals> + <string name="drawer_no_proton">Δεν έχει εγκατασταθεί Proton ακόμα.</string> + <string name="drawer_game_running">παιχνίδι σε εκτέλεση</string> + <string name="drawer_reapplied">επαναφέρθηκε στην εκκίνηση %1$s</string> + <string name="drawer_original">Πρωτότυπο</string> + <string name="drawer_components_note">Εισαγωγή και διαγραφή στη σελίδα Στοιχεία.</string> + <string name="setup_check_done">Έγινε</string> + <string name="setup_check_warn">Χρειάζεται προσοχή</string> + <string name="setup_check_busy">Σε εξέλιξη</string> + <string name="setup_limit_adb_failed">Ελέγξτε τη διεύθυνση IP και τη θύρα του Ασύρματου εντοπισμού σφαλμάτων, ή συζεύξτε ξανά αν το Android αφαίρεσε αυτή τη συσκευή.</string> + <string name="setup_install">Εγκατάσταση</string> + <string name="setup_manage">Διαχείριση</string> + <string name="setup_tab_overview">Επισκόπηση</string> + <string name="setup_tab_controller">Χειριστήριο</string> + <string name="setup_tab_session">Συνεδρία</string> + <string name="setup_tab_launcher">Εκκινητής</string> + <string name="setup_title">Ρύθμιση</string> + <string name="setup_all_set">● Όλα έτοιμα</string> + <string name="setup_n_ready">%1$d από %2$d έτοιμα</string> + <string name="setup_system_check">Έλεγχος συστήματος</string> + <string name="setup_gpu_supported">Η συσκευή υποστηρίζεται</string> + <string name="setup_gpu_untested">Μη δοκιμασμένη GPU</string> + <string name="setup_gpu_unsupported">Η GPU δεν υποστηρίζεται</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">Το Steam απαιτεί πρόγραμμα οδήγησης Adreno· το %1$s μπορεί να εμφανίσει μαύρη οθόνη</string> + <string name="setup_runtime">Runtime Linux</string> + <string name="setup_runtime_progress">%1$s · %2$d%%</string> + <string name="setup_runtime_missing">Δεν έχει εγκατασταθεί · περίπου 3 GB, εγκαθίσταται στο πρώτο Play</string> + <string name="setup_installed">Εγκατεστημένο</string> + <string name="setup_runtime_update">%1$s · διαθέσιμη ενημέρωση</string> + <string name="setup_runtime_current">%1$s · ενημερωμένο</string> + <string name="setup_limit">Όριο θυγατρικών διαδικασιών</string> + <string name="setup_limit_on">Ενεργό · Το Android μπορεί να κλείσει το Steam. Η απενεργοποίησή του διαρκεί ένα λεπτό.</string> + <string name="setup_limit_unset">Δεν έχει ορισθεί · Το Android μπορεί να κλείσει το Steam. Η απενεργοποίησή του διαρκεί ένα λεπτό.</string> + <string name="setup_limit_unknown">Δεν ήταν δυνατός ο έλεγχος · η απενεργοποίηση διαρκεί ένα λεπτό.</string> + <string name="setup_fix_it">Διόρθωση</string> + <string name="setup_details">Λεπτομέρειες</string> + <string name="setup_dev_options">Επιλογές προγραμματιστή</string> + <string name="setup_use_wireless">Χρήση Ασύρματου εντοπισμού σφαλμάτων</string> + <string name="setup_check_again">Έλεγχος ξανά</string> + <string name="setup_limit_turn_on">Ενεργοποίηση ορίου</string> + <string name="setup_account">Λογαριασμός Steam</string> + <string name="setup_signed_in_offline">Συνδεδεμένος ως %1$s · λειτουργία εκτός σύνδεσης</string> + <string name="setup_signed_in">Συνδεδεμένος ως %1$s</string> + <string name="setup_sign_in">Πατήστε Play και συνδεθείτε στο Steam</string> + <string name="setup_tools">Εργαλεία</string> + <string name="setup_controller">Χειριστήριο</string> + <string name="setup_controller_unavailable">Οι ρυθμίσεις χειριστηρίου δεν είναι διαθέσιμες.</string> + <string name="setup_session">Συνεδρία</string> + <string name="setup_logs">Αρχεία καταγραφής συνεδρίας</string> + <string name="setup_logs_hint">Αποθηκεύονται μετά από κάθε συνεδρία</string> + <string name="setup_agent_commands">Εργαλεία εντοπισμού σφαλμάτων</string> + <string name="setup_latest_logs">Πιο πρόσφατα αρχεία καταγραφής συνεδρίας</string> + <string name="setup_saved_logs">Αποθηκευμένα αρχεία καταγραφής συνεδρίας</string> + <string name="setup_saved_logs_hint">Διατηρεί τα %1$d πιο πρόσφατα</string> + <string name="setup_clear_logs">Εκκαθάριση αρχείων καταγραφής</string> + <string name="setup_offline">Λειτουργία εκτός σύνδεσης</string> + <string name="setup_sign_in_first">Συνδεθείτε πρώτα στο Steam</string> + <string name="setup_launcher">Εκκινητής</string> + <string name="setup_language">Γλώσσα</string> + <string name="setup_language_hint">Το χρησιμοποιεί και το Steam, από την επόμενη φορά που θα ξεκινήσει.</string> + <string name="setup_language_system">Προεπιλογή συστήματος (%1$s)</string> + <string name="setup_language_system_short">Προεπιλογή συστήματος</string> + <string name="setup_app_scale">Κλίμακα εφαρμογής</string> + <string name="setup_app_scale_hint">Κείμενο και χειριστήρια του DroidDeck</string> + <string name="setup_app_scale_default">%1$d%% (Προεπιλογή)</string> + <string name="setup_theme">Θέμα</string> + <string name="setup_theme_hint">Εμφάνιση εκκινητή</string> + <string name="setup_home">Χρήση ως αρχική οθόνη</string> + <string name="setup_home_on">Το DroidDeck μπορεί να είναι η εφαρμογή αρχικής οθόνης</string> + <string name="setup_home_off">Το DroidDeck δεν προτείνεται ποτέ ως εφαρμογή αρχικής οθόνης</string> + <string name="setup_orientation">Προσανατολισμός</string> + <string name="setup_orientation_auto">Αυτόματος οριζόντιος προσανατολισμός</string> + <string name="setup_orientation_landscape">Οριζόντιος προσανατολισμός</string> + <string name="setup_orientation_reverse">Αντίστροφος οριζόντιος προσανατολισμός</string> + <string name="setup_fullscreen">Πλήρης οθόνη</string> + <string name="setup_fullscreen_on">Αποκρύπτει τις γραμμές κατάστασης και πλοήγησης του Android</string> + <string name="setup_fullscreen_off">Εμφανίζει τις γραμμές κατάστασης και πλοήγησης του Android</string> + <string name="setup_default_home">Προεπιλεγμένη εφαρμογή αρχικής οθόνης</string> + <string name="setup_choose_home">Επιλογή εφαρμογής αρχικής οθόνης</string> + <string name="setup_choose">Επιλογή</string> + <string name="setup_linux_apps">Εφαρμογές Linux (beta)</string> + <string name="setup_tool_files">Αρχεία</string> + <string name="setup_tool_files_hint">Περιήγηση και διαχείριση αρχείων</string> + <string name="setup_tool_protons">Εκδόσεις Proton</string> + <string name="setup_tool_protons_hint">Εγκατάσταση builds Proton για ARM64</string> + <string name="setup_tool_performance">Απόδοση</string> + <string name="setup_tool_performance_hint">Ανάθεση πυρήνων CPU</string> + <string name="setup_tool_roms">Φάκελος ROM</string> + <string name="setup_tool_roms_hint">Πού αποθηκεύονται τα παιχνίδια προσομοιωτών</string> + <string name="setup_card_components">Στοιχεία</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Χειριστήρια</string> + <string name="setup_card_controls_hint">Αντιστοίχιση κουμπιών</string> + <string name="store_tab_discover">Ανακάλυψη</string> + <string name="store_tab_search">Αναζήτηση</string> + <string name="store_tab_installed">Εγκατεστημένα</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Κατάστημα</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Το Flatpak είναι έτοιμο</string> + <string name="store_needs_runtime">Εγκαταστήστε πρώτα το runtime Linux από τη Ρύθμιση.</string> + <string name="store_setup_title">Ρύθμιση του Flatpak</string> + <string name="store_setup_text">Προσθέτει το Flatpak και το συνδέει με το Flathub (περίπου 20 MB). Η πρώτη εφαρμογή κάνει επίσης λήψη του runtime της.</string> + <string name="store_setting_up">Ρύθμιση…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Ενημερώσεις</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d%%</string> + <string name="store_starting">Εκκίνηση…</string> + <string name="store_categories">Κατηγορίες</string> + <string name="store_unreachable">Δεν ήταν δυνατή η πρόσβαση στο Flathub. Ελέγξτε τη σύνδεσή σας και δοκιμάστε ξανά.</string> + <string name="store_try_again">Δοκιμάστε ξανά</string> + <string name="store_search_label">Αναζήτηση στο Flathub</string> + <string name="store_search_placeholder">Firefox, παιχνίδια, προσομοιωτές…</string> + <string name="store_search">Αναζήτηση</string> + <string name="store_all">Όλα</string> + <string name="store_searching">Αναζήτηση…</string> + <string name="store_search_hint">Αναζητήστε με όνομα ή επιλέξτε κατηγορία. Αναφέρονται μόνο εφαρμογές ARM64.</string> + <string name="store_no_results">Καμία εφαρμογή ARM64 στο Flathub δεν ταιριάζει με αυτό.</string> + <string name="store_results">Αποτελέσματα</string> + <string name="store_check_updates">Έλεγχος για ενημερώσεις</string> + <string name="store_update_all">Ενημέρωση όλων (%1$d)</string> + <string name="store_all_current">● Όλα είναι ενημερωμένα</string> + <string name="store_no_apps">Δεν υπάρχουν εφαρμογές ακόμα. Βρείτε κάποιες στην Ανακάλυψη ή την Αναζήτηση.</string> + <string name="store_setup_first_note">Ρυθμίστε το Flatpak για να εγκαταστήσετε εφαρμογές.</string> + <string name="store_update_available_prefix">Διαθέσιμη ενημέρωση · %1$s</string> + <string name="store_open">Άνοιγμα</string> + <string name="store_details">Λεπτομέρειες</string> + <string name="store_remove">Αφαίρεση</string> + <string name="store_press_again">Πατήστε ξανά για αφαίρεση</string> + <string name="store_installing">Εγκατάσταση…</string> + <string name="store_install">Εγκατάσταση</string> + <string name="store_setup_first">Ρυθμίστε πρώτα το Flatpak</string> + <string name="store_download_size">Λήψη %1$s</string> + <string name="store_installed_chip">● Εγκατεστημένο</string> + <string name="store_details_failed">Δεν ήταν δυνατή η φόρτωση της σελίδας Flathub αυτής της εφαρμογής.</string> + <string name="store_loading">Φόρτωση από το Flathub…</string> + <string name="store_no_arm">Δεν υπάρχει build ARM64 στο Flathub, οπότε δεν μπορεί να εγκατασταθεί εδώ.</string> + <string name="store_screenshots">Στιγμιότυπα οθόνης</string> + <string name="store_about">Σχετικά</string> + <string name="store_app_id">ID εφαρμογής</string> + <string name="store_license">Άδεια χρήσης</string> + <string name="store_runtime">Runtime</string> + <string name="store_installed_size">Μέγεθος εγκατάστασης</string> + <string name="store_installed_size_value">%1$s (συν το κοινό runtime του)</string> + <string name="store_website">Ιστότοπος</string> + <string name="store_verified">Επιβεβαιωμένο</string> + <string name="upd_title">Ενημερώσεις</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Ελέγχθηκε %1$s</string> + <string name="upd_not_checked">Δεν έχει ελεγχθεί ακόμα</string> + <string name="upd_perm_title">Επιτρέψτε στο DroidDeck να ενημερωθεί μόνο του</string> + <string name="upd_perm_text">Στην επόμενη οθόνη, ενεργοποιήστε την Αποδοχή από αυτή την πηγή και επιστρέψτε εδώ. Η ενημέρωση συνεχίζεται μόνη της.</string> + <string name="upd_open_settings">Άνοιγμα ρυθμίσεων</string> + <string name="upd_not_now">Όχι τώρα</string> + <string name="upd_signed_status">Υπογεγραμμένο διαφορετικά</string> + <string name="upd_signed_headline">Δεν γίνεται ενημέρωση επί τόπου</string> + <string name="upd_signed_detail">Αυτή η έκδοση δεν είναι υπογεγραμμένη με το κλειδί κυκλοφορίας του DroidDeck, οπότε οι ενημερώσεις δεν μπορούν να εγκατασταθούν πάνω της. Απεγκαταστήστε την για να αλλάξετε.</string> + <string name="upd_available">Υπάρχει ενημέρωση</string> + <string name="upd_blocked_status">Δεν γίνεται εγκατάσταση</string> + <string name="upd_switch">Έτοιμο για αλλαγή</string> + <string name="upd_ahead_status">Μπροστά από το Stable</string> + <string name="upd_ahead_headline">Είστε μπροστά από το Stable</string> + <string name="upd_ahead_detail">Αυτό το build είναι νεότερο από το τελευταίο Stable. Θα μεταβείτε στο Stable με την επόμενη κυκλοφορία του.</string> + <string name="upd_gone_status">Η δοκιμή τελείωσε</string> + <string name="upd_gone_headline">Αυτή η δοκιμή έχει τελειώσει</string> + <string name="upd_gone_detail">Η διόρθωσή της συγχωνεύτηκε ή εγκαταλείφθηκε. Ακολουθήστε το Preview για τις νεότερες διορθώσεις.</string> + <string name="upd_current_status">Ενημερωμένο</string> + <string name="upd_current_headline">Έχετε την πιο πρόσφατη έκδοση %1$s</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Ενημέρωση%1$s</string> + <string name="upd_install_button">Εγκατάσταση%1$s</string> + <string name="upd_install_anyway">Εγκατάσταση Stable %1$s παρόλα αυτά</string> + <string name="upd_follow_preview">Ακολούθηση Preview</string> + <string name="upd_stop_session">Διακόψτε την τρέχουσα συνεδρία για ενημέρωση.</string> + <string name="upd_less">Λιγότερα</string> + <string name="upd_more">Περισσότερα</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Νέο build Preview</string> + <string name="upd_new_test">Δοκιμαστικό build PR #%1$d</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">ΚΑΝΑΛΙ ΕΝΗΜΕΡΩΣΗΣ</string> + <string name="upd_stable">Stable</string> + <string name="upd_stable_hint">Δοκιμασμένες εκδόσεις, για τους περισσότερους</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Preview</string> + <string name="upd_preview_hint">Νεότερες διορθώσεις, πριν το Stable</string> + <string name="upd_preview_current">Ενημερωμένο</string> + <string name="upd_tests">Δοκιμαστικά builds</string> + <string name="upd_tests_hint">Δοκιμάστε μια διόρθωση πριν κυκλοφορήσει</string> + <string name="upd_tests_none">Κανένα προς το παρόν</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Δεν έχει κατασκευαστεί για αυτή την έκδοση του DroidDeck</string> + <string name="upd_updated">Ενημερώθηκε %1$s</string> + <string name="upd_test_name">η δοκιμή PR #%1$d</string> + <string name="upd_while_ago">πριν από λίγο</string> + <string name="upd_just_now">μόλις τώρα</string> + <plurals name="upd_tests_count"> + <item quantity="one">%1$d προς δοκιμή</item> + <item quantity="other">%1$d προς δοκιμή</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="one">Αλλαγές από το build σας · %1$d build πίσω</item> + <item quantity="other">Αλλαγές από το build σας · %1$d builds πίσω</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="one">Πρόσφατες αλλαγές · %1$d build</item> + <item quantity="other">Πρόσφατες αλλαγές · %1$d builds</item> + </plurals> + <string name="gate_title">Μία ρύθμιση Android για αλλαγή</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Το Android διακόπτει εφαρμογές που ξεκινούν πολλές διαδικασίες παρασκηνίου, και το Steam ξεκινά δεκάδες. Μέχρι να αλλάξει αυτό, τα παιχνίδια κλείνουν χωρίς σφάλμα.</string> + <string name="gate_fix_for_me">Διόρθωσέ το για εμένα</string> + <string name="gate_toggle_steps">Ενεργοποιήστε «Απενεργοποίηση περιορισμών θυγατρικών διαδικασιών» και επιστρέψτε εδώ. Αφήστε τις Επιλογές προγραμματιστή ενεργές, διαφορετικά ο διακόπτης απενεργοποιείται ξανά.</string> + <string name="gate_open_dev">Άνοιγμα Επιλογών προγραμματιστή</string> + <string name="gate_no_toggle">Αυτή η έκδοση Android δεν έχει διακόπτη για αυτό, οπότε το DroidDeck το αλλάζει μέσω Ασύρματου εντοπισμού σφαλμάτων. Διαρκεί περίπου ένα λεπτό, χωρίς υπολογιστή.</string> + <string name="gate_needs_wifi">Ο Ασύρματος εντοπισμός σφαλμάτων απαιτεί Wi-Fi. Οποιοδήποτε δίκτυο λειτουργεί· δεν απαιτείται σύνδεση στο διαδίκτυο.</string> + <string name="gate_needs_notifications">Ο κωδικός σύζευξης εμφανίζεται σε ειδοποίηση. Επιτρέψτε ειδοποιήσεις για το DroidDeck, ή χρησιμοποιήστε Άλλοι τρόποι.</string> + <string name="gate_allow_notifications">Αποδοχή ειδοποιήσεων</string> + <string name="gate_step1">Ανοίξτε Ρυθμίσεις → Επιλογές προγραμματιστή → Ασύρματος εντοπισμός σφαλμάτων και ενεργοποιήστε τον</string> + <string name="gate_step2">Πατήστε «Σύζευξη συσκευής με κωδικό σύζευξης»</string> + <string name="gate_step3">Πληκτρολογήστε τον κωδικό στην ειδοποίηση του DroidDeck</string> + <string name="gate_waiting">Αναμονή για το αναδυόμενο παράθυρο σύζευξης…</string> + <string name="gate_code_needed">Βρέθηκε αναδυόμενο παράθυρο. Εισαγάγετε τον κωδικό του στην ειδοποίηση.</string> + <string name="gate_status">Κατάσταση</string> + <string name="gate_polling">Έλεγχος κάθε 2 δευτερόλεπτα. Αυτή η σελίδα κλείνει όταν είναι απενεργοποιημένο.</string> + <string name="gate_other_ways">Άλλοι τρόποι</string> + <string name="gate_other_toggle">Ασύρματος εντοπισμός σφαλμάτων, μη αυτόματη διεύθυνση, ADB υπολογιστή</string> + <string name="gate_other_no_toggle">Μη αυτόματη διεύθυνση, ADB υπολογιστή, Επιλογές προγραμματιστή</string> + <string name="gate_use_wireless_instead">Χρήση Ασύρματου εντοπισμού σφαλμάτων αντ\' αυτού</string> + <string name="gate_some_roms">Ορισμένες ROM προσθέτουν τον διακόπτη ούτως ή άλλως. Αναζητήστε «θυγατρική διαδικασία» στις Επιλογές προγραμματιστή.</string> + <string name="gate_manual_hint">Αν η ειδοποίηση δεν λειτουργεί, εισαγάγετε τη διεύθυνση και τον κωδικό του αναδυόμενου παραθύρου εσείς.</string> + <string name="gate_enter_manually">Μη αυτόματη εισαγωγή διεύθυνσης</string> + <string name="gate_from_computer">Από υπολογιστή με ADB:</string> + <string name="gate_copy_command">Αντιγραφή εντολής</string> + <string name="gate_choose_display">Επιλέξτε σε ποια οθόνη θα ανοίξουν οι Ρυθμίσεις Android.</string> + <string name="gate_main_screen">Κύρια οθόνη</string> + <string name="wadb_finding_port">Έγινε σύζευξη. Αναζήτηση θύρας Ασύρματου εντοπισμού σφαλμάτων…</string> + <string name="wadb_enter_port">Έγινε σύζευξη. Εισαγάγετε τη θύρα από τις Ρυθμίσεις.</string> + <string name="wadb_applying">Η συσκευή βρέθηκε. Εφαρμογή της ρύθμισης…</string> + <string name="wadb_apply_failed">Δεν ήταν δυνατή η αυτόματη εφαρμογή. Ελέγξτε την παρακάτω διεύθυνση και δοκιμάστε ξανά. %1$s</string> + <string name="wadb_title_wireless">Ασύρματος εντοπισμός σφαλμάτων</string> + <string name="wadb_title_done">Το όριο θυγατρικών διαδικασιών ενημερώθηκε</string> + <string name="wadb_title">Αλλαγή του ορίου θυγατρικών διαδικασιών</string> + <string name="wadb_lede_compact0">Αν η επιλογή λείπει, κάντε σύζευξη μέσω Ασύρματου εντοπισμού σφαλμάτων.</string> + <string name="wadb_lede_compact1">Έγινε σύζευξη. Ελέγξτε τη διεύθυνση IP και τη θύρα στον Ασύρματο εντοπισμό σφαλμάτων.</string> + <string name="wadb_lede_done">Το όριο θυγατρικών διαδικασιών ενημερώθηκε.</string> + <string name="wadb_lede0">Χρησιμοποιήστε το βήμα 2 αν οι Επιλογές προγραμματιστή δεν έχουν ρύθμιση θυγατρικών διαδικασιών. Ανοίξτε Επιλογές προγραμματιστή → Ασύρματος εντοπισμός σφαλμάτων → Σύζευξη συσκευής με κωδικό σύζευξης.</string> + <string name="wadb_lede1">Έγινε σύζευξη. Χρησιμοποιήστε την παρακάτω διεύθυνση, ή ενημερώστε την από Ασύρματος εντοπισμός σφαλμάτων → Διεύθυνση IP & θύρα.</string> + <string name="wadb_lede_done_long">Το όριο θυγατρικών διαδικασιών ενημερώθηκε μέσω Ασύρματου εντοπισμού σφαλμάτων.</string> + <string name="wadb_confirmed_on">Το Android επιβεβαίωσε ότι το όριο είναι ενεργό.</string> + <string name="wadb_confirmed_off">Το Android επιβεβαίωσε ότι το όριο είναι ανενεργό.</string> + <string name="wadb_pair_group">2 · Σύζευξη Ασύρματου εντοπισμού σφαλμάτων</string> + <string name="wadb_pair_address">Διεύθυνση IP & θύρα αναδυόμενου παραθύρου σύζευξης</string> + <string name="wadb_pin">Κωδικός σύζευξης (PIN)</string> + <string name="wadb_pin_placeholder">6 ψηφία</string> + <string name="wadb_pair">Σύζευξη</string> + <string name="wadb_apply_group">2 · Εφαρμογή μέσω Ασύρματου εντοπισμού σφαλμάτων</string> + <string name="wadb_connect_address">Διεύθυνση IP & θύρα σύνδεσης</string> + <string name="wadb_pair_again">Σύζευξη ξανά</string> + <string name="wadb_turn_off">Απενεργοποίηση ορίου</string> + <string name="wadb_other_ways">Άλλοι τρόποι αλλαγής</string> + <string name="wadb_dev_try_first">1 · Επιλογές προγραμματιστή (δοκιμάστε πρώτα)</string> + <string name="wadb_restrict_on">Ενεργοποιήστε «Περιορισμός θυγατρικών διαδικασιών».</string> + <string name="wadb_restrict_off">Απενεργοποιήστε «Περιορισμός θυγατρικών διαδικασιών».</string> + <string name="wadb_computer_last">3 · ADB υπολογιστή (τελευταία λύση)</string> + <string name="wadb_only_if">Μόνο αν ο Ασύρματος εντοπισμός σφαλμάτων δεν είναι διαθέσιμος.</string> + <string name="wadb_computer_group">3 · ADB υπολογιστή (τελευταία λύση)</string> + <string name="ctrl_osc_hint">Όταν εμφανίζονται τα χειριστήρια αφής σε μια συνεδρία</string> + <string name="ctrl_color">Χρώμα</string> + <string name="ctrl_color_hint">Απόχρωση των κουμπιών οθόνης</string> + <string name="ctrl_custom">Προσαρμοσμένο</string> + <string name="ctrl_opacity">Διαφάνεια</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">Μέγεθος κουμπιών</string> + <string name="ctrl_size_hint" formatted="false">Το 100% είναι το κανονικό μέγεθος</string> + <string name="ctrl_stick_click">Κλικ μοχλού</string> + <string name="ctrl_stick_click_hint">Πατήστε δύο φορές και κρατήστε έναν μοχλό για L3 ή R3</string> + <string name="ctrl_adaptive">Προσαρμοστικοί μοχλοί</string> + <string name="ctrl_rumble">Κραδασμός παιχνιδιού</string> + <string name="ctrl_steam_button">Κουμπί Steam</string> + <string name="ctrl_qam_button">Κουμπί QAM</string> + <string name="ctrl_keyboard_button">Κουμπί πληκτρολογίου</string> + <string name="ctrl_select_steam">Πατήστε Select για να ανοίξετε το μενού Steam</string> + <string name="ctrl_select_steam_hint">Αντί για View/Select.</string> + <string name="setup_animations">Κινήσεις</string> + <string name="ctrl_adaptive_hint">Οι μοχλοί εμφανίζονται όπου αγγίζετε και εξαφανίζονται όταν αφήνετε</string> + <string name="ctrl_layout">Διάταξη</string> + <string name="ctrl_layout_custom">Αποθηκεύτηκαν προσαρμοσμένες θέσεις</string> + <string name="ctrl_layout_default">Προσαρμοσμένο σε αυτή την οθόνη</string> + <string name="ctrl_edit">Επεξεργασία</string> + <string name="ctrl_reset">Επαναφορά</string> + <string name="ctrl_mapping">Αντιστοίχιση κουμπιών</string> + <string name="ctrl_mapping_default">Κάθε κουμπί στέλνει τη δική του είσοδο</string> + <string name="ctrl_mapping_count">%1$d από %2$d κουμπιά επαναντιστοιχίστηκαν</string> + <string name="ctrl_configure">Διαμόρφωση</string> + <string name="ctrl_reset_all">Επαναφορά χειριστηρίου</string> + <string name="ctrl_reset_all_hint">Επαναφέρει όλες τις ρυθμίσεις χειριστηρίου και τη διάταξη</string> + <string name="ctrl_mapping_lede">Τι στέλνει κάθε κουμπί οθόνης στο παιχνίδι. Το Κρυμμένο αφαιρεί το κουμπί.</string> + <string name="ctrl_onscreen_buttons">Κουμπιά οθόνης</string> + <string name="ctrl_defaults">Προεπιλογές</string> + <string name="ctrl_reset_mapping">Επαναφορά αντιστοίχισης</string> + <string name="content_android_apps">Εφαρμογές Android</string> + <string name="content_apps_count">%1$d εφαρμογές</string> + <string name="content_no_android_apps">Δεν βρέθηκαν εφαρμογές Android για εκκίνηση.</string> + <string name="content_desktop_installed">● Επιφάνεια εργασίας εγκατεστημένη</string> + <string name="content_desktop_first_open">Εγκαθίσταται στο πρώτο άνοιγμα</string> + <string name="content_available">Διαθέσιμο για εγκατάσταση</string> + <string name="content_not_installed">Δεν έχει εγκατασταθεί.</string> + <string name="content_not_installed_suffix">%1$s · δεν έχει εγκατασταθεί</string> + <string name="content_open_named">Άνοιγμα %1$s</string> + <string name="content_games">Παιχνίδια</string> + <string name="content_choose_roms">Επιλέξτε έναν φάκελο ROM.</string> + <string name="content_retroarch_roms">Μεταβείτε στο /root/ROMs στο RetroArch.</string> + <string name="content_add_roms">Προσθέστε παιχνίδια %1$s στο ROMs/%2$s.</string> + <string name="content_install_named">Εγκατάσταση %1$s</string> + <string name="content_stop_to_install">Διακοπή συνεδρίας για εγκατάσταση</string> + <string name="content_runtime_required">Απαιτείται runtime</string> + <string name="content_loading_details">Φόρτωση λεπτομερειών εγκατάστασης…</string> + <string name="content_details_unavailable">Οι λεπτομέρειες εγκατάστασης δεν είναι διαθέσιμες. Δοκιμάστε ξανά αργότερα.</string> + <string name="content_needs_runtime">Εγκαταστήστε πρώτα το runtime Linux από τη Ρύθμιση.</string> + <string name="content_stop_first">Διακόψτε την τρέχουσα συνεδρία για να εγκαταστήσετε εφαρμογές επιφάνειας εργασίας.</string> + <string name="content_install_into">Εγκαθιστά το %1$s στην επιφάνεια εργασίας Linux.</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">Η απόκρυψη το αφαιρεί από την Επιφάνεια εργασίας. Τα αρχεία του παραμένουν στο runtime Linux.</string> + <string name="content_rom_gone">Αυτό το παιχνίδι δεν βρίσκεται πλέον στον φάκελο ROM.</string> + <string name="content_desktop_system">Επιφάνεια εργασίας · %1$s</string> + <string name="content_launch_in">Εκκίνηση σε %1$s</string> + <string name="content_also_in">Επίσης σε %1$s</string> + <string name="content_page_gone">Αυτή η σελίδα δεν υπάρχει πια. Επιλέξτε μια ενότητα αριστερά.</string> + <string name="content_open_desktop">Άνοιγμα επιφάνειας εργασίας</string> + <string name="content_install_open_desktop">Εγκατάσταση & άνοιγμα επιφάνειας εργασίας</string> + <string name="desktop_remove_title">Απεγκατάσταση επιφάνειας εργασίας</string> + <string name="desktop_remove_text">Αφαιρεί την επιφάνεια εργασίας και τις ενσωματωμένες εφαρμογές της. Διατηρεί παιχνίδια, προστιθέμενες εφαρμογές, ρυθμίσεις και κοινές βιβλιοθήκες.</string> + <string name="desktop_removing">Αφαίρεση επιφάνειας εργασίας</string> + <string name="desktop_removed">Η επιφάνεια εργασίας αφαιρέθηκε</string> + <string name="desktop_cleaning_legacy">Αφαίρεση παλιών αρχείων LXQt</string> + <string name="desktop_removal_unknown">Ενημερώστε το DroidDeck πριν αφαιρέσετε αυτή την έκδοση επιφάνειας εργασίας.</string> + <string name="content_linux_desktop">Επιφάνεια εργασίας Linux</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox και οι προσομοιωτές σας</string> + <string name="games_empty">Τα παιχνίδια που εγκαθιστάτε στο Steam εμφανίζονται εδώ μετά τη συνεδρία.</string> + <string name="games_play_steam">Αναπαραγωγή Steam</string> + <string name="games_launch_settings">Ρυθμίσεις εκκίνησης</string> + <string name="games_launch">Εκκίνηση</string> + <string name="games_files">Αρχεία παιχνιδιού</string> + <string name="games_prefix">Prefix Proton</string> + <string name="games_saves_looking">Αναζήτηση αποθηκευμένων αρχείων…</string> + <string name="games_saves_none">Δεν βρέθηκε φάκελος αποθήκευσης με όνομα. Η εξαγωγή λαμβάνει ολόκληρο τον φάκελο χρήστη.</string> + <string name="games_saves_folders">%1$d φάκελοι αποθήκευσης</string> + <string name="games_saves_summary">%1$s · %2$d αρχεία · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">Διαχείριση αποθηκευμένων</string> + <string name="games_saves">Αποθηκευμένα παιχνιδιού</string> + <string name="games_import_saves">Εισαγωγή αποθηκευμένων…</string> + <string name="games_export_gamehub">Εξαγωγή ως zip GameHub…</string> + <string name="games_export_winlator">Εξαγωγή ως zip Winlator…</string> + <string name="games_open_save_folder">Άνοιγμα φακέλου αποθήκευσης</string> + <string name="games_never_played">Δεν έχει παιχτεί ποτέ</string> + <string name="games_runtime_busy_percent">Το runtime είναι απασχολημένο · %1$d%%</string> + <string name="games_runtime_busy">Το runtime είναι απασχολημένο</string> + <string name="games_runtime_first_play">Το runtime εγκαθίσταται στο πρώτο Play</string> + <string name="games_runtime_update">Υπάρχει ενημέρωση runtime</string> + <string name="games_runtime_ready">● Το runtime είναι έτοιμο</string> + <string name="games_last_played">Παίχτηκε τελευταία φορά %1$s</string> + <string name="games_internal">Εσωτερική αποθήκευση</string> + <string name="games_added">Προστέθηκε παιχνίδι</string> + <string name="games_custom">Προσαρμοσμένο παιχνίδι</string> + <string name="games_store_storage">Αποθήκευση παιχνιδιών</string> + <string name="gpu_auto_hint">Το καλύτερο ζεύγος για αυτή τη GPU, ενημερωμένο</string> + <string name="gpu_manual_hint">Επιλέξτε ένα ζεύγος εσείς</string> + <string name="gpu_checking">Έλεγχος για προγράμματα οδήγησης…</string> + <string name="gpu_none_listed">Δεν υπάρχουν ακόμα προγράμματα οδήγησης. Ανανεώστε για να τα αναζητήσετε.</string> + <string name="gpu_hide_other">Απόκρυψη προγραμμάτων οδήγησης για άλλες GPU</string> + <string name="gpu_show_other">Εμφάνιση προγραμμάτων οδήγησης για άλλες GPU</string> + <string name="gpu_advanced">Για προχωρημένους</string> + <string name="gpu_set_with_display">Ορίζεται μαζί με το πρόγραμμα οδήγησης οθόνης: το %1$s φέρει και τα δύο.</string> + <string name="gpu_set_with_runtime">Ορίζεται μαζί με το πρόγραμμα οδήγησης runtime: το %1$s φέρει και τα δύο.</string> + <string name="gpu_runtime_hint">Χρησιμοποιείται από παιχνίδια και την επιφάνεια εργασίας.</string> + <string name="gpu_runtime_default">Προεπιλογή runtime</string> + <string name="gpu_display_hint">Σχεδιάζει καρέ στην οθόνη. Εφαρμόζεται μετά την επανεκκίνηση του DroidDeck.</string> + <string name="gpu_auto_by_gpu">Αυτόματο - επιλέγεται από τη GPU</string> + <string name="gpu_manage">Λήψη, εισαγωγή ή διαγραφή…</string> + <string name="gpu_in_use_caps">ΣΕ ΧΡΗΣΗ</string> + <string name="gpu_builtin">Τα προγράμματα οδήγησης ενσωματωμένα στο DroidDeck</string> + <string name="gpu_matched_pair">Τα προγράμματα οδήγησης runtime και οθόνης ορίζονται μαζί ως αντιστοιχισμένο ζεύγος.</string> + <string name="gpu_recommended_for">Προτείνεται για αυτή τη GPU: %1$s.</string> + <string name="gpu_refresh_hint">Ανανεώστε για να αναζητήσετε προγράμματα οδήγησης για αυτή τη GPU.</string> + <string name="gpu_recommended_caps">ΠΡΟΤΕΙΝΕΤΑΙ</string> + <string name="gpu_unavailable">Μη διαθέσιμο</string> + <string name="load_runtime">Runtime Linux</string> + <string name="load_runtime_detail">Προετοιμασία του runtime Linux</string> + <string name="load_session">Συνεδρία</string> + <string name="load_session_detail">Εκκίνηση οθόνης και ήχου</string> + <string name="load_steam">Steam client</string> + <string name="load_steam_detail">Έλεγχος του Steam και των εργαλείων συμβατότητάς του</string> + <string name="load_opening_steam">Άνοιγμα Steam</string> + <string name="load_opening_steam_detail">Αναμονή για το πρώτο καρέ του Steam</string> + <string name="load_desktop">Επιφάνεια εργασίας</string> + <string name="load_desktop_detail">Προετοιμασία της επιφάνειας εργασίας</string> + <string name="load_opening">Άνοιγμα</string> + <string name="load_opening_detail">Αναμονή για το πρώτο καρέ</string> + <string name="load_starting_steam">Εκκίνηση του Steam</string> + <string name="load_ended">Η συνεδρία έληξε</string> + <string name="load_step">Βήμα %1$d από %2$d</string> + <string name="load_steam_paused">Το Steam είναι σε παύση</string> + <string name="deck_tab_grips">Λαβές</string> + <string name="deck_tab_left">Αριστερή επιφάνεια</string> + <string name="deck_tab_right">Δεξιά επιφάνεια</string> + <string name="deck_tab_both">Και τα δύο</string> + <string name="deck_tab_gamepad">Χειριστήριο</string> + <string name="deck_surface_stick">Μοχλός</string> + <string name="deck_surface_trackpad">Πινακίδα αφής</string> + <string name="deck_surface_switch">Εναλλαγή μεταξύ μοχλού και πινακίδας αφής</string> + <string name="deck_more_controls">Περισσότερα χειριστήρια Deck</string> + <string name="deck_panels">ΠΙΝΑΚΕΣ</string> + <string name="deck_close">Κλείσιμο χειριστηρίων δεύτερης οθόνης</string> + <string name="deck_open_steam_menu">Άνοιγμα μενού Steam</string> + <string name="deck_left_upper">Αριστερά · πάνω</string> + <string name="deck_left_lower">Αριστερά · κάτω</string> + <string name="deck_right_upper">Δεξιά · πάνω</string> + <string name="deck_right_lower">Δεξιά · κάτω</string> + <string name="deck_click">Κλικ</string> + <string name="deck_right_trackpad">Δεξιά πινακίδα αφής</string> + <string name="deck_left_trackpad">Αριστερή πινακίδα αφής</string> + <string name="second_trackpad">Πινακίδα αφής</string> + <string name="second_show_keyboard">Εμφάνιση πληκτρολογίου</string> + <string name="second_hide_keyboard">Απόκρυψη πληκτρολογίου</string> + <string name="second_terminal">Τερματικό Linux</string> + <string name="second_terminal_not_ready">Η συνεδρία Linux δεν είναι ακόμα έτοιμη για τερματικό.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Παιχνίδια</string> + <string name="rail_desktop">Επιφάνεια</string> + <string name="rail_store">Κατάστημα</string> + <string name="rail_components">Στοιχεία</string> + <string name="rail_apps">Εφαρμογές</string> + <string name="rail_setup">Ρύθμιση</string> + <string name="rail_updates">Ενημερώσεις</string> + <string name="rail_percent">%1$d%%</string> + <string name="rail_resume_named">Συνέχιση %1$s</string> + <string name="roms_text">Εμφανίζεται ως /root/ROMs σε κάθε συνεδρία. Ο εσωτερικός χώρος αποθήκευσης βρίσκεται στο /root/Storage.</string> + <string name="roms_none">Δεν έχει επιλεγεί φάκελος</string> + <string name="roms_applies">Ισχύει από την επόμενη συνεδρία. Οι κάρτες SD λειτουργούν.</string> + <string name="roms_choose">Επιλογή φακέλου</string> + <string name="roms_change">Αλλαγή φακέλου</string> + <string name="proton_lede">Εγκαταστήστε μια έκδοση Proton για ARM64, και επιλέξτε την ανά παιχνίδι από το Steam → Ιδιότητες → Συμβατότητα.</string> + <string name="proton_stop_first">Διακόψτε την τρέχουσα συνεδρία για να εγκαταστήσετε ή να καταργήσετε ένα εργαλείο συμβατότητας.</string> + <string name="proton_available">Διαθέσιμες εκδόσεις</string> + <string name="proton_installed">Εγκατεστημένο %1$s</string> + <string name="proton_queued">Σε εκκρεμότητα από προηγούμενο αίτημα</string> + <string name="proton_not_installed">Δεν είναι εγκατεστημένο</string> + <string name="proton_update_available">Διαθέσιμη ενημέρωση → %1$s</string> + <string name="proton_remove_older">Κατάργηση παλιών εκδόσεων %1$s</string> + <string name="proton_remove_older_body">Καταργεί %1$s. Τα παιχνίδια που έχουν ρυθμιστεί σε αυτές τις εκδόσεις πρέπει να αλλάξουν σε άλλο Proton.</string> + <string name="proton_remove_body">Τα παιχνίδια που έχουν ρυθμιστεί σε αυτή την έκδοση πρέπει να αλλάξουν σε άλλο Proton.</string> + <string name="proton_install_now">Εγκατάσταση τώρα</string> + <string name="proton_cancel_queued">Ακύρωση σε εκκρεμότητα</string> + <string name="pckb_android">Πληκτρολόγιο Android</string> + <string name="pckb_hide">Απόκρυψη ✕</string> + <string name="main_launch_named">Εκκίνηση %1$s</string> + <string name="main_choose_screen">Επιλέξτε οθόνη. Δευτερεύουσα: %1$s.</string> + <string name="main_secondary_gone">Η δευτερεύουσα οθόνη δεν είναι πλέον διαθέσιμη.</string> + <string name="main_primary">Κύρια οθόνη</string> + <string name="main_secondary">Δευτερεύουσα οθόνη</string> + <string name="widgets_prev_tab">Προηγούμενη καρτέλα</string> + <string name="widgets_next_tab">Επόμενη καρτέλα</string> + <string name="widgets_on">Ενεργό</string> + <string name="widgets_all_n">Όλα %1$d</string> + <string name="widgets_none">Κανένα</string> + <string name="widgets_n_of_m">%1$d από %2$d</string> + <string name="widgets_off">Ανενεργό</string> + <string name="screen_bottom">Κάτω οθόνη</string> + <string name="game_env_applies">Ισχύει από την επόμενη εκκίνηση παιχνιδιού. Το προφίλ ενός παιχνιδιού υπερισχύει του κοινού.</string> + <string name="game_env_other">Άλλο παιχνίδι με αναγνωριστικό εφαρμογής…</string> + <string name="game_env_done">Τέλος</string> + <string name="game_env_default">Προεπιλογή</string> + <string name="game_env_off">Ανενεργό</string> + <string name="game_env_change">Αλλαγή…</string> + <string name="game_env_change_title">Αλλαγή μεταβλητής</string> + <string name="game_env_restore_default">Επαναφορά στην προεπιλογή</string> + <string name="game_env_eyebrow">Παιχνίδια Proton</string> + <string name="pip_title">Εικόνα εντός εικόνας</string> + <string name="pip_auto">Αυτόματη είσοδος σε λειτουργία εικόνας εντός εικόνας</string> + <string name="unfolded_controls">Χειριστήρια στην ξεδιπλωμένη οθόνη</string> + <string name="unfolded_controls_hint">Ξεδιπλωμένη, το παιχνίδι βρίσκεται πάνω από τις πινακίδες αφής και τις πίσω λαβές του Steam Deck. Διπλωμένη, το παιχνίδι καταλαμβάνει όλη την οθόνη.</string> + <string name="pip_suspended">Σε αναστολή</string> + <string name="pip_not_ready">Η λειτουργία εικόνας εντός εικόνας είναι διαθέσιμη μόλις η συνεδρία είναι έτοιμη.</string> + <string name="pip_permission">Επιτρέψτε την εικόνα εντός εικόνας για το DroidDeck στις ρυθμίσεις Android.</string> + <string name="pip_unavailable">Το Android δεν μπόρεσε να ανοίξει τη λειτουργία εικόνας εντός εικόνας.</string> + <string name="game_shortcut">Συντόμευση</string> + <string name="game_shortcut_add">Προσθήκη στην αρχική οθόνη</string> + <string name="game_shortcut_choose">Επιλογή παιχνιδιού</string> + <string name="game_shortcut_scanning">Σάρωση εγκατεστημένων παιχνιδιών…</string> + <string name="game_link_copy">Αντιγραφή συνδέσμου εκκίνησης</string> + <string name="game_link_copied">Ο σύνδεσμος εκκίνησης αντιγράφηκε</string> + <string name="game_shortcut_unsupported">Αυτό το πρόγραμμα εκκίνησης δεν μπορεί να προσθέσει συντομεύσεις. Χρησιμοποιήστε την Αντιγραφή συνδέσμου εκκίνησης.</string> + <string name="game_link_invalid">Μη έγκυρος σύνδεσμος εκκίνησης παιχνιδιού.</string> + <string name="game_link_missing">Αυτό το παιχνίδι δεν βρίσκεται πλέον στη βιβλιοθήκη DroidDeck σας.</string> + <string name="game_link_session_busy">Διακόψτε την τρέχουσα συνεδρία πριν εκκινήσετε ένα παιχνίδι Steam.</string> + <string name="game_link_failed">Δεν ήταν δυνατή η αποστολή του παιχνιδιού στο Steam. Δοκιμάστε να το εκκινήσετε ξανά.</string> + <string name="setup_retry_removal">Επανάληψη κατάργησης</string> + <string name="runtime_removing">Κατάργηση</string> + <string name="runtime_removal_incomplete">Η κατάργηση δεν ολοκληρώθηκε</string> + <string name="game_file_export">Εξαγωγή αρχείου παιχνιδιού</string> + <string name="game_file_sync">Συγχρονισμός όλων των αρχείων παιχνιδιού σε φάκελο</string> + <string name="game_file_stop_sync">Διακοπή συγχρονισμού αρχείων παιχνιδιού</string> + <string name="game_file_folder">Επιλέξτε φάκελο για τα αρχεία παιχνιδιού της διεπαφής</string> + <string name="game_file_exported">Τα αρχεία παιχνιδιού αποθηκεύτηκαν στο %1$s</string> + <string name="game_frontend_files">Αρχεία διεπαφής</string> + + <string name="main_wait_runtime">Περιμένετε να ολοκληρωθεί η λειτουργία του 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> + <string name="main_saves_importing">Εισαγωγή στο %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="one">Έγινε εισαγωγή %1$d αρχείου από το %2$s στο %3$s</item> + <item quantity="other">Έγινε εισαγωγή %1$d αρχείων από το %2$s στο %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">Έγινε εισαγωγή %1$d αρχείου από το %2$s στο %3$s. Τα παλιά αρχεία αποθήκευσης κρατήθηκαν ως αντίγραφο ασφαλείας στο Download/DroidDeck/Saves/backups</item> + <item quantity="other">Έγινε εισαγωγή %1$d αρχείων από το %2$s στο %3$s. Τα παλιά αρχεία αποθήκευσης κρατήθηκαν ως αντίγραφο ασφαλείας στο Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Επιλέξτε ένα zip αποθηκευμένων αρχείων για το %1$s</string> + <string name="main_saves_exporting">Εξαγωγή %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="one">Έγινε εξαγωγή %1$d αρχείου ως %2$s: %3$s</item> + <item quantity="other">Έγινε εξαγωγή %1$d αρχείων ως %2$s: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Επιλέξτε πού θα αποθηκευτεί το %1$s (%2$s)</string> + <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> + <plurals name="main_logs_cleared"> + <item quantity="one">Έγινε εκκαθάριση %1$d φακέλου αρχείων καταγραφής συνεδρίας.</item> + <item quantity="other">Έγινε εκκαθάριση %1$d φακέλων αρχείων καταγραφής συνεδρίας.</item> + </plurals> + <string name="main_wadb_pair_failed">Η σύζευξη ασύρματου εντοπισμού σφαλμάτων απέτυχε</string> + <string name="main_wadb_command_failed">Η εντολή ασύρματου εντοπισμού σφαλμάτων απέτυχε</string> + <string name="main_wadb_unavailable">Ο ασύρματος εντοπισμός σφαλμάτων δεν είναι διαθέσιμος</string> + <string name="main_adb_copied">Η εντολή ADB αντιγράφηκε</string> + <string name="main_pick_roms">Επιλέξτε τον φάκελο ROMs</string> + <string name="main_non_adreno_title">Δεν είναι GPU Adreno</string> + <string name="main_non_adreno_text">Το Turnip υποστηρίζει GPU Adreno. Σε %1$s, το Steam ίσως εμφανίσει μαύρη οθόνη. Λήψη: %2$.0f MB.</string> + <string name="main_install_anyway">Εγκατάσταση παρόλα αυτά</string> + <string name="main_remove_runtime_title">Κατάργηση του Linux runtime</string> + <string name="main_remove_runtime_text">Αυτό διαγράφει το runtime, τον πελάτη Steam μέσα σε αυτό και κάθε παιχνίδι που είναι εγκατεστημένο εκεί.</string> + <string name="main_applied">Εφαρμόστηκε: %1$s</string> + <string name="android_app_open_failed_primary">Δεν ήταν δυνατό το άνοιγμα του %1$s στην κύρια οθόνη</string> + <string name="android_app_open_failed_on">Δεν ήταν δυνατό το άνοιγμα του %1$s στο %2$s</string> + <string name="android_app_open_failed_display">Δεν ήταν δυνατό το άνοιγμα του %1$s στην οθόνη %2$d</string> + <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> + <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> + <string name="comp_stored">Αποθηκεύτηκε %1$s</string> + <string name="comp_download_failed">Η λήψη απέτυχε: %1$s</string> + <string name="drivers_pick_linux">Επιλέξτε πρόγραμμα οδήγησης runtime 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> + <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">Επιλέξτε αρχείο ZIP προσθέτου Decky</string> + <string name="main_storage_not_usable">Δεν είναι χρησιμοποιήσιμο: %1$s</string> + <string name="main_running_program">Πρόγραμμα</string> + <string name="main_removing_runtime">Κατάργηση του Linux runtime</string> + <string name="main_settings_main_screen">Δεν ήταν δυνατό το άνοιγμα των Ρυθμίσεων στην κάτω οθόνη· άνοιξαν στην κύρια οθόνη.</string> + <string name="main_open_dev_options">Άνοιγμα Επιλογών προγραμματιστή στις Ρυθμίσεις</string> + + <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> + <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> + + <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> + + <string name="drivers_not_checked">Δεν έχει ελεγχθεί ακόμα - πατήστε ανανέωση για αναζήτηση νέων προγραμμάτων οδήγησης</string> + <string name="drivers_bundle_sets_display">Android + Linux: ορίζει το πρόγραμμα οδήγησης οθόνης μαζί με αυτό</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: ορίζει το πρόγραμμα οδήγησης runtime μαζί με αυτό</string> + <string name="drivers_runtime_builtin">το Turnip που είναι ενσωματωμένο στο runtime</string> + <string name="drivers_system_vulkan">Vulkan συστήματος: δεν υπάρχει πακέτο έκδοσης για αυτό το GPU</string> + <string name="drivers_bundled">%1$s (σε πακέτο)</string> + <string name="drivers_manual">Μη αυτόματο</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">Αυτή τη στιγμή έχει δημοσιευτεί μόνο το ένα μισό</string> + <string name="drivers_pair_size">%1$.0f MB για τα δύο</string> + <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> + <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> + <string name="drivers_switched_to">Έγινε αλλαγή σε %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Έγινε αλλαγή σε %1$s %2$s. Το πρόγραμμα οδήγησης οθόνης εφαρμόζεται μετά την επανεκκίνηση του DroidDeck.</string> + <string name="drivers_using">Χρήση %1$s %2$s.</string> + <string name="drivers_using_restart">Χρήση %1$s %2$s. Το πρόγραμμα οδήγησης οθόνης εφαρμόζεται μετά την επανεκκίνηση του DroidDeck.</string> + <string name="drivers_not_a_driver">Δεν είναι zip προγράμματος οδήγησης: ούτε πρόγραμμα οδήγησης AdrenoTools, ούτε -Linux Turnip, ούτε πακέτο Android + Linux</string> + <string name="drivers_import_failed">Η εισαγωγή απέτυχε: %1$s</string> + <string name="drivers_imported_unnamed">Έγινε εισαγωγή προγράμματος οδήγησης</string> + <string name="drivers_using_bundle">Χρήση %1$s ως πρόγραμμα οδήγησης runtime και οθόνης.</string> + <string name="drivers_using_bundle_restart">Χρήση %1$s ως πρόγραμμα οδήγησης runtime και οθόνης. Το πρόγραμμα οδήγησης οθόνης εφαρμόζεται μετά την επανεκκίνηση του DroidDeck.</string> + <string name="drivers_deleted_bundle">Διαγράφηκε %1$s (και τα δύο προγράμματα οδήγησης)</string> + <string name="drivers_deleted">Διαγράφηκε %1$s</string> + <string name="drivers_release_status">Πιο πρόσφατο: %1$s · έλεγχος %2$s</string> + <string name="drivers_release_status_failed">Πιο πρόσφατο: %1$s · %2$s μη προσβάσιμο · έλεγχος %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">%1$d λεπτό πριν</item> + <item quantity="other">%1$d λεπτά πριν</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">%1$d ώρα πριν</item> + <item quantity="other">%1$d ώρες πριν</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">%1$d ημέρα πριν</item> + <item quantity="other">%1$d ημέρες πριν</item> + </plurals> + <string name="drivers_checking_sources">Έλεγχος Banners-Turnip, WinNative και DroidDeck…</string> + <string name="drivers_check_failed">ο έλεγχος απέτυχε</string> + <string name="drivers_installed_pick_either">Εγκατεστημένο %1$s - επιλέξτε το σε οποιαδήποτε λίστα για να ορίσετε και τα δύο προγράμματα οδήγησης</string> + <string name="drivers_installed_pick">Εγκατεστημένο %1$s - επιλέξτε το στο μενού</string> + + <string name="session_wait_removal">Περιμένετε να ολοκληρωθεί η κατάργηση του runtime</string> + <string name="session_title_game">Παιχνίδι</string> + <string name="session_touch_auto_touchpad">πινακίδα αφής</string> + <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> + <string name="session_named_paused">%1$s είναι σε παύση</string> + <string name="session_program_paused">Το πρόγραμμα είναι σε παύση</string> + <string name="session_starting_desktop">Εκκίνηση της επιφάνειας εργασίας</string> + <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> + <string name="session_runtime_downloading">λήψη του Linux runtime</string> + <string name="session_runtime_downloading_mb">λήψη του Linux runtime · %1$d από %2$d MB</string> + <string name="session_runtime_checking">έλεγχος του Linux runtime</string> + <string name="session_runtime_unpacking">αποσυμπίεση του Linux runtime</string> + <string name="session_desktop_downloading">λήψη της επιφάνειας εργασίας</string> + <string name="session_desktop_downloading_mb">λήψη της επιφάνειας εργασίας · %1$d από %2$d MB</string> + <string name="session_desktop_checking">έλεγχος της επιφάνειας εργασίας</string> + <string name="session_desktop_unpacking">αποσυμπίεση της επιφάνειας εργασίας</string> + <string name="session_proton_downloading">λήψη του Proton Experimental (ARM64)</string> + <string name="session_proton_downloading_mb">λήψη του Proton Experimental (ARM64) · %1$d από %2$d MB</string> + <string name="session_proton_checking">έλεγχος του Proton Experimental (ARM64)</string> + <string name="session_proton_unpacking">αποσυμπίεση του Proton Experimental (ARM64)</string> + <string name="session_runtime_catalog_unreachable">Δεν ήταν δυνατή η πρόσβαση στον κατάλογο runtime. Ελέγξτε τη σύνδεση και πατήστε ξανά Αναπαραγωγή.</string> + <string name="session_runtime_install_failed">Το Linux runtime δεν εγκαταστάθηκε. Ελέγξτε το runtime στη Ρύθμιση και πατήστε ξανά Αναπαραγωγή.</string> + <string name="session_desktop_install_failed">Η επιφάνεια εργασίας δεν εγκαταστάθηκε (%1$s). Ελέγξτε τη σύνδεση και πατήστε ξανά Επιφάνεια εργασίας.</string> + <string name="session_ended_named">%1$s διακόπηκε αναπάντεχα. Κοινοποιήστε τα αρχεία καταγραφής με μια αναφορά σφάλματος, ή δοκιμάστε ξανά.</string> + <string name="session_ended_program">Το πρόγραμμα διακόπηκε αναπάντεχα. Κοινοποιήστε τα αρχεία καταγραφής με μια αναφορά σφάλματος, ή δοκιμάστε ξανά.</string> + <string name="session_ended_desktop">Η επιφάνεια εργασίας διακόπηκε αναπάντεχα. Κοινοποιήστε τα αρχεία καταγραφής με μια αναφορά σφάλματος, ή δοκιμάστε ξανά.</string> + <string name="session_ended_detail">Κατάσταση εξόδου %1$d · %2$s</string> + <string name="session_ended_no_log">χωρίς αρχείο καταγραφής</string> + <plurals name="session_hint_enosys"> + <item quantity="one">Το αρχείο καταγραφής δείχνει %1$d σφάλμα «Function not implemented»: η επιτάχυνση seccomp του proot αποτυγχάνει σε έναν βοηθητικό μηχανισμό σε αυτή τη συσκευή. Δοκιμάστε %2$s → «%3$s» (ή «%4$s») και ξεκινήστε ξανά.</item> + <item quantity="other">Το αρχείο καταγραφής δείχνει %1$d σφάλματα «Function not implemented»: η επιτάχυνση seccomp του proot αποτυγχάνει σε έναν βοηθητικό μηχανισμό σε αυτή τη συσκευή. Δοκιμάστε %2$s → «%3$s» (ή «%4$s») και ξεκινήστε ξανά.</item> + </plurals> + + <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> + <string name="store_cat_games">Παιχνίδια</string> + <string name="store_cat_audio_video">Ήχος & βίντεο</string> + <string name="store_cat_graphics">Γραφικά</string> + <string name="store_cat_internet">Διαδίκτυο</string> + <string name="store_cat_office">Γραφείο</string> + <string name="store_cat_developer">Προγραμματιστής</string> + <string name="store_cat_education">Εκπαίδευση</string> + <string name="store_cat_science">Επιστήμη</string> + <string name="store_cat_system">Σύστημα</string> + <string name="store_cat_utilities">Βοηθητικά</string> + <string name="store_section_popular">Δημοφιλή</string> + <string name="store_section_trending">Σε άνοδο</string> + <string name="store_section_new">Νέα</string> + <string name="store_section_updated">Ενημερωμένα</string> + <string name="store_section_loading">γίνεται φόρτωση…</string> + <string name="store_flatpak_setup">Ρύθμιση Flatpak</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Δεν ήταν δυνατός ο έλεγχος του Flathub για ενημερώσεις</string> + <string name="content_rom_installed">εγκατεστημένο</string> + <string name="content_rom_folder">φάκελος</string> + <string name="theme_graphite">Γραφίτης</string> + <string name="theme_graphite_detail">Επιφάνειες από γραφίτη, με το μπλε της σφαίρας του λογότυπου για ό,τι είναι επιλεγμένο, εστιασμένο ή πατημένο.</string> + <string name="theme_paper">Χαρτί σε μαύρο</string> + <string name="theme_paper_detail">Το λευκό είναι το κύριο χρώμα, το μπλε του εικονιδίου είναι το σήμα για γραμμές, δαχτυλίδια και κουκκίδες.</string> + <string name="theme_icon_blue">Μπλε εικονιδίου</string> + <string name="theme_icon_blue_detail">Μαύρο φόντο, λευκά γράμματα, το μπλε για ό,τι είναι επιλεγμένο, εστιασμένο ή πατημένο.</string> + <string name="theme_electric">Ηλεκτρικό ναυτικό μπλε</string> + <string name="theme_electric_detail">Το μπλε πιο έντονο, σε επιφάνειες που τείνουν προς το ναυτικό μπλε αντί για γκρι.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Space</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <string name="game_sync_err_runtime">Εγκαταστήστε πρώτα το 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">Τα manifest του Steam δεν είναι διαθέσιμα</string> + <string name="content_system_many">Πολλά συστήματα</string> + <string name="fm_drive_internal">Εσωτερική</string> + <string name="fm_drive_linux">Σύστημα Linux</string> + <string name="fm_drive_sd">Κάρτα SD</string> + <string name="fm_drive_storage">Αποθηκευτικός χώρος</string> + <string name="fm_select">Επιλογή</string> + <string name="fm_properties">Ιδιότητες</string> + <string name="fm_favorite_add">Προσθήκη στα αγαπημένα</string> + <string name="fm_favorite_remove">Κατάργηση από τα αγαπημένα</string> + <string name="fm_rename">Μετονομασία</string> + <string name="fm_copy">Αντιγραφή</string> + <string name="fm_cut">Αποκοπή</string> + <string name="fm_actions">Ενέργειες</string> + <string name="fm_favorites">Αγαπημένα</string> + <string name="fm_pin_current">Καρφίτσωμα τρέχοντος φακέλου</string> + <string name="fm_no_favorites">Δεν υπάρχουν ακόμα αγαπημένα - καρφιτσώστε έναν φάκελο με το μενού ⋮ του για να έρχεστε εδώ γρήγορα.</string> + <string name="fm_unpin_favorite">Κατάργηση από τα αγαπημένα</string> + <string name="fm_size_zero">0 bytes</string> + <string name="fm_unit_bytes">bytes</string> + <string name="fm_prop_location">Τοποθεσία</string> + <string name="fm_prop_type">Τύπος</string> + <string name="fm_prop_size">Μέγεθος</string> + <string name="fm_prop_modified">Τροποποιήθηκε</string> + <string name="fm_type_folder">Φάκελος</string> + <string name="fm_type_file">Αρχείο</string> + <string name="fm_type_ext_file">Αρχείο %1$s</string> + <string name="fm_read_only">Μόνο για ανάγνωση</string> + <string name="fm_read_only_hint">Αποτρέπει τα παιχνίδια (και το Wine) από την αντικατάσταση ή διαγραφή αυτού του αρχείου.</string> + <string name="fm_read_only_failed">Δεν ήταν δυνατή η αλλαγή του Μόνο για ανάγνωση</string> + <string name="fm_hidden">Κρυφό</string> + <string name="fm_hidden_unsupported">Αυτός ο αποθηκευτικός χώρος δεν μπορεί να αποθηκεύσει τη σημαία απόκρυψης.</string> + <string name="fm_hidden_hint">Σημαδεύει το αρχείο ως κρυφό στα Windows (η ιδιότητα κρυφού του Wine).</string> + <string name="fm_hidden_failed">Δεν ήταν δυνατή η αλλαγή του Κρυφό σε αυτόν τον αποθηκευτικό χώρο</string> + <string name="fm_done">Τέλος</string> + <string name="fm_op_deleting">Γίνεται διαγραφή...</string> + <string name="fm_delete_failed">Η διαγραφή απέτυχε</string> + <string name="fm_op_moving">Γίνεται μετακίνηση...</string> + <string name="fm_op_copying">Γίνεται αντιγραφή...</string> + <string name="fm_op_moving_item">Μετακίνηση - %1$s</string> + <string name="fm_op_copying_item">Αντιγραφή - %1$s</string> + <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> + <string name="fm_paste_failed">%1$d ολοκληρώθηκαν, %2$d απέτυχαν</string> + <string name="fm_paste_skipped">%1$d ολοκληρώθηκαν, %2$d παραλείφθηκαν</string> + <plurals name="fm_paste_moved"> + <item quantity="one">%1$d στοιχείο μετακινήθηκε</item> + <item quantity="other">%1$d στοιχεία μετακινήθηκαν</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">%1$d στοιχείο αντιγράφηκε</item> + <item quantity="other">%1$d στοιχεία αντιγράφηκαν</item> + </plurals> + <string name="fm_already_exists">Το «%1$s» υπάρχει ήδη</string> + <string name="fm_op_renaming">Γίνεται μετονομασία...</string> + <string name="fm_rename_failed">Η μετονομασία απέτυχε</string> + <string name="fm_op_creating_folder">Γίνεται δημιουργία φακέλου...</string> + <string name="fm_create_folder_failed">Δεν ήταν δυνατή η δημιουργία φακέλου</string> + <string name="fm_new_folder">Νέος φάκελος</string> + <string name="fm_folder_name">Όνομα φακέλου</string> + <string name="fm_create">Δημιουργία</string> + <string name="fm_new_name">Νέο όνομα</string> + <string name="fm_delete_title">Διαγραφή;</string> + <string name="fm_delete_text">Διαγραφή του «%1$s» οριστικά;</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">Διαγραφή %1$d στοιχείου;</item> + <item quantity="other">Διαγραφή %1$d στοιχείων;</item> + </plurals> + <string name="fm_cannot_undo">Αυτό δεν μπορεί να αναιρεθεί.</string> + <plurals name="fm_and_more"> + <item quantity="one">…και %1$d ακόμα</item> + <item quantity="other">…και %1$d ακόμα</item> + </plurals> + <string name="fm_op_deleting_batch">Διαγραφή %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">%1$d δεν ήταν δυνατό να διαγραφεί</item> + <item quantity="other">%1$d δεν ήταν δυνατό να διαγραφούν</item> + </plurals> + <string name="fm_conflict_merge_hint">Η συγχώνευση προσθέτει και αντικαθιστά αρχεία μέσα στον υπάρχοντα φάκελο.</string> + <string name="fm_conflict_overwrite_hint">Η αντικατάσταση αντικαθιστά το υπάρχον αρχείο.</string> + <string name="fm_conflict_apply_all">Εφαρμογή σε όλες τις συγκρούσεις</string> + <string name="fm_conflict_merge">Συγχώνευση</string> + <string name="fm_conflict_overwrite">Αντικατάσταση</string> + <string name="fm_conflict_keep_both">Διατήρηση και των δύο</string> + <string name="fm_conflict_skip">Παράλειψη</string> + <string name="fm_select_this_folder">Επιλογή αυτού του φακέλου</string> + <string name="fm_back">Πίσω</string> + <string name="fm_drive_not_readable">Το %1$s είναι προσαρτημένο αλλά δεν είναι αναγνώσιμο αυτή τη στιγμή</string> + <string name="fm_list_view">Προβολή λίστας</string> + <string name="fm_grid_view">Προβολή πλέγματος</string> + <string name="fm_search">Αναζήτηση</string> + <string name="fm_sort">Ταξινόμηση</string> + <string name="fm_sort_name">Όνομα</string> + <string name="fm_sort_date">Ημερομηνία τροποποίησης</string> + <string name="fm_rows_comfortable">Άνετες γραμμές</string> + <string name="fm_rows_compact">Συμπαγείς γραμμές</string> + <string name="fm_hide_hidden">Απόκρυψη κρυφών αρχείων</string> + <string name="fm_show_hidden">Εμφάνιση κρυφών αρχείων</string> + <string name="fm_hide_favorites">Απόκρυψη αγαπημένων</string> + <string name="fm_show_favorites">Εμφάνιση αγαπημένων</string> + <string name="fm_filter_folder">Φιλτράρισμα αυτού του φακέλου</string> + <string name="fm_free_space">%1$s ελεύθερα</string> + <string name="fm_selected_count">%1$d επιλεγμένα</string> + <string name="fm_select_none">Κανένα</string> + <string name="fm_select_all">Όλα</string> + <string name="fm_paste">Επικόλληση</string> + <plurals name="fm_items"> + <item quantity="one">%1$d στοιχείο</item> + <item quantity="other">%1$d στοιχεία</item> + </plurals> + <string name="fm_paste_here">Επικόλληση %1$s εδώ</string> + <string name="fm_paste_move_here">Επικόλληση %1$s (μετακίνηση) εδώ</string> + <string name="fm_rail_title">Αρχεία</string> + <string name="fm_rail_storage">ΑΠΟΘΗΚΕΥΤΙΚΟΣ ΧΩΡΟΣ</string> + <string name="fm_rail_quick">ΓΡΗΓΟΡΑ</string> + <string name="fm_rail_favorites">ΑΓΑΠΗΜΕΝΑ</string> + <string name="fm_rail_downloads">Λήψεις</string> + <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> + <string name="fm_favorite_added">Το «%1$s» προστέθηκε στα αγαπημένα</string> + <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> + <string name="ctrl_tint_steam_blue">Μπλε Steam</string> + <string name="ctrl_tint_sky">Γαλάζιο</string> + <string name="ctrl_tint_white">Λευκό</string> + <string name="ctrl_tint_violet">Βιολετί</string> + <string name="ctrl_tint_green">Πράσινο</string> + <string name="ctrl_tint_amber">Κεχριμπαρί</string> + <string name="ctrl_tint_red">Κόκκινο</string> + <string name="ctrl_tint_pink">Ροζ</string> + <string name="ctrl_button_a">Κουμπί A</string> + <string name="ctrl_button_b">Κουμπί B</string> + <string name="ctrl_button_x">Κουμπί X</string> + <string name="ctrl_button_y">Κουμπί Y</string> + <string name="ctrl_button_lb">Αριστερό bumper</string> + <string name="ctrl_button_rb">Δεξί bumper</string> + <string name="ctrl_button_lt">Αριστερή σκανδάλη</string> + <string name="ctrl_button_rt">Δεξιά σκανδάλη</string> + <string name="ctrl_button_view">Κουμπί View</string> + <string name="ctrl_button_menu">Κουμπί Menu</string> + <string name="ctrl_target_view">View</string> + <string name="ctrl_target_menu">Menu</string> + <string name="ctrl_target_up">D-pad πάνω</string> + <string name="ctrl_target_down">D-pad κάτω</string> + <string name="ctrl_target_left">D-pad αριστερά</string> + <string name="ctrl_target_right">D-pad δεξιά</string> + <string name="ctrl_target_hidden">Κρυφό</string> + <string name="ctrl_editor_drag">Σύρετε για μετακίνηση</string> + <string name="ctrl_editor_snap">Προσκόλληση</string> + <string name="ctrl_editor_save">Αποθήκευση</string> + <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> + + <string name="rtinst_removing">Κατάργηση του Linux runtime</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">Κατάργηση του Linux runtime · %1$d εγγραφή</item> + <item quantity="other">Κατάργηση του Linux runtime · %1$d εγγραφές</item> + </plurals> + <string name="rtinst_removed">Το Linux runtime καταργήθηκε</string> + <string name="rtinst_removal_failed">Δεν ήταν δυνατή η ολοκλήρωση της κατάργησης του Linux runtime. Επαναλάβετε την κατάργηση στη Ρύθμιση.</string> + <string name="rtinst_removing_leftovers">Κατάργηση υπολειμμάτων του runtime</string> + <string name="rtinst_downloading">Γίνεται λήψη</string> + <string name="rtinst_verifying">Γίνεται επαλήθευση</string> + <string name="rtinst_extracting">Γίνεται εξαγωγή</string> + <string name="rtinst_channel_description">Εμφανίζεται όσο το Linux runtime εγκαθίσταται ή καταργείται</string> + <string name="rtinst_title_installing">Εγκατάσταση του Linux runtime</string> + <string name="rtinst_title_removing">Κατάργηση του Linux runtime</string> + + <string name="deskpkg_runtime_missing">Το Linux runtime δεν είναι εγκατεστημένο</string> + <string name="deskpkg_no_checksum">Ο κατάλογος δεν έχει checksum για το %1$s</string> + <string name="deskpkg_checksum_mismatch">Αναντιστοιχία checksum - τίποτα δεν άλλαξε</string> + <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> + + <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> + <string name="flatpakmgr_installing">Εγκατάσταση %1$s</string> + <string name="flatpakmgr_updating">Ενημέρωση %1$s</string> + <string name="flatpakmgr_removing">Κατάργηση %1$s</string> + <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> + <string name="flatpakmgr_ref_translations">μεταφράσεις</string> + <string name="flatpakmgr_ref_graphics_drivers">προγράμματα οδήγησης γραφικών</string> + <string name="flatpakmgr_ref_media_codecs">codecs πολυμέσων</string> + <string name="flatpakmgr_ref_runtime">runtime %1$s</string> + <string name="flatpakmgr_ref_runtime_branch">runtime %1$s %2$s</string> + + <string name="appimg_too_small">Το αρχείο είναι πολύ μικρό για να είναι AppImage</string> + <string name="appimg_unreadable">Το αρχείο δεν μπορεί να αναγνωστεί: %1$s</string> + <string name="appimg_not_linux">Αυτό δεν είναι AppImage (δεν είναι πρόγραμμα Linux)</string> + <string name="appimg_wrong_arch">Αυτό το AppImage δεν είναι χτισμένο ούτε για ARM64 (aarch64) ούτε για PC x86</string> + <string name="appimg_not_appimage">Αυτό είναι πρόγραμμα Linux αλλά όχι AppImage</string> + <string name="appimg_wrong_type">Μόνο AppImage τύπου 2 μπορούν να εισαχθούν (αυτό είναι τύπου %1$d)</string> + <string name="appimg_no_space_import">Δεν υπάρχει αρκετός ελεύθερος χώρος: η εισαγωγή απαιτεί περίπου %1$s</string> + <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> + <string name="appimg_extracting">Εξαγωγή %1$s</string> + <string name="appimg_extract_failed">Δεν ήταν δυνατή η εξαγωγή του AppImage</string> + <string name="appimg_extract_failed_detail">Δεν ήταν δυνατή η εξαγωγή του AppImage: %1$s</string> + + <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> + <string name="deckymgr_checksum_download_failed">Δεν ήταν δυνατή η λήψη του αθροίσματος ελέγχου της κυκλοφορίας</string> + <string name="deckymgr_checksum_missing">Λείπει το άθροισμα ελέγχου της κυκλοφορίας</string> + <string name="deckymgr_checksum_invalid">Το άθροισμα ελέγχου της κυκλοφορίας δεν είναι έγκυρο</string> + <string name="deckymgr_checksum_mismatch">Αναντιστοιχία αθροίσματος ελέγχου του Decky Loader</string> + <string name="deckymgr_not_elf">Το αρχείο της κυκλοφορίας δεν είναι συμβατό εκτελέσιμο Linux 64-bit</string> + <string name="deckymgr_session_started_loader">Μια συνεδρία ξεκίνησε κατά τη λήψη· διακόψτε την πριν εγκαταστήσετε το Decky Loader</string> + <string name="deckymgr_loader_not_executable">Δεν ήταν δυνατό να οριστεί το PluginLoader ως εκτελέσιμο</string> + <string name="deckymgr_cef_failed">Δεν ήταν δυνατή η ενεργοποίηση του απομακρυσμένου εντοπισμού σφαλμάτων CEF του Steam</string> + <string name="deckymgr_stage_failed">Δεν ήταν δυνατή η προετοιμασία του PluginLoader</string> + <string name="deckymgr_loader_preserve_failed">Δεν ήταν δυνατή η διατήρηση του υπάρχοντος PluginLoader</string> + <string name="deckymgr_loader_activate_failed">Δεν ήταν δυνατή η ενεργοποίηση του νέου PluginLoader</string> + <string name="deckymgr_stop_session_plugin">Διακόψτε την ενεργή συνεδρία πριν εγκαταστήσετε ένα πρόσθετο</string> + <string name="deckymgr_loader_required">Εγκαταστήστε το Decky Loader πριν εισαγάγετε ένα πρόσθετο</string> + <string name="deckymgr_zip_unreadable">Δεν ήταν δυνατή η ανάγνωση του επιλεγμένου αρχείου ZIP</string> + <string name="deckymgr_plugins_dir_failed">Δεν ήταν δυνατή η δημιουργία του καταλόγου προσθέτων Decky</string> + <string name="deckymgr_extracting_zip">Εξαγωγή ZIP προσθέτου</string> + <string name="deckymgr_zip_one_folder">Το ZIP προσθέτου πρέπει να περιέχει έναν φάκελο προσθέτου στο ανώτερο επίπεδο</string> + <string name="deckymgr_zip_missing_files">Από το ZIP προσθέτου λείπουν τα plugin.json, package.json ή dist/index.js</string> + <string name="deckymgr_plugin_name_missing">Λείπει το όνομα του προσθέτου</string> + <string name="deckymgr_session_started_plugin">Μια συνεδρία ξεκίνησε κατά την εγκατάσταση· διακόψτε την πριν εγκαταστήσετε ένα πρόσθετο</string> + <string name="deckymgr_plugin_installed">Το πρόσθετο εγκαταστάθηκε · κάντε επανεκκίνηση του Steam για να φορτωθεί</string> + <string name="deckymgr_plugin_install_failed">Η εγκατάσταση του ZIP προσθέτου απέτυχε</string> + <string name="deckymgr_zip_too_many">Το ZIP προσθέτου περιέχει πολύ μεγάλο αριθμό εγγραφών</string> + <string name="deckymgr_zip_invalid_path">Το ZIP προσθέτου περιέχει μη έγκυρη διαδρομή</string> + <string name="deckymgr_zip_duplicate_paths">Το ZIP προσθέτου περιέχει διπλές διαδρομές</string> + <string name="deckymgr_zip_unsafe_path">Το ZIP προσθέτου περιέχει μη ασφαλή διαδρομή</string> + <string name="deckymgr_folder_failed">Δεν ήταν δυνατή η δημιουργία φακέλου προσθέτου</string> + <string name="deckymgr_zip_too_large">Το ZIP προσθέτου επεκτείνεται πέρα από το όριο του 1 GiB</string> + <string name="deckymgr_bundled_not_executable">Δεν ήταν δυνατό να οριστεί το ενσωματωμένο εκτελέσιμο του προσθέτου ως εκτελέσιμο</string> + <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> + <string name="deckymgr_remote_hash_invalid">Το απομακρυσμένο εκτελέσιμο %1$s έχει μη έγκυρο κατακερματισμό SHA-256</string> + <string name="deckymgr_bin_dir_failed">Δεν ήταν δυνατή η δημιουργία καταλόγου bin του προσθέτου</string> + <string name="deckymgr_downloading_binary">Λήψη εκτελέσιμου προσθέτου · %1$s</string> + <string name="deckymgr_binary_download_failed">Δεν ήταν δυνατή η λήψη του εκτελέσιμου προσθέτου %1$s</string> + <string name="deckymgr_binary_checksum_mismatch">Αναντιστοιχία αθροίσματος ελέγχου εκτελέσιμου προσθέτου · %1$s</string> + <string name="deckymgr_binary_not_executable">Δεν ήταν δυνατό να οριστεί το εκτελέσιμο του προσθέτου ως εκτελέσιμο · %1$s</string> + <string name="deckymgr_binary_replace_failed">Δεν ήταν δυνατή η αντικατάσταση του εκτελέσιμου του προσθέτου · %1$s</string> + <string name="deckymgr_binary_install_failed">Δεν ήταν δυνατή η εγκατάσταση του εκτελέσιμου του προσθέτου · %1$s</string> + <string name="deckymgr_folder_taken">Ο φάκελος προσθέτου ανήκει σε άλλο πρόσθετο</string> + <string name="deckymgr_preserve_failed">Δεν ήταν δυνατή η διατήρηση του υπάρχοντος προσθέτου</string> + <string name="deckymgr_activate_failed">Δεν ήταν δυνατή η ενεργοποίηση του εισαγόμενου προσθέτου</string> + <string name="deckymgr_settings_failed">Δεν ήταν δυνατή η ενημέρωση των ρυθμίσεων προσθέτων Decky</string> + <string name="deckymgr_restore_failed">Δεν ήταν δυνατή η επαναφορά του υπάρχοντος προσθέτου</string> + + <string name="adbfix_save_failed">Δεν ήταν δυνατή η αποθήκευση της ταυτότητας ασύρματου εντοπισμού σφαλμάτων</string> + <string name="adbfix_command_failed">Η εντολή ADB απέτυχε (%1$d)</string> + <string name="adbfix_not_confirmed">Το Android δεν επιβεβαίωσε ότι το όριο θυγατρικών διαδικασιών άλλαξε: %1$s</string> + <string name="adbfix_no_pairing">Δεν υπάρχει αποθηκευμένη σύζευξη ασύρματου εντοπισμού σφαλμάτων</string> + <string name="adbfix_enter_address">Εισαγάγετε την τρέχουσα διεύθυνση IP & θύρα του ασύρματου εντοπισμού σφαλμάτων</string> + + <string name="adbpair_timed_out">Λήξη χρόνου αναμονής για το αναδυόμενο παράθυρο σύζευξης. Δοκιμάστε ξανά από το DroidDeck.</string> + <string name="adbpair_discovery_failed_code">Δεν ήταν δυνατή η αναζήτηση του αναδυόμενου παραθύρου σύζευξης (%1$d). Ελέγξτε ότι το Wi-Fi είναι ενεργό.</string> + <string name="adbpair_discovery_failed">Δεν ήταν δυνατή η αναζήτηση του αναδυόμενου παραθύρου σύζευξης: %1$s</string> + <string name="adbpair_six_digits">Εισαγάγετε όλα τα 6 ψηφία του κωδικού</string> + <string name="adbpair_pairing">Γίνεται σύζευξη…</string> + <string name="adbpair_code_rejected">Αυτός ο κωδικός δεν λειτούργησε. Ανοίξτε ξανά το αναδυόμενο παράθυρο σύζευξης και εισαγάγετε τον νέο κωδικό του.</string> + <string name="adbpair_connecting">Έγινε σύζευξη. Γίνεται σύνδεση…</string> + <string name="adbpair_port_not_found">Έγινε σύζευξη, αλλά δεν βρέθηκε η θύρα του ασύρματου εντοπισμού σφαλμάτων. Κρατήστε τον ασύρματο εντοπισμό σφαλμάτων ενεργό και δοκιμάστε ξανά.</string> + <string name="adbpair_applying">Εφαρμογή της ρύθμισης…</string> + <string name="adbpair_command_failed">Η εντολή ασύρματου εντοπισμού σφαλμάτων απέτυχε</string> + <string name="adbpair_channel_name">Ρύθμιση ασύρματου εντοπισμού σφαλμάτων</string> + <string name="adbpair_channel_description">Ζητά τον κωδικό σύζευξης ασύρματου εντοπισμού σφαλμάτων όσο οι Ρυθμίσεις είναι ανοιχτές</string> + <string name="adbpair_title_pair">Σύζευξη ασύρματου εντοπισμού σφαλμάτων</string> + <string name="adbpair_waiting_text">Ενεργοποιήστε τον ασύρματο εντοπισμό σφαλμάτων και πατήστε «Σύζευξη συσκευής με κωδικό σύζευξης».</string> + <string name="adbpair_waiting_long">Ενεργοποιήστε τον ασύρματο εντοπισμό σφαλμάτων και πατήστε «Σύζευξη συσκευής με κωδικό σύζευξης». Παραμείνετε στις Ρυθμίσεις: ο κωδικός εμφανίζεται σε αυτή την ειδοποίηση.</string> + <string name="adbpair_code_label">Εξαψήφιος κωδικός σύζευξης</string> + <string name="adbpair_code_hint">Πατήστε Εισαγωγή κωδικού και πληκτρολογήστε τα 6 ψηφία που εμφανίζονται στις Ρυθμίσεις.</string> + <string name="adbpair_title_code">Εισαγάγετε τον κωδικό σύζευξης Wi-Fi</string> + <string name="adbpair_title_retry">Δοκιμάστε ξανά τον κωδικό</string> + <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> + + <string name="selfinst_http">Η λήψη απάντησε με HTTP %1$d</string> + <string name="selfinst_size_mismatch">Το μέγεθος της λήψης δεν ταίριαζε με το δημοσιευμένο APK και απορρίφθηκε - δοκιμάστε ξανά</string> + <string name="selfinst_checksum_mismatch">Η λήψη δεν ταίριαζε με το άθροισμα ελέγχου της και απορρίφθηκε - δοκιμάστε ξανά</string> + <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> + <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> + <string name="selfinst_no_signing">Το ληφθέν APK δεν έχει πληροφορίες υπογραφής</string> + <string name="selfinst_wrong_signer">Το ληφθέν APK δεν είναι υπογεγραμμένο με το κλειδί κυκλοφορίας του DroidDeck</string> + <string name="appupd_http">Ο κατάλογος ενημερώσεων απάντησε με HTTP %1$d</string> + + <string name="loadstate_starting">Εκκίνηση της συνεδρίας…</string> + <string name="loadstate_elapsed">%1$d:%2$02d πέρασαν · ακόμα σε εξέλιξη</string> + <string name="loadstate_steam_update">Λήψη της ενημέρωσης του πελάτη Steam · %1$d%%</string> + <string name="loadstate_steam_client">Λήψη του πελάτη Steam · %1$s</string> + + <string name="pextra_busy">Μια εγκατάσταση εργαλείου συμβατότητας εκτελείται ήδη</string> + <string name="pextra_stop_session">Διακόψτε την ενεργή συνεδρία πριν εγκαταστήσετε εργαλεία συμβατότητας</string> + <string name="pextra_no_release">Δεν βρέθηκε κυκλοφορία ARM64 για το %1$s</string> + <string name="pextra_no_space">Απαιτούνται τουλάχιστον 4 GB ελεύθερα συν το μέγεθος της λήψης για να εγκατασταθεί το %1$s</string> + <string name="pextra_downloading">Λήψη %1$s %2$s</string> + <string name="pextra_retrying">Επανάληψη λήψης · προσπάθεια %1$d από %2$d</string> + <string name="pextra_interrupted">Η λήψη διακόπηκε</string> + <string name="pextra_download_failed">Η λήψη απέτυχε· το μερικό αρχείο διατηρείται ώστε μια νέα προσπάθεια να μπορεί να συνεχίσει</string> + <string name="pextra_too_big">Η λήψη υπερέβη το μέγεθος της κυκλοφορίας και διαγράφηκε</string> + <string name="pextra_incomplete">Η λήψη ήταν ημιτελής· το μερικό αρχείο διατηρείται για επανάληψη με συνέχιση</string> + <string name="pextra_verifying">Επαλήθευση λήψης</string> + <string name="pextra_checksum_unreadable">Δεν ήταν δυνατή η ανάγνωση του αθροίσματος ελέγχου της κυκλοφορίας· δοκιμάστε ξανά</string> + <string name="pextra_no_checksum">Το %1$s %2$s δεν δημοσιεύει άθροισμα ελέγχου· δεν εγκαταστάθηκε τίποτα</string> + <string name="pextra_checksum_mismatch">Αναντιστοιχία αθροίσματος ελέγχου· η λήψη διαγράφηκε</string> + <string name="pextra_session_started">Μια συνεδρία ξεκίνησε κατά τη λήψη· διακόψτε την πριν εγκαταστήσετε εργαλεία συμβατότητας</string> + <string name="pextra_installing">Εγκατάσταση %1$s</string> + <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> + + <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> + <string name="cmgr_not_present">δεν υπάρχει</string> + <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> + <string name="cmgr_restore_queued">Το %1$s του %2$s θα επανέλθει στο αρχικό του όταν κλείσει το παιχνίδι που εκτελείται σε αυτό.</string> + <string name="cmgr_restore_done">Το %1$s του %2$s επανήλθε από το αρχικό του %3$s.</string> + <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> + <string name="cmgr_deleted_original">Διαγράφηκε το αρχικό %1$s του %2$s.</string> + <string name="cmgr_nightlies_only">Οι λήψεις προέρχονται μόνο από τις κυκλοφορίες Nightlies</string> + <string name="cmgr_list_predates_checksums">Αυτή η λίστα πακέτων προηγείται των αθροισμάτων ελέγχου - ανανεώστε την και δοκιμάστε ξανά</string> + <string name="cmgr_checksum_mismatch">Αναντιστοιχία αθροίσματος ελέγχου</string> + + <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> + + <string name="look_off_desc">Δεν εφαρμόζεται τίποτα.</string> + <string name="look_game_clarity">Σαφήνεια παιχνιδιού</string> + <string name="look_game_clarity_desc">Όξυνση με μια πινελιά αντίθεσης - το καθημερινό.</string> + <string name="look_vivid">Έντονο</string> + <string name="look_vivid_desc">Έντονα ενισχυμένο χρώμα. Το Deband ενεργό ώστε οι ουρανοί να μη γραμμώνονται.</string> + <string name="look_cinematic">Κινηματογραφικό</string> + <string name="look_cinematic_desc">Βαθύτερη αντίθεση, πιο ψυχρό χρώμα, ομαλές ακμές.</string> + <string name="look_competitive">Ανταγωνιστικό</string> + <string name="look_competitive_desc">Ανασηκωμένες σκιές και έντονη όξυνση - εντοπίστε πρώτα τους παίκτες.</string> + <string name="look_adaptive_sharpen">Προσαρμοστική όξυνση</string> + <string name="look_adaptive_sharpen_desc">Μόνο όξυνση, το χρώμα παραμένει ανεπηρέαστο.</string> + <string name="look_filmic">Φιλμικό</string> + <string name="look_filmic_desc">Καμπύλη σε στιλ φιλμ, ελαφρώς απαλή.</string> + <string name="look_arcade">Arcade</string> + <string name="look_arcade_desc">Έντονο και δυναμικό.</string> + <string name="look_retro_crt">Ρετρό CRT</string> + <string name="look_retro_crt_desc">Ο σωλήνας CRT και τα αναλογικά περάσματα σήματος μαζί.</string> + <string name="look_upscale_sharp">Έντονη μεγέθυνση</string> + <string name="look_upscale_sharp_desc">Για λειτουργία κάτω από την ανάλυση της οθόνης.</string> + <string name="look_pixel_clean">Καθαρά Pixel</string> + <string name="look_pixel_clean_desc">Ευκρινή pixel για 2D και παλιά παιχνίδια.</string> + <string name="look_anime_edge">Περίγραμμα Anime</string> + <string name="look_anime_edge_desc">Περιγράμματα τύπου toon συν όξυνση.</string> + + <string name="gpuinfo_not_adreno">Δεν είναι GPU Adreno</string> + <string name="gpuinfo_supported">Υποστηρίζεται</string> + <string name="gpuinfo_experimental">Πειραματικό: τα προγράμματα οδήγησής του είναι δοκιμαστικές εκδόσεις</string> + <string name="gpuinfo_below_tested">Κάτω από το δοκιμασμένο υλικό (Adreno 725 και νεότερα): ίσως να μην εκτελεστεί</string> + <string name="gpuinfo_unsupported">Δεν υποστηρίζεται: το DroidDeck χρειάζεται GPU Adreno (Snapdragon)</string> + + <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> + <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">αυτό είναι πρόγραμμα οδήγησης runtime Linux (εισαγάγετέ το από το «Πρόγραμμα οδήγησης runtime Linux»)</string> + <string name="turnip_reject_wayland">αυτό είναι πρόγραμμα οδήγησης παιχνιδιών Wayland, το οποίο αυτή η εφαρμογή δεν χρησιμοποιεί</string> + <string name="turnip_reject_no_library_name">Το meta.json δεν ονομάζει κανένα libraryName, οπότε δεν ήταν δυνατό να δοθεί τίποτα στο AdrenoTools</string> + <string name="turnip_reject_library_missing">Το meta.json ονομάζει το %1$s, το οποίο δεν βρίσκεται στο zip</string> + <string name="turnip_reject_not_aarch64">Το %1$s δεν είναι κοινόχρηστη βιβλιοθήκη ELF 64-bit AArch64</string> + <string name="turnip_reject_glibc">Το %1$s συνδέεται με glibc - ένα Turnip «-Linux», που ανήκει στο «Πρόγραμμα οδήγησης runtime Linux»</string> + <string name="lvd_no_library">Δεν υπάρχει libvulkan_freedreno*.so σε αυτό το zip. Ένα zip Android (AdrenoTools) ή -Wayland Turnip δεν είναι πρόγραμμα οδήγησης runtime Linux.</string> + <string name="lvd_not_aarch64">Το %1$s δεν είναι κοινόχρηστη βιβλιοθήκη ELF 64-bit AArch64.</string> + <string name="lvd_not_glibc">Το %1$s δεν είναι πρόγραμμα οδήγησης glibc - συνδέεται με τη libc του Android. Το Linux runtime χρειάζεται ένα zip «-Linux»· ένα απλό ή «-Wayland» Turnip δεν μπορεί να φορτωθεί καθόλου από τον πελάτη Steam.</string> + <string name="bundle_no_manifest">Δεν είναι πακέτο προγραμμάτων οδήγησης: δεν υπάρχει manifest.json που να ονομάζει πρόγραμμα οδήγησης android και linux</string> + <string name="bundle_entry_outside">Δεν είναι πακέτο προγραμμάτων οδήγησης: το %1$s δείχνει έξω από αυτό</string> + <string name="bundle_too_large">Δεν είναι πακέτο προγραμμάτων οδήγησης: αποσυμπιέζεται σε περισσότερα από %1$d MB</string> + + <string name="gstore_sd_card">Κάρτα SD</string> + <string name="gstore_folder">Φάκελος</string> + + <string name="logshare_chooser">Κοινοποίηση αρχείων καταγραφής συνεδρίας</string> + + <string name="hdr_needs_android_10">απαιτεί Android 10 ή νεότερο (επίπεδα οθόνης με χρωματικό χώρο)</string> + <string name="hdr_none">αυτή η οθόνη δεν αναφέρει υποστήριξη HDR</string> + <string name="hdr_not_hdr10">αυτή η οθόνη υποστηρίζει %1$s, όχι HDR10</string> + + <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> + <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> + <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> + <string name="upd_fail_with_message">Η ενημέρωση δεν εγκαταστάθηκε: %1$s</string> + <string name="upd_fail_generic">Η ενημέρωση δεν εγκαταστάθηκε.</string> + <string name="saves_layout_zip">zip %1$s</string> + <string name="saves_zip">zip</string> + <string name="saves_not_launched">Το %1$s δεν έχει εκκινηθεί ακόμα</string> + <string name="saves_none_found">Δεν βρέθηκαν αρχεία αποθήκευσης για το %1$s</string> + <string name="saves_not_save_zip">Το %1$s δεν έχει φάκελο drive_c, οπότε δεν είναι zip αποθήκευσης GameHub ή Winlator</string> + <string name="gstore_cannot_create">δεν είναι δυνατή η δημιουργία φακέλων στο %1$s</string> + <string name="gstore_cannot_write">δεν είναι δυνατή η εγγραφή στο %1$s</string> + <string name="gstore_cannot_write_detail">δεν είναι δυνατή η εγγραφή στο %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">Ο φάκελος δεν είναι διαθέσιμος</string> + <string name="gfiles_name_taken">Ένα διαφορετικό αρχείο χρησιμοποιεί ήδη το %1$s</string> + <string name="gfiles_art_unavailable">Ο φάκελος γραφικών δεν είναι διαθέσιμος</string> + <string name="gfiles_unrelated_inventory">Ο φάκελος έχει άσχετο ευρετήριο συγχρονισμού</string> + <string name="gfiles_remove_failed">Δεν ήταν δυνατή η κατάργηση του %1$s</string> + <string name="gfiles_outside_folder">Το αρχείο βγαίνει εκτός του φακέλου εξαγωγής</string> + <string name="cmgr_fex_unnamed">FEX (έκδοση χωρίς όνομα έκδοσης)</string> + <string name="cmgr_no_profile">χωρίς profile.json στο %1$s</string> + <string name="cmgr_not_component_package">Το %1$s δεν είναι πακέτο στοιχείου Linux (τύπος %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s ελεύθερα</string> + <string name="mode_storage_auto_short">Αυτόματο</string> + <string name="appupd_catalog_rejected">Ο κατάλογος ενημερώσεων απέτυχε στους ελέγχους του (%1$s)</string> + <string name="main_adb_clip_label">Ρύθμιση θυγατρικών διαδικασιών DroidDeck</string> + <string name="setup_signed_in_offline_unnamed">Συνδεδεμένος · λειτουργία εκτός σύνδεσης</string> + <string name="setup_signed_in_unnamed">Συνδεδεμένος</string> + <string name="driver_tag_bundled">ΣΕ ΠΑΚΕΤΟ</string> + <string name="driver_tag_downloaded">ΛΗΦΘΗΚΕ</string> + <string name="driver_tag_imported">ΕΙΣΗΧΘΗΚΕ</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Οθόνη συνεδρίας</string> + <string name="display_resolution">Ανάλυση</string> + <string name="display_game_fit">Προσαρμογή παιχνιδιού 16:9</string> + <string name="display_game_fit_hint">Πώς προσαρμόζονται τα παιχνίδια 16:9 σε αυτή την οθόνη. Από την επόμενη συνεδρία.</string> + <string name="display_game_fit_unavailable">Επιλέξτε Αντιστοίχιση οθόνης ή ένα προσαρμοσμένο μέγεθος πιο στενό από 16:9.</string> + <string name="display_preserve">Διατήρηση αναλογιών</string> + <string name="display_stretch">Διάταση στην οθόνη</string> + <string name="display_image_scaling">Κλιμάκωση εικόνας</string> + <string name="display_filter">Φίλτρο κλιμάκωσης</string> + <string name="display_filter_hint">Κοινό για το Steam και την Επιφάνεια εργασίας.</string> + <string name="display_filter_live">Εφαρμόζεται σε πραγματικό χρόνο. Κοινό για το Steam και την Επιφάνεια εργασίας.</string> + <string name="display_filter_note">Τα FSR, GSR και NIS εφαρμόζονται κατά τη μεγέθυνση της εικόνας συνεδρίας. Το Μόνο όξυνση λειτουργεί και χωρίς μεγέθυνση.</string> + <string name="display_sharpness">Οξύτητα φίλτρου</string> + <string name="display_frame_rate">Καρέ ανά δευτερόλεπτο</string> + <string name="display_look_filter">%1$s Ορίζει την κλιμάκωση σε %2$s.</string> + <string name="display_match_screen">Αντιστοίχιση οθόνης (%1$d×%2$d)</string> + <string name="display_follow_screen">Ακολούθηση οθόνης (αλλάζει μέγεθος όταν διπλώνει ή ξεδιπλώνει)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Εμφάνιση στοιχείων συσκευής</string> + <string name="device_info_hide">Απόκρυψη στοιχείων συσκευής</string> + <string name="device_info_loading">Ανάγνωση της συσκευής…</string> + <string name="device_info_copy">Αντιγραφή</string> + <string name="device_info_copy_hint">Αντιγράψτε αυτά τα στοιχεία σε μια αναφορά σφάλματος.</string> + <string name="device_info_copied">Αντιγράφηκε.</string> + <string name="force_ssbs_title">Εξαναγκασμός SSBS</string> + <string name="force_ssbs_hint">Διατηρεί μια ρύθμιση ταχύτητας CPU που το Wine απενεργοποιεί συνεχώς. Μπορεί να κάνει τα παιχνίδια Windows πιο γρήγορα σε Snapdragon 8 Elite· μικρή διαφορά σε παλαιότερα chip. Ισχύει από την επόμενη εκκίνηση παιχνιδιού.</string> + + <string name="rail_stores">Καταστήματα</string> + <string name="source_steam">Steam</string> + <string name="source_added">Προσαρμοσμένο</string> + <string name="setup_stores">Καταστήματα</string> + <string name="setup_stores_open_on">Άνοιγμα καταστήματος σε</string> + <string name="setup_stores_speed">Επίπεδο ταχύτητας λήψης</string> + <string name="setup_stores_engine">Μηχανή καταστημάτων</string> + <string name="setup_stores_engine_chip_ready">● Η μηχανή είναι έτοιμη</string> + <string name="setup_stores_engine_chip_missing">Δεν έχει χτιστεί</string> + <string name="stores_tier_balanced">Ισορροπημένο</string> + <string name="stores_tier_fast">Ταχύ</string> + <string name="stores_tier_max">Μέγιστο</string> + <string name="stores_title">Καταστήματα</string> + <string name="stores_settings">Ρυθμίσεις καταστημάτων</string> + <string name="stores_settings_eyebrow">Ρυθμίσεις</string> + <string name="stores_chip_signed_out">(μη συνδεδεμένος)</string> + <string name="stores_tab_store">Κατάστημα</string> + <string name="stores_tab_installed">Εγκατεστημένα</string> + <string name="stores_tab_library">Βιβλιοθήκη</string> + <string name="stores_tab_all">Όλα</string> + <string name="stores_tab_downloads">Λήψεις</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Σύνδεση</string> + <string name="stores_signout_named">%1$s · αποσύνδεση</string> + <string name="stores_signout_title">Αποσύνδεση από %1$s;</string> + <string name="stores_signout_note">Τα εγκατεστημένα παιχνίδια παραμένουν. Η βιβλιοθήκη ανακτάται ξανά όταν συνδεθείτε πάλι.</string> + <string name="stores_signout_stay">Παραμονή συνδεδεμένου</string> + <string name="stores_signout_go">Αποσύνδεση</string> + <string name="stores_search_catalog">Αναζήτηση στον κατάλογο %1$s…</string> + <string name="stores_search_installed">Αναζήτηση εγκατεστημένων παιχνιδιών…</string> + <string name="stores_search_library">Αναζήτηση στη βιβλιοθήκη σας…</string> + <string name="stores_search_open">Αναζήτηση</string> + <string name="stores_installed_empty">Τίποτα εγκατεστημένο</string> + <string name="stores_library_loading">Φόρτωση</string> + <string name="stores_nothing_matches">Δεν υπάρχουν παιχνίδια</string> + <string name="stores_shelves_loading">Φόρτωση του καταστήματος…</string> + <string name="stores_amazon_no_catalog">Δεν υπάρχει κατάλογος</string> + <string name="stores_shelf_new">Τι νέο υπάρχει</string> + <string name="stores_shelf_deals">Προσφορές</string> + <string name="stores_shelf_free">Δωρεάν παιχνίδια</string> + <string name="stores_shelf_trending">Σε άνοδο</string> + <string name="stores_shelf_library">Η βιβλιοθήκη σας</string> + <string name="stores_play">Αναπαραγωγή</string> + <string name="stores_install">Εγκατάσταση</string> + <string name="stores_resume_install">Συνέχιση εγκατάστασης</string> + <string name="stores_dl_interrupted">Διακόπηκε</string> + <string name="stores_install_size">Εγκατάσταση · %1$s</string> + <string name="stores_get_free">Απόκτηση δωρεάν</string> + <string name="stores_view_on">Προβολή στο %1$s</string> + <string name="stores_buy_price">Αγορά · %1$s</string> + <string name="stores_installed_chip">Εγκατεστημένο</string> + <string name="stores_in_library">Στη βιβλιοθήκη</string> + <string name="stores_free">Δωρεάν</string> + <string name="stores_resume">Συνέχιση</string> + <string name="stores_refresh">Ανανέωση</string> + <string name="stores_show_mature">Εμφάνιση περιεχομένου για ενήλικες</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Εκτός σύνδεσης</string> + <string name="epic_card_none">Ανενεργό</string> + <string name="epic_eos">Σύνδεση Epic (EOS)</string> + <string name="epic_offline">Εκκίνηση εκτός σύνδεσης</string> + <string name="epic_resolve">Επίλυση σύνδεσης Epic</string> + <string name="store_cloud_saves">Αποθηκεύσεις στο cloud</string> + <string name="cloud_upload">Μεταφόρτωση στο cloud</string> + <string name="cloud_download">Λήψη από το cloud</string> + <string name="cloud_none">Καμία αποθήκευση στο cloud</string> + <string name="cloud_conflict">Σύγκρουση cloud</string> + <string name="cloud_conflict_files">Σύγκρουση cloud · %1$d αρχεία</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Διατήρηση του cloud</string> + <string name="cloud_keep_local">Διατήρηση του τοπικού</string> + <string name="cloud_checking">Έλεγχος cloud…</string> + <string name="cloud_last_sync">Τελευταίος συγχρονισμός %1$s</string> + <string name="cloud_never">Δεν έχει συγχρονιστεί ακόμα</string> + <string name="cloud_done">%1$d αρχεία</string> + <string name="stores_eyebrow_installed">εγκατεστημένο</string> + <string name="stores_eyebrow_library">στη βιβλιοθήκη σας</string> + <string name="stores_eyebrow_store">κατάστημα</string> + <string name="achievements_title">Επιτεύγματα</string> + <string name="achievements_count">%1$d από %2$d ξεκλειδώθηκαν</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Ξεκλειδωμένα · %1$d</string> + <string name="achievements_group_locked">Κλειδωμένα · %1$d</string> + <string name="achievements_group_hidden">Κρυφά · %1$d</string> + <string name="achievements_hidden">Κρυφό επίτευγμα</string> + <string name="achievements_hidden_detail">Οι λεπτομέρειες εμφανίζονται μόλις ξεκλειδωθεί.</string> + <string name="achievements_all">Όλα τα επιτεύγματα ξεκλειδώθηκαν</string> + <string name="achievements_reading">Φόρτωση…</string> + <string name="achievements_signed_out">Συνδεθείτε στο %1$s για να δείτε τα επιτεύγματά σας.</string> + <string name="achievements_unreachable">Δεν ήταν δυνατή η σύνδεση με το %1$s. Δοκιμάστε ξανά όταν είστε συνδεδεμένοι.</string> + <string name="achievements_none">Το %1$s δεν αναφέρει επιτεύγματα για αυτό το παιχνίδι.</string> + <string name="achievements_show_all">Εμφάνιση όλων (%1$d)</string> + <string name="achievements_dialog_title">Επιτεύγματα · %1$s</string> + <string name="stores_in_steam">✓ Στο Steam</string> + <string name="stores_uninstall">Κατάργηση εγκατάστασης</string> + <string name="stores_uninstalling">Γίνεται κατάργηση εγκατάστασης…</string> + <string name="stores_uninstall_title">Κατάργηση εγκατάστασης του %1$s;</string> + <string name="stores_uninstall_body_size">Ελευθερώνει %1$s και το αφαιρεί από το Steam.</string> + <string name="stores_uninstall_body">Αφαιρεί τα αρχεία του και τη συντόμευσή του στο Steam.</string> + <string name="stores_launch_not_registered">Το Steam το εντοπίζει την επόμενη φορά που θα ξεκινήσει· εκκινήστε το από εκεί, ή ξεκινήστε το Steam τώρα.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Επιλογή του Steam ανά παιχνίδι</string> + <string name="stores_card_audio">Ήχος</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Στοιχεία</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Μηχανή</string> + <string name="stores_card_engine_value">%1$s · εγγενής</string> + <string name="stores_card_engine_java">%1$s · ενσωματωμένη</string> + <string name="stores_how_it_runs">Πώς εκτελείται</string> + <string name="stores_kv_fetched">Προέρχεται από</string> + <string name="stores_kv_fetched_value">%1$s, με τον λογαριασμό σας</string> + <string name="stores_kv_installed_to">Εγκαταστάθηκε σε</string> + <string name="stores_kv_installed_to_value">Αποθηκευτικός χώρος παιχνιδιών (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Εκκινείται από</string> + <string name="stores_kv_launched_by_value">Steam και Proton, η ίδια διαδρομή με τα παιχνίδια Steam</string> + <string name="stores_kv_launched_by_epic">Steam και Proton · σύνδεση Epic ανά εκκίνηση, επικάλυψη EOS ανενεργή</string> + <string name="stores_kv_launched_by_amazon">Steam και Proton · ορισμένες μεταβλητές Amazon Games Services</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">προστέθηκε στο Steam κατά την εγκατάσταση (Deck UI + Steam Input)</string> + <string name="stores_downloads_empty">Καμία λήψη</string> + <string name="stores_dl_paused">Σε παύση</string> + <string name="stores_dl_queued">Σε αναμονή</string> + <string name="stores_dl_queued_at">Σε αναμονή · #%1$d</string> + <string name="stores_dl_cancelled">Ακυρώθηκε</string> + <string name="stores_dl_failed">Αποτυχία</string> + <string name="stores_dl_failed_reason">Αποτυχία · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Παύση</string> + <string name="stores_dl_resume">Συνέχιση</string> + <string name="stores_dl_cancel">Ακύρωση</string> + <string name="stores_dl_retry">Επανάληψη</string> + <string name="stores_dl_clear">Εκκαθάριση</string> + <string name="stores_dl_clear_all">Εκκαθάριση ολοκληρωμένων</string> + <string name="stores_stage_manifest">Manifest</string> + <string name="stores_stage_download">Λήψη</string> + <string name="stores_stage_verify">Επαλήθευση</string> + <string name="stores_stage_install">Εγκατάσταση</string> + <string name="stores_stage_done">Τέλος</string> + <string name="stores_stage_checking">Έλεγχος</string> + <string name="stores_stage_downloading">Γίνεται λήψη</string> + <string name="stores_dl_cancel_confirm">Διαγραφή λήψης;</string> + <string name="stores_dl_failed_kept">Αποτυχία · το %1$s διατηρήθηκε</string> + <string name="stores_stage_verifying">Γίνεται επαλήθευση</string> + <string name="stores_stage_installing">Γίνεται εγκατάσταση</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d αρχεία</string> + <string name="stores_download_manager">Διαχείριση λήψεων</string> + <string name="stores_parallel">Ταυτόχρονες λήψεις</string> + <string name="stores_install_where">Εγκατάσταση σε</string> + <string name="stores_install_title">Εγκατάσταση %1$s:</string> + <string name="stores_target_internal">Εσωτερική</string> + <string name="stores_target_free">%1$s ελεύθερα</string> + <string name="stores_notification_channel">Λήψεις καταστήματος</string> + <string name="stores_notification_channel_hint">Εμφανίζει την πρόοδο λήψης παιχνιδιών και κρατά τις λήψεις ενεργές στο παρασκήνιο</string> + <string name="stores_notification_one">Γίνεται λήψη</string> + <string name="stores_notification_many">%1$d λήψεις</string> + <string name="stores_notification_waiting">Προετοιμασία…</string> + <string name="stores_notification_done_channel">Ολοκληρωμένες λήψεις καταστήματος</string> + <string name="stores_notification_installed">Το %1$s εγκαταστάθηκε</string> + <string name="stores_notification_failed">Η λήψη του %1$s απέτυχε</string> + <string name="stores_notification_delete">Διαγραφή</string> + <string name="stores_notification_keep">Διατήρηση</string> + <string name="stores_login_title">Σύνδεση στο %1$s</string> + <string name="stores_login_provider_title">Συνδεθείτε με τον πάροχό σας</string> + <string name="stores_login_reload">Επαναφόρτωση</string> + <string name="stores_login_close">Κλείσιμο</string> + <string name="stores_login_retry">Δοκιμάστε ξανά</string> + <string name="stores_login_finishing">Ολοκλήρωση σύνδεσης…</string> + <string name="stores_login_error_network">Δεν ήταν δυνατή η φόρτωση της σελίδας σύνδεσης: %1$s</string> + <string name="stores_login_error_idp_blocked">Αυτός ο πάροχος αρνείται να συνδεθεί από μια ενσωματωμένη σελίδα. Συνδεθείτε με τον κωδικό πρόσβασης του καταστήματός σας αντ\' αυτού.</string> + <string name="stores_login_error_http">Η σελίδα σύνδεσης απάντησε με σφάλμα (HTTP %1$d).</string> + <string name="stores_login_error_ssl">Η σύνδεση με τη σελίδα σύνδεσης δεν είναι ασφαλής.</string> + <string name="stores_login_error_verification">Δεν ήταν δυνατή η επαλήθευση της σύνδεσης. Δοκιμάστε ξανά.</string> + <string name="stores_login_error_generic">Η σύνδεση απέτυχε. Δοκιμάστε ξανά.</string> + <string name="common_close">Κλείσιμο</string> + <string name="common_back">Πίσω</string> + <string name="common_edit">Επεξεργασία</string> + <string name="games_add_all">Προσθήκη όλων των παιχνιδιών σε αυτόν τον φάκελο</string> + <string name="games_added_none">Τίποτα νέο σε αυτόν τον φάκελο</string> + <string name="edit_name">Όνομα</string> + <string name="edit_target">Προορισμός</string> + <string name="edit_start_in">Εκκίνηση σε</string> + <string name="edit_launch_options">Επιλογές εκκίνησης</string> + <string name="edit_artwork">Γραφικά</string> + <string name="edit_other_exe">Άλλο exe…</string> + <string name="edit_remove">Κατάργηση</string> + <string name="art_cover">Εξώφυλλο</string> + <string name="art_background">Φόντο</string> + <string name="art_logo">Λογότυπο</string> + <string name="art_icon">Εικονίδιο</string> + <string name="art_src_folder">Φάκελος παιχνιδιού</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">Το αρχείο σας</string> + <string name="art_src_exe">Από το exe</string> + <string name="art_src_auto">Αυτόματο</string> + <string name="art_pick_image">Επιλογή εικόνας…</string> + <string name="art_pick_title">Επιλέξτε μια εικόνα: %1$s</string> + <string name="art_reset">Επαναφορά στο αυτόματο</string> + <plurals name="games_added_n"> + <item quantity="one">%1$d παιχνίδι προστέθηκε</item> + <item quantity="other">%1$d παιχνίδια προστέθηκαν</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="one">%1$d ήδη στα Παιχνίδια</item> + <item quantity="other">%1$d ήδη στα Παιχνίδια</item> + </plurals> + <string name="hop_back">B · Πίσω στο %1$s</string> + <string name="hop_from_games">το παιχνίδι</string> + <string name="hop_from_play">Ρυθμίσεις αναπαραγωγής</string> + <string name="sheet_graphics_drivers">Προγράμματα οδήγησης γραφικών</string> + <string name="comp_this_proton">Αυτό το Proton</string> + <string name="comp_every_proton">Κάθε Proton</string> + <string name="comp_for_every_proton">Για κάθε Proton</string> + <string name="comp_available_count">Διαθέσιμα · %1$d</string> + <string name="comp_checked">Ελέγχθηκε %1$s</string> + <string name="comp_check_all">Έλεγχος για νέα στοιχεία και προγράμματα οδήγησης</string> + <string name="comp_get">Λήψη</string> + <string name="comp_proton_bar">%1$s · τα DXVK, VKD3D-Proton και FEX παρακάτω ανήκουν σε αυτό το Proton</string> + <string name="comp_device_tested">%1$s · δοκιμασμένο</string> + <string name="comp_device_untested">%1$s · μη δοκιμασμένο</string> + <string name="comp_device_unsupported">%1$s · μη υποστηριζόμενο</string> + <string name="gpu_auto_keeps">Κρατά το προτεινόμενο ζεύγος ενημερωμένο</string> + <string name="gpu_bar_auto">Αυτόματο · %1$s</string> + <string name="gpu_separately">Προγράμματα οδήγησης runtime και οθόνης ξεχωριστά</string> + <string name="gpu_use_title">Χρήση %1$s;</string> + <string name="proton_remove_title">Κατάργηση %1$s;</string> + <string name="setup_tab_diagnostics">Διαγνωστικά</string> + <string name="setup_ready">Έτοιμο για παιχνίδι</string> + <string name="setup_not_ready">Δεν είναι ακόμα έτοιμο για παιχνίδι</string> + <string name="setup_ready_runtime">runtime %1$s</string> + <string name="setup_ready_limit_off">όριο διαδικασιών ανενεργό</string> + <string name="setup_ready_signed_in">συνδεδεμένος ως %1$s</string> + <string name="setup_needs_you">Όταν κάτι χρειάζεται εσάς</string> + <string name="setup_group_look">Εμφάνιση</string> + <string name="setup_group_device">Αυτή η συσκευή</string> + <string name="setup_group_art">Γραφικά βιβλιοθήκης</string> + <string name="flathub_eyebrow">ΕΦΑΡΜΟΓΕΣ LINUX</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Εφαρμογές επιφάνειας εργασίας για την επιφάνεια εργασίας Linux, ανοίγουν από την Επιφάνεια εργασίας.</string> + <string name="flathub_setup">Ρύθμιση του Flathub</string> + <string name="flathub_remove_title">Κατάργηση %1$s;</string> + <string name="flathub_remove_note">Οι ρυθμίσεις και τα αρχεία του διατηρούνται.</string> + <string name="flathub_keep">Διατήρηση</string> + <string name="desktop_size">Μέγεθος στον δίσκο</string> + <string name="desktop_uninstall_title">Κατάργηση εγκατάστασης της επιφάνειας εργασίας;</string> + <string name="desktop_all_systems">Όλα</string> + <string name="gog_achievements">Επιτεύγματα GOG</string> + <string name="gog_achievements_hint">Συγχρονίζει επιτεύγματα για υποστηριζόμενα παιχνίδια GOG. Ισχύει από την επόμενη εκκίνηση.</string> + <string name="gog_achievements_off">Τα επιτεύγματα είναι απενεργοποιημένα</string> + <string name="gog_achievements_on">Τα επιτεύγματα είναι ενεργοποιημένα</string> + <string name="gog_achievements_signin">Συνδεθείτε ξανά στο GOG</string> + <string name="gog_achievements_uploaded">Τα επιτεύγματα ανέβηκαν</string> + <string name="gog_achievements_pending">Η μεταφόρτωση επιτεύγματος εκκρεμεί</string> + <string name="gog_achievements_enable_store">Ενεργοποιήστε τα επιτεύγματα GOG στις ρυθμίσεις καταστημάτων</string> + <string name="gog_achievements_unavailable">Τα επιτεύγματα δεν είναι διαθέσιμα· ελέγξτε τα αρχεία καταγραφής της συνεδρίας</string> + <string name="achievement_unlocked">Το επίτευγμα ξεκλειδώθηκε</string> + <string name="touch_paste">Επικόλληση</string> + <string name="touch_dpad">Σταυρός κατεύθυνσης</string> + <string name="touch_left_stick">Αριστερό στικ</string> + <string name="touch_right_stick">Δεξί στικ</string> + <string name="touch_menu">Μενού</string> + <string name="touch_keyboard">Πληκτρολόγιο</string> + <string name="touch_dpad_up">Σταυρός κατεύθυνσης πάνω</string> + <string name="touch_dpad_down">Σταυρός κατεύθυνσης κάτω</string> + <string name="touch_dpad_left">Σταυρός κατεύθυνσης αριστερά</string> + <string name="touch_dpad_right">Σταυρός κατεύθυνσης δεξιά</string> + <string name="touch_title">Χειριστήρια αφής</string> + <string name="touch_controller">Χειριστήριο αφής</string> + <string name="touch_mouse">Ποντίκι</string> + <string name="touch_on">Ενεργό</string> + <string name="touch_off">Ανενεργό</string> + <string name="touch_speed_slow">Αργή</string> + <string name="touch_speed_normal">Κανονική</string> + <string name="touch_speed_fast">Ταχεία</string> + <string name="touch_edit_layout">Επεξεργασία διάταξης</string> + <string name="touch_droiddeck_menu">Μενού DroidDeck</string> + <string name="touch_drag_hint">Σύρετε τα χειριστήρια στην οθόνη</string> + <string name="touch_arrange">Διάταξη</string> + <string name="touch_discard">Απόρριψη</string> + <string name="touch_saving">Αποθήκευση…</string> + <string name="touch_icon">Εικονίδιο</string> + <string name="touch_narrower">Π−</string> + <string name="touch_wider">Π+</string> + <string name="touch_shorter">Υ−</string> + <string name="touch_taller">Υ+</string> + <string name="touch_hide">Απόκρυψη</string> + <string name="touch_color_opacity">Χρώμα και διαφάνεια…</string> + <string name="touch_copy_layout">Αντιγραφή αυτής της διάταξης</string> + <string name="touch_layout_copied">Η διάταξη αντιγράφηκε· επικολλήστε την στη διάταξη αφής οποιουδήποτε παιχνιδιού</string> + <string name="touch_paste_layout">Επικόλληση αντιγραμμένης διάταξης</string> + <string name="touch_reset_layout">Επαναφορά στην προεπιλογή του Steam</string> + <string name="touch_no_binding">Αυτό το χειριστήριο δεν έχει αντιστοίχιση στο Steam για να του δοθεί εικονίδιο</string> + <string name="touch_icon_for">Εικονίδιο για %1$s</string> + <string name="touch_layout_color">Χρώμα διάταξης αφής</string> + <string name="touch_done">Τέλος</string> + <string name="touch_save_failed">Δεν ήταν δυνατή η αποθήκευση της διάταξης αφής</string> + <string name="touch_saved">Η διάταξη αφής αποθηκεύτηκε στο Steam</string> + <string name="touch_app_id">Εφαρμογή %1$d</string> + <string name="touch_label_hint">Ετικέτα (προαιρετικό)</string> + <string name="touch_icons_missing">Τα εικονίδια του Steam δεν βρέθηκαν</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d εικονίδιο από το Steam</item> + <item quantity="other">%1$d εικονίδια από το Steam</item> + </plurals> + <string name="touch_icon_color">Χρώμα εικονιδίου</string> + <string name="touch_button_color">Χρώμα κουμπιού</string> + <string name="touch_no_icon">Χωρίς εικονίδιο</string> + + <string name="stores_sync_sign_in">Έλεγχος σύνδεσης…</string> + <string name="stores_sync_library">Λήψη της βιβλιοθήκης σας…</string> + <string name="stores_sync_game_list">Λήψη λίστας παιχνιδιών…</string> + <string name="stores_sync_details">Φόρτωση στοιχείων παιχνιδιών… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">Συγχρονισμός %1$d παιχνιδιού…</item> + <item quantity="other">Συγχρονισμός %1$d παιχνιδιών…</item> + </plurals> + <string name="stores_sync_progress">Συγχρονισμός… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Λήψη εξωφύλλων…</string> + <string name="stores_sync_failed">Ο συγχρονισμός απέτυχε</string> + <string name="stores_sync_no_games">Δεν βρέθηκαν παιχνίδια στη βιβλιοθήκη σας στο %1$s</string> + <string name="stores_sync_amazon_no_answer">Το Amazon Games δεν απάντησε για τη βιβλιοθήκη σας: κάντε ανανέωση ή συνδεθείτε ξανά</string> + <string name="stores_sync_gog_unreachable">Δεν ήταν δυνατή η σύνδεση με το GOG</string> + <string name="stores_sync_gog_unreadable">Δεν ήταν δυνατή η ανάγνωση της βιβλιοθήκης σας στο GOG</string> + <string name="stores_registering">Εγγραφή στο Steam…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Λειτουργία Cardboard</string> + <string name="cardboard_mode_hint">Εικόνες για το αριστερό και το δεξί μάτι, για γυαλιά VR κινητού.</string> + <string name="cardboard_source">Εικόνα</string> + <string name="cardboard_source_screen">Εικονική οθόνη</string> + <string name="cardboard_source_sbs">3D δίπλα-δίπλα</string> + <string name="cardboard_viewer">Γυαλιά</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Χωρίς φακό</string> + <string name="cardboard_ipd">Απόσταση ματιών</string> + <string name="cardboard_screen_width">Πλάτος οθόνης</string> + <string name="cardboard_screen_distance">Απόσταση οθόνης</string> + <string name="cardboard_head_tracking">Παρακολούθηση κεφαλιού</string> + <string name="cardboard_no_sensor">Αυτή η συσκευή δεν έχει αισθητήρα περιστροφής.</string> + <string name="cardboard_recenter">Επανακεντράρισμα προβολής</string> + <string name="cardboard_recenter_button">Επανακεντράρισμα</string> + <string name="cardboard_steamvr">SteamVR (πειραματικό)</string> + <string name="cardboard_steamvr_install">Εγκατάσταση</string> + <string name="cardboard_steamvr_install_hint">Κατεβάζει το SteamVR ARM64 της Valve (571 MB).</string> + <string name="cardboard_steamvr_installing">Λήψη και αποσυμπίεση του SteamVR…</string> + <string name="cardboard_steamvr_start">Έναρξη</string> + <string name="cardboard_steamvr_ready_hint">Εκτελεί το SteamVR με το κεφάλι σας ως headset· η εικόνα αλλάζει σε 3D δίπλα-δίπλα.</string> + <string name="cardboard_steamvr_stop">Διακοπή</string> + <string name="cardboard_steamvr_running_hint">Το SteamVR εκτελείται.</string> + <string name="cardboard_steamvr_unavailable">Δεν είναι διαθέσιμο σε αυτή τη συνεδρία.</string> + <string name="cardboard_steamvr_failed">Το SteamVR δεν αποκρίθηκε· ελέγξτε το αρχείο καταγραφής της συνεδρίας.</string> + <string name="drawer_component_downloads">Λήψη στοιχείων</string> + <string name="drawer_component_downloads_hint">Οι λήψεις είναι κοινές με τη σελίδα Στοιχεία. Επιλέξτε παραπάνω μια έκδοση που έχει ληφθεί για να χρησιμοποιηθεί στην επόμενη εκκίνηση παιχνιδιού.</string> + <string name="drawer_component_choose_download">Επιλέξτε έκδοση για λήψη</string> + <string name="stores_fact_developer">Δημιουργός</string> + <string name="stores_fact_publisher">Εκδότης</string> + <string name="stores_fact_released">Κυκλοφορία</string> + <string name="stores_hltb_main">Κύρια ιστορία</string> + <string name="stores_hltb_extras">+ Επιπλέον</string> + <string name="stores_hltb_completionist">Ολοκλήρωση</string> + <string name="stores_hltb_hours">%1$s ώ.</string> + <string name="stores_hltb_minutes">%1$d λεπ.</string> +</resources> diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 64ed71fea..2d137c2b5 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -15,8 +15,10 @@ <string name="session_channel">Sesión</string> <string name="session_channel_description">Se muestra mientras hay una sesión de Steam o de escritorio abierta</string> <string name="session_notification">Steam se está ejecutando</string> + <string name="session_notification_downloads">Descarga de Steam en curso</string> <string name="session_notification_paused">Sesión en pausa - toca Reanudar cuando quieras</string> <string name="resume_session">Reanudar</string> + <string name="suspend_now">Suspender ahora</string> <string name="stop_session">Detener sesión</string> <string name="frame_gen_title">Generación de fotogramas</string> <string name="frame_gen_hint">Elige el modo de generación de fotogramas</string> @@ -182,6 +184,8 @@ <string name="add_app_flatpak_id">ID de app</string> <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> <string name="add_app_flatpak_invalid">No es un ID de app de Flathub</string> + <string name="add_app_flatpak_bundle_hint">O un archivo .flatpak, para una app que no esté en Flathub.</string> + <string name="add_app_pick_flatpak">Elige un archivo .flatpak</string> <string name="add_app_source">Origen</string> <string name="add_app_appearance">En tu Escritorio</string> <string name="add_app_preview_name">Nueva app</string> @@ -251,6 +255,10 @@ <string name="mode_osc">Controles en pantalla</string> <string name="mode_osc_qam">Steam + QAM</string> <string name="mode_osc_note">Auto muestra los controles cuando no hay ningún mando conectado.</string> + <string name="osc_steam_touch">Táctil de Steam (beta)</string> + <string name="osc_style_next_session">Los controles táctiles de Steam se activan en la próxima sesión de Steam</string> + <string name="osc_steam_layout">Disposición táctil de Steam</string> + <string name="osc_steam_layout_hint">Mueve, cambia de tamaño u oculta los controles táctiles de este juego; se guarda en Steam</string> <string name="mode_controller">Mando</string> <string name="mode_controller_hint">Cómo ve Steam tu mando. Se aplica en la próxima sesión.</string> <string name="mode_controller_deck">Mando de Steam Deck</string> @@ -268,6 +276,8 @@ <string name="mode_suspend_manual">Manual</string> <string name="mode_suspend_native">Nativo</string> <string name="mode_suspend_steam_note">Auto pausa y reanuda al mostrarse. Nativo prepara Steam para la suspensión primero. Manual espera a Reanudar. Nunca sigue ejecutándose.</string> + <string name="mode_background_downloads">Mantener las descargas activas</string> + <string name="mode_background_downloads_hint">Retrasa la suspensión en segundo plano mientras Steam esté descargando.</string> <string name="mode_suspend_note">Auto pausa en segundo plano y reanuda al mostrarse. Manual espera a Reanudar. Nunca sigue ejecutándose.</string> <string name="mode_startup">Inicio</string> <string name="mode_steam_startup">Iniciar Steam con DroidDeck</string> @@ -333,6 +343,7 @@ <string name="wincomp_found_shipped">El juego tiene %1$s (suele funcionar tal cual)</string> <string name="wincomp_already_here">Ya está en este juego</string> <string name="wincomp_no_recommendation">Nada en la carpeta del juego apunta a un componente.</string> + <string name="wincomp_active">Instalados para este juego (%1$d)</string> <string name="wincomp_all">Todos los componentes (%1$d)</string> <string name="wincomp_installers">Necesitan un instalador de Windows (%1$d)</string> <string name="wincomp_installers_note">Estos ejecutan un instalador real dentro del prefijo (.NET, PhysX, XNA, los instaladores completos de Visual C++…). DroidDeck aún no puede hacerlo; es lo siguiente.</string> @@ -343,6 +354,9 @@ <string name="wincomp_loading">Cargando los componentes…</string> <string name="wincomp_failed">No se pudo instalar: %1$s</string> <string name="wincomp_downloaded">Descargado</string> + <string name="wincomp_downloading">Descargando %1$s</string> + <string name="wincomp_installing">Instalando: %1$s</string> + <string name="wincomp_reverting">Desactivando…</string> <string name="wincomp_download_size">Descarga de %1$s</string> <string name="wincomp_next_launch">Se aplica la próxima vez que se inicie el juego.</string> <string name="mode_stretch169">Estirar juegos 16:9</string> @@ -401,6 +415,44 @@ <string name="comp_check_nightlies">Buscar paquetes nuevos en las Nightlies</string> <string name="comp_proton">Proton</string> <string name="comp_proton_note">El Proton elegido aquí ejecuta todos los juegos que no tengan uno propio en Steam, y un cambio hecho en Steam también se ve aquí. Steam reemplaza el Proton de Valve al actualizarse; los originales se conservan por versión.</string> + <string name="comp_scope">Configuración</string> + <string name="comp_profile_configure">Configurar perfil</string> + <string name="comp_profile">Perfil</string> + <string name="comp_scope_general">Perfil predeterminado</string> + <string name="comp_scope_general_detail">Lo usan automáticamente los juegos sin ajustes propios.</string> + <string name="comp_profile_game_detail">Ajustes guardados que se usan automáticamente al iniciar %1$s.</string> + <string name="comp_game_hint">Elige solo los ajustes que necesita este juego. Todo lo demás sigue usando el perfil predeterminado.</string> + <string name="comp_game_fex">Preajuste de FEX</string> + <string name="comp_game_fex_inherited">Usa el preajuste del perfil predeterminado.</string> + <string name="comp_game_fex_override">Preajuste personalizado guardado para este juego.</string> + <string name="comp_game_fex_custom">Variables de FEX personalizadas</string> + <string name="comp_game_use_general">Usar predeterminado</string> + <string name="comp_game_compatibility">Compatibilidad del juego</string> + <string name="comp_game_environment_hint">Opciones de entorno de Proton, DXVK, VKD3D y sincronización para este juego.</string> + <string name="comp_game_proton_inherited">Usa el Proton del perfil predeterminado.</string> + <string name="comp_game_proton_override">Proton personalizado guardado para este juego mediante la asignación de compatibilidad de Steam.</string> + <string name="comp_no_proton_short">Sin Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Usa el paquete del perfil predeterminado de %1$s, salvo que este juego elija otro paquete.</string> + <string name="comp_game_component_general">Usa el paquete de componentes del perfil predeterminado.</string> + <string name="comp_game_component_override">Usa %1$s solo para este juego.</string> + <string name="comp_game_runtime_options">Opciones del runtime</string> + <string name="comp_game_environment_other_hint">Otras opciones de entorno de Proton, DXVK, VKD3D-Proton y sincronización para cada juego.</string> + <string name="comp_game_saved_next">El perfil de %1$s se ha guardado para el próximo inicio del juego.</string> + <string name="game_no_steam_title">Iniciar sin Steam</string> + <string name="game_no_steam_detail">Para un juego que se cierra enseguida: lo inicia sin la configuración de Steam de Proton. Las funciones de Steam, como la superposición, están desactivadas para él.</string> + <string name="game_no_steam_on">Activado</string> + <string name="comp_game_proton_live">La elección de Proton de %1$s se ha guardado y ya está disponible en Steam.</string> + <string name="comp_game_proton_next">La elección de Proton de %1$s se ha guardado. Reinicia Steam antes de iniciar el juego.</string> + <string name="comp_game_proton_not_runnable">%1$s no puede usar el Proton elegido en este dispositivo.</string> + <string name="comp_game_load_failed">No se pudo cargar el perfil de %1$s: %2$s</string> + <string name="comp_game_save_failed">No se pudo guardar el perfil del juego: %1$s</string> + <string name="comp_game_reset">Restablecer perfil del juego</string> + <string name="comp_game_reset_title">¿Restablecer el perfil de %1$s?</string> + <string name="comp_game_reset_body">¿Quitar los ajustes propios de este juego para Proton, FEX, paquete de componentes, entorno, filtrado de texturas y componentes de Windows? Usará el perfil predeterminado.</string> + <string name="comp_game_reset_done">%1$s ahora usa el perfil predeterminado.</string> + <string name="comp_game_reset_failed">No se pudo restablecer del todo el perfil del juego: %1$s</string> <string name="comp_default_live">%1$s ahora ejecuta todos los juegos que no tengan su propio Proton en Steam.</string> <string name="comp_default_next">%1$s ejecutará todos los juegos que no tengan su propio Proton a partir del próximo inicio de Steam.</string> <string name="comp_default_not_runnable">%1$s no puede iniciar juegos aquí, así que no puede ser el predeterminado.</string> @@ -564,6 +616,20 @@ <string name="drawer_stop_title">¿Detener la sesión?</string> <string name="drawer_running_now">En ejecución</string> <string name="drawer_no_game">Ningún juego se ejecuta con un Proton</string> + <string name="drawer_selected_profile">Perfil del juego</string> + <string name="drawer_selected_profile_none">Los detalles del perfil aún no están disponibles.</string> + <string name="drawer_profile_refresh_failed_initial">No se pudieron cargar los perfiles de juego. Se reintentará automáticamente.</string> + <string name="drawer_profile_refresh_failed_stale">No se pudieron actualizar los perfiles de juego. Se muestran los últimos datos disponibles; se reintentará automáticamente.</string> + <string name="drawer_profile_inspect_hint">Consulta y edita los ajustes guardados que se usarán en el próximo inicio.</string> + <string name="drawer_profile_default">Usa el perfil predeterminado</string> + <string name="drawer_profile_custom">Perfil personalizado</string> + <string name="drawer_component_inherited">Usa %1$s del perfil predeterminado.</string> + <string name="drawer_game_components_note">Los cambios se guardan en el perfil de %1$s y se aplican la próxima vez que se inicie el juego.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d ajuste propio del juego</item> + <item quantity="many">%1$d ajustes propios del juego</item> + <item quantity="other">%1$d ajustes propios del juego</item> + </plurals> <string name="drawer_no_proton">Aún no hay ningún Proton instalado.</string> <string name="drawer_game_running">juego en ejecución</string> <string name="drawer_reapplied">reaplicado al iniciar %1$s</string> @@ -620,6 +686,14 @@ <string name="setup_saved_logs">Registros de sesión guardados</string> <string name="setup_saved_logs_hint">Conserva los %1$d más recientes</string> <string name="setup_clear_logs">Borrar registros</string> + <string name="setup_reset_settings">Restablecer todos los ajustes</string> + <string name="setup_reset_settings_hint">Devuelve cada ajuste a su valor predeterminado</string> + <string name="setup_reset_settings_button">Restablecer</string> + <string name="setup_reset_settings_title">¿Restablecer todos los ajustes?</string> + <string name="setup_reset_settings_body">Los ajustes de pantalla, rendimiento, mando, tema, idioma y lanzador vuelven a sus valores predeterminados. Se conservan tus juegos, la sesión de Steam, las cuentas de tiendas, los controladores descargados y las carpetas de almacenamiento.</string> + <string name="setup_reset_settings_profiles">Restablecer también los perfiles de juego (Proton, FEX, componentes y entorno de cada juego, y el entorno del perfil predeterminado)</string> + <string name="setup_reset_settings_session_running">Detén la sesión antes de restablecer los ajustes.</string> + <string name="setup_reset_settings_partial">Los ajustes se restablecieron, pero algunas partes no se pudieron restablecer. Los registros tienen los detalles.</string> <string name="setup_offline">Modo sin conexión</string> <string name="setup_sign_in_first">Inicia sesión en Steam primero</string> <string name="setup_launcher">Launcher</string> @@ -635,6 +709,10 @@ <string name="setup_home">Usar como pantalla de inicio</string> <string name="setup_home_on">DroidDeck puede ser la app de inicio</string> <string name="setup_home_off">DroidDeck nunca se ofrece como app de inicio</string> + <string name="setup_orientation">Orientación</string> + <string name="setup_orientation_auto">Horizontal automática</string> + <string name="setup_orientation_landscape">Horizontal</string> + <string name="setup_orientation_reverse">Horizontal invertida</string> <string name="setup_fullscreen">Pantalla completa</string> <string name="setup_fullscreen_on">Oculta las barras de estado y navegación de Android</string> <string name="setup_fullscreen_off">Muestra las barras de estado y navegación de Android</string> @@ -898,6 +976,12 @@ <string name="content_page_gone">Esa página ya no existe. Elige una sección a la izquierda.</string> <string name="content_open_desktop">Abrir escritorio</string> <string name="content_install_open_desktop">Instalar y abrir escritorio</string> + <string name="desktop_remove_title">Desinstalar escritorio</string> + <string name="desktop_remove_text">Elimina el escritorio y sus apps incluidas. Se conservan los juegos, las apps añadidas, los ajustes y las bibliotecas compartidas.</string> + <string name="desktop_removing">Eliminando el escritorio</string> + <string name="desktop_removed">Escritorio eliminado</string> + <string name="desktop_cleaning_legacy">Eliminando archivos antiguos de LXQt</string> + <string name="desktop_removal_unknown">Actualiza DroidDeck antes de eliminar esta versión del escritorio.</string> <string name="content_linux_desktop">Escritorio de Linux</string> <string name="content_linux_desktop_hint">KDE Plasma, Firefox y tus emuladores</string> <string name="games_empty">Los juegos que instales en Steam aparecen aquí después de la sesión.</string> @@ -927,6 +1011,8 @@ <string name="games_last_played">Última partida %1$s</string> <string name="games_internal">Almacenamiento interno</string> <string name="games_added">Juego añadido</string> + <string name="games_custom">Juego personalizado</string> + <string name="games_store_storage">Almacenamiento de juegos</string> <string name="gpu_auto_hint">El mejor par para esta GPU, siempre actualizado</string> <string name="gpu_manual_hint">Elige un par manualmente</string> <string name="gpu_checking">Buscando controladores…</string> @@ -968,6 +1054,12 @@ <string name="deck_tab_left">Pad izquierdo</string> <string name="deck_tab_right">Pad derecho</string> <string name="deck_tab_both">Ambos</string> + <string name="deck_tab_gamepad">Mando</string> + <string name="deck_surface_stick">Stick</string> + <string name="deck_surface_trackpad">Trackpad</string> + <string name="deck_surface_switch">Cambiar entre stick y trackpad</string> + <string name="deck_more_controls">Más controles de Deck</string> + <string name="deck_panels">PANELES</string> <string name="deck_close">Cerrar controles de la segunda pantalla</string> <string name="deck_open_steam_menu">Abrir menú de Steam</string> <string name="deck_left_upper">Izquierdo · superior</string> @@ -1003,6 +1095,10 @@ <string name="proton_installed">Instalada %1$s</string> <string name="proton_queued">Pendiente de una solicitud anterior</string> <string name="proton_not_installed">No instalada</string> + <string name="proton_update_available">Actualización disponible → %1$s</string> + <string name="proton_remove_older">Quitar versiones antiguas de %1$s</string> + <string name="proton_remove_older_body">Elimina %1$s. Los juegos configurados con estas versiones deberán cambiarse a otro Proton.</string> + <string name="proton_remove_body">Los juegos configurados con esta versión deberán cambiarse a otro Proton.</string> <string name="proton_install_now">Instalar ahora</string> <string name="proton_cancel_queued">Cancelar en cola</string> <string name="pckb_android">Teclado de Android</string> @@ -1031,6 +1127,8 @@ <string name="game_env_eyebrow">Juegos de Proton</string> <string name="pip_title">Imagen en imagen</string> <string name="pip_auto">Entrar en PiP automáticamente</string> + <string name="unfolded_controls">Controles con la pantalla desplegada</string> + <string name="unfolded_controls_hint">Desplegado, el juego queda encima de los trackpads y los agarres traseros de Steam Deck. Plegado, el juego ocupa toda la pantalla.</string> <string name="pip_suspended">Suspendida</string> <string name="pip_not_ready">Imagen en imagen estará disponible cuando la sesión esté lista.</string> <string name="pip_permission">Permite imagen en imagen para DroidDeck en los ajustes de Android.</string> @@ -1056,19 +1154,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> @@ -1080,21 +1171,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> @@ -1109,78 +1192,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> @@ -1188,19 +1248,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> @@ -1215,11 +1269,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> @@ -1238,39 +1288,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> @@ -1285,35 +1321,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> @@ -1326,28 +1349,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> @@ -1356,8 +1365,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> @@ -1375,27 +1382,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> @@ -1413,8 +1410,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> @@ -1422,8 +1417,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> @@ -1437,19 +1430,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> @@ -1461,8 +1446,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> @@ -1473,8 +1456,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> @@ -1482,15 +1463,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> @@ -1506,8 +1483,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> @@ -1522,15 +1497,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"> @@ -1538,37 +1507,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> @@ -1577,8 +1532,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> @@ -1589,34 +1542,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> @@ -1633,75 +1573,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> @@ -1737,27 +1649,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> @@ -1765,24 +1663,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> @@ -1796,31 +1684,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> @@ -1828,19 +1707,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> @@ -1857,55 +1728,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> @@ -1930,29 +1784,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> @@ -1963,115 +1811,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> @@ -2089,6 +1883,7 @@ <string name="display_frame_rate">Velocidad de fotogramas</string> <string name="display_look_filter">%1$s Cambia el escalado a %2$s.</string> <string name="display_match_screen">Igual a la pantalla (%1$d×%2$d)</string> + <string name="display_follow_screen">Seguir la pantalla (cambia de tamaño al plegar o desplegar)</string> <string name="mode_tab_steam">Steam</string> <string name="device_info_show">Mostrar detalles del dispositivo</string> <string name="device_info_hide">Ocultar detalles del dispositivo</string> @@ -2098,6 +1893,186 @@ <string name="device_info_copied">Copiado.</string> <string name="force_ssbs_title">Forzar SSBS</string> <string name="force_ssbs_hint">Mantiene activado un ajuste de velocidad de la CPU que Wine sigue desactivando. Puede hacer más rápidos los juegos de Windows en Snapdragon 8 Elite; poco cambio en chips anteriores. Se aplica en el próximo inicio de un juego.</string> + + <string name="rail_stores">Tiendas</string> + <string name="source_steam">Steam</string> + <string name="source_added">Personalizados</string> + <string name="setup_stores">Tiendas</string> + <string name="setup_stores_open_on">Abrir una tienda en</string> + <string name="setup_stores_speed">Nivel de velocidad de descarga</string> + <string name="setup_stores_engine">Motor de las tiendas</string> + <string name="setup_stores_engine_chip_ready">● Motor listo</string> + <string name="setup_stores_engine_chip_missing">Sin compilar</string> + <string name="stores_tier_balanced">Equilibrado</string> + <string name="stores_tier_fast">Rápido</string> + <string name="stores_tier_max">Máximo</string> + <string name="stores_title">Tiendas</string> + <string name="stores_settings">Ajustes de las tiendas</string> + <string name="stores_settings_eyebrow">Ajustes</string> + <string name="stores_chip_signed_out">(sin iniciar sesión)</string> + <string name="stores_tab_store">Tienda</string> + <string name="stores_tab_installed">Instalados</string> + <string name="stores_tab_library">Biblioteca</string> + <string name="stores_tab_all">Todos</string> + <string name="stores_tab_downloads">Descargas</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Iniciar sesión</string> + <string name="stores_signout_named">%1$s · cerrar sesión</string> + <string name="stores_signout_title">¿Cerrar sesión en %1$s?</string> + <string name="stores_signout_note">Los juegos instalados se conservan. La biblioteca se vuelve a cargar cuando inicias sesión de nuevo.</string> + <string name="stores_signout_stay">Mantener la sesión</string> + <string name="stores_signout_go">Cerrar sesión</string> + <string name="stores_search_catalog">Buscar en el catálogo de %1$s…</string> + <string name="stores_search_installed">Buscar juegos instalados…</string> + <string name="stores_search_library">Buscar en tu biblioteca…</string> + <string name="stores_search_open">Buscar</string> + <string name="stores_installed_empty">Nada instalado</string> + <string name="stores_library_loading">Cargando</string> + <string name="stores_nothing_matches">No hay juegos</string> + <string name="stores_shelves_loading">Cargando la tienda…</string> + <string name="stores_amazon_no_catalog">Sin catálogo</string> + <string name="stores_shelf_new">Novedades</string> + <string name="stores_shelf_deals">Ofertas</string> + <string name="stores_shelf_free">Juegos gratis</string> + <string name="stores_shelf_trending">En tendencia</string> + <string name="stores_shelf_library">Tu biblioteca</string> + <string name="stores_play">Jugar</string> + <string name="stores_install">Instalar</string> + <string name="stores_resume_install">Reanudar instalación</string> + <string name="stores_dl_interrupted">Interrumpida</string> + <string name="stores_install_size">Instalar · %1$s</string> + <string name="stores_get_free">Obtener gratis</string> + <string name="stores_view_on">Ver en %1$s</string> + <string name="stores_buy_price">Comprar · %1$s</string> + <string name="stores_installed_chip">Instalado</string> + <string name="stores_in_library">En la biblioteca</string> + <string name="stores_free">Gratis</string> + <string name="stores_resume">Reanudar</string> + <string name="stores_refresh">Actualizar</string> + <string name="stores_show_mature">Mostrar contenido para adultos</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Sin conexión</string> + <string name="epic_card_none">Desactivado</string> + <string name="epic_eos">Inicio de sesión de Epic (EOS)</string> + <string name="epic_offline">Iniciar sin conexión</string> + <string name="epic_resolve">Solucionar el inicio de sesión de Epic</string> + <string name="store_cloud_saves">Partidas en la nube</string> + <string name="cloud_upload">Subir a la nube</string> + <string name="cloud_download">Descargar de la nube</string> + <string name="cloud_none">No hay partidas en la nube</string> + <string name="cloud_conflict">Conflicto en la nube</string> + <string name="cloud_conflict_files">Conflicto en la nube · %1$d archivos</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Conservar la de la nube</string> + <string name="cloud_keep_local">Conservar la local</string> + <string name="cloud_checking">Comprobando la nube…</string> + <string name="cloud_last_sync">Última sincronización %1$s</string> + <string name="cloud_never">Aún sin sincronizar</string> + <string name="cloud_done">%1$d archivos</string> + <string name="stores_eyebrow_installed">instalado</string> + <string name="stores_eyebrow_library">en tu biblioteca</string> + <string name="stores_eyebrow_store">tienda</string> + <string name="achievements_title">Logros</string> + <string name="achievements_count">%1$d de %2$d desbloqueados</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Desbloqueados · %1$d</string> + <string name="achievements_group_locked">Bloqueados · %1$d</string> + <string name="achievements_group_hidden">Ocultos · %1$d</string> + <string name="achievements_hidden">Logro oculto</string> + <string name="achievements_hidden_detail">Los detalles se muestran al desbloquearlo.</string> + <string name="achievements_all">Todos los logros desbloqueados</string> + <string name="achievements_reading">Leyendo…</string> + <string name="achievements_signed_out">Inicia sesión en %1$s para ver tus logros.</string> + <string name="achievements_unreachable">No se pudo conectar con %1$s. Vuelve a intentarlo cuando tengas conexión.</string> + <string name="achievements_none">%1$s no indica logros para este juego.</string> + <string name="achievements_show_all">Ver los %1$d</string> + <string name="achievements_dialog_title">Logros · %1$s</string> + <string name="stores_in_steam">✓ En Steam</string> + <string name="stores_uninstall">Desinstalar</string> + <string name="stores_uninstalling">Desinstalando…</string> + <string name="stores_uninstall_title">¿Desinstalar %1$s?</string> + <string name="stores_uninstall_body_size">Libera %1$s y lo quita de Steam.</string> + <string name="stores_uninstall_body">Elimina sus archivos y su acceso directo de Steam.</string> + <string name="stores_launch_not_registered">Steam detectará el juego la próxima vez que se inicie; ábrelo desde allí o inicia Steam ahora.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Lo elige Steam para cada juego</string> + <string name="stores_card_audio">Audio</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Componentes</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Motor</string> + <string name="stores_card_engine_value">%1$s · nativo</string> + <string name="stores_card_engine_java">%1$s · integrado</string> + <string name="stores_how_it_runs">Cómo se ejecuta</string> + <string name="stores_kv_fetched">Obtenido de</string> + <string name="stores_kv_fetched_value">%1$s, con tu cuenta</string> + <string name="stores_kv_installed_to">Instalado en</string> + <string name="stores_kv_installed_to_value">Almacenamiento de juegos (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Se inicia con</string> + <string name="stores_kv_launched_by_value">Steam y Proton, igual que los juegos de Steam</string> + <string name="stores_kv_launched_by_epic">Steam y Proton · inicio de sesión de Epic en cada arranque, superposición de EOS desactivada</string> + <string name="stores_kv_launched_by_amazon">Steam y Proton · variables de Amazon Games Services definidas</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">se añade a Steam al instalar (interfaz Deck + Steam Input)</string> + <string name="stores_downloads_empty">No hay descargas</string> + <string name="stores_dl_paused">En pausa</string> + <string name="stores_dl_queued">En cola</string> + <string name="stores_dl_queued_at">En cola · #%1$d</string> + <string name="stores_dl_cancelled">Cancelada</string> + <string name="stores_dl_failed">Fallida</string> + <string name="stores_dl_failed_reason">Fallida · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Pausar</string> + <string name="stores_dl_resume">Reanudar</string> + <string name="stores_dl_cancel">Cancelar</string> + <string name="stores_dl_retry">Reintentar</string> + <string name="stores_dl_clear">Quitar</string> + <string name="stores_dl_clear_all">Quitar terminadas</string> + <string name="stores_stage_manifest">Manifiesto</string> + <string name="stores_stage_download">Descarga</string> + <string name="stores_stage_verify">Verificación</string> + <string name="stores_stage_install">Instalación</string> + <string name="stores_stage_done">Hecho</string> + <string name="stores_stage_checking">Comprobando</string> + <string name="stores_stage_downloading">Descargando</string> + <string name="stores_dl_cancel_confirm">¿Eliminar la descarga?</string> + <string name="stores_dl_failed_kept">Fallida · se conservan %1$s</string> + <string name="stores_stage_verifying">Verificando</string> + <string name="stores_stage_installing">Instalando</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d archivos</string> + <string name="stores_download_manager">Gestor de descargas</string> + <string name="stores_parallel">Descargas simultáneas</string> + <string name="stores_install_where">Instalar en</string> + <string name="stores_install_title">Instalar %1$s:</string> + <string name="stores_target_internal">Interno</string> + <string name="stores_target_free">%1$s libres</string> + <string name="stores_notification_channel">Descargas de la tienda</string> + <string name="stores_notification_channel_hint">Muestra el progreso de las descargas de juegos y las mantiene activas en segundo plano</string> + <string name="stores_notification_one">Descargando</string> + <string name="stores_notification_many">%1$d descargas</string> + <string name="stores_notification_waiting">Preparando…</string> + <string name="stores_notification_done_channel">Descargas de la tienda terminadas</string> + <string name="stores_notification_installed">%1$s instalado</string> + <string name="stores_notification_failed">Falló la descarga de %1$s</string> + <string name="stores_notification_delete">Eliminar</string> + <string name="stores_notification_keep">Conservar</string> + <string name="stores_login_title">Iniciar sesión en %1$s</string> + <string name="stores_login_provider_title">Inicia sesión con tu proveedor</string> + <string name="stores_login_reload">Recargar</string> + <string name="stores_login_close">Cerrar</string> + <string name="stores_login_retry">Reintentar</string> + <string name="stores_login_finishing">Terminando de iniciar sesión…</string> + <string name="stores_login_error_network">No se pudo cargar la página de inicio de sesión: %1$s</string> + <string name="stores_login_error_idp_blocked">Este proveedor no permite iniciar sesión desde una página integrada. Inicia sesión con la contraseña de la tienda.</string> + <string name="stores_login_error_http">La página de inicio de sesión respondió con un error (HTTP %1$d).</string> + <string name="stores_login_error_ssl">La conexión con la página de inicio de sesión no es segura.</string> + <string name="stores_login_error_verification">No se pudo verificar el inicio de sesión. Inténtalo de nuevo.</string> + <string name="stores_login_error_generic">No se pudo iniciar sesión. Inténtalo de nuevo.</string> <string name="common_close">Cerrar</string> <string name="common_back">Atrás</string> <string name="common_edit">Editar</string> @@ -2133,21 +2108,126 @@ <item quantity="many">%1$d ya en Juegos</item> <item quantity="other">%1$d ya en Juegos</item> </plurals> - <string name="achievements_title">Logros</string> - <string name="achievements_count">%1$d de %2$d desbloqueados</string> - <string name="achievements_count_short">%1$d / %2$d</string> - <string name="achievements_group_unlocked">Desbloqueados · %1$d</string> - <string name="achievements_group_locked">Bloqueados · %1$d</string> - <string name="achievements_group_hidden">Ocultos · %1$d</string> - <string name="achievements_hidden">Logro oculto</string> - <string name="achievements_hidden_detail">Los detalles se muestran al desbloquearlo.</string> - <string name="achievements_all">Todos los logros desbloqueados</string> - <string name="achievements_reading">Leyendo…</string> - <string name="achievements_signed_out">Inicia sesión en %1$s para ver tus logros.</string> - <string name="achievements_unreachable">No se pudo conectar con %1$s. Vuelve a intentarlo cuando tengas conexión.</string> - <string name="achievements_none">%1$s no indica logros para este juego.</string> - <string name="achievements_show_all">Ver los %1$d</string> - <string name="achievements_dialog_title">Logros · %1$s</string> + <string name="hop_back">B · Volver a %1$s</string> + <string name="hop_from_games">la página del juego</string> + <string name="hop_from_play">los ajustes de Jugar</string> + <string name="sheet_graphics_drivers">Controladores gráficos</string> + <string name="comp_this_proton">Este Proton</string> + <string name="comp_every_proton">Todos los Protons</string> + <string name="comp_for_every_proton">Para todos los Protons</string> + <string name="comp_available_count">Disponibles · %1$d</string> + <string name="comp_checked">Comprobado %1$s</string> + <string name="comp_check_all">Buscar componentes y controladores nuevos</string> + <string name="comp_get">Obtener</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton y FEX de abajo son de este Proton</string> + <string name="comp_device_tested">%1$s · probado</string> + <string name="comp_device_untested">%1$s · sin probar</string> + <string name="comp_device_unsupported">%1$s · no compatible</string> + <string name="gpu_auto_keeps">Mantiene actualizado el par recomendado</string> + <string name="gpu_bar_auto">Auto · %1$s</string> + <string name="gpu_separately">Controladores de runtime y de pantalla por separado</string> + <string name="gpu_use_title">¿Usar %1$s?</string> + <string name="proton_remove_title">¿Quitar %1$s?</string> + <string name="setup_tab_diagnostics">Diagnóstico</string> + <string name="setup_ready">Listo para jugar</string> + <string name="setup_not_ready">Aún no está listo para jugar</string> + <string name="setup_ready_runtime">runtime %1$s</string> + <string name="setup_ready_limit_off">límite de procesos desactivado</string> + <string name="setup_ready_signed_in">sesión iniciada como %1$s</string> + <string name="setup_needs_you">Cuando algo requiera tu atención</string> + <string name="setup_group_look">Aspecto</string> + <string name="setup_group_device">Este dispositivo</string> + <string name="setup_group_art">Ilustraciones de la biblioteca</string> + <string name="flathub_eyebrow">APPS DE LINUX</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Apps para el escritorio de Linux, que se abren desde Escritorio.</string> + <string name="flathub_setup">Configurar Flathub</string> + <string name="flathub_remove_title">¿Quitar %1$s?</string> + <string name="flathub_remove_note">Se conservan sus ajustes y archivos.</string> + <string name="flathub_keep">Conservar</string> + <string name="desktop_size">Tamaño en disco</string> + <string name="desktop_uninstall_title">¿Desinstalar el escritorio?</string> + <string name="desktop_all_systems">Todos</string> + <string name="gog_achievements">Logros de GOG</string> + <string name="gog_achievements_hint">Sincroniza los logros de los juegos de GOG compatibles. Se aplica en el próximo inicio.</string> + <string name="gog_achievements_off">Logros desactivados</string> + <string name="gog_achievements_on">Logros activados</string> + <string name="gog_achievements_signin">Vuelve a iniciar sesión en GOG</string> + <string name="gog_achievements_uploaded">Logros subidos</string> + <string name="gog_achievements_pending">Subida de logro pendiente</string> + <string name="gog_achievements_enable_store">Activa los logros de GOG en los ajustes de tiendas</string> + <string name="gog_achievements_unavailable">Logros no disponibles; revisa los registros de la sesión</string> + <string name="achievement_unlocked">Logro desbloqueado</string> + <string name="touch_paste">Pegar</string> + <string name="touch_dpad">Pad direccional</string> + <string name="touch_left_stick">Stick izquierdo</string> + <string name="touch_right_stick">Stick derecho</string> + <string name="touch_menu">Menú</string> + <string name="touch_keyboard">Teclado</string> + <string name="touch_dpad_up">Pad direccional arriba</string> + <string name="touch_dpad_down">Pad direccional abajo</string> + <string name="touch_dpad_left">Pad direccional izquierda</string> + <string name="touch_dpad_right">Pad direccional derecha</string> + <string name="touch_title">Controles táctiles</string> + <string name="touch_controller">Mando táctil</string> + <string name="touch_mouse">Ratón</string> + <string name="touch_on">Activado</string> + <string name="touch_off">Desactivado</string> + <string name="touch_speed_slow">Lenta</string> + <string name="touch_speed_normal">Normal</string> + <string name="touch_speed_fast">Rápida</string> + <string name="touch_edit_layout">Editar disposición</string> + <string name="touch_droiddeck_menu">Menú de DroidDeck</string> + <string name="touch_drag_hint">Arrastra controles a la pantalla</string> + <string name="touch_arrange">Organizar</string> + <string name="touch_discard">Descartar</string> + <string name="touch_saving">Guardando…</string> + <string name="touch_icon">Icono</string> + <string name="touch_narrower">A−</string> + <string name="touch_wider">A+</string> + <string name="touch_shorter">Al−</string> + <string name="touch_taller">Al+</string> + <string name="touch_hide">Ocultar</string> + <string name="touch_color_opacity">Color y opacidad…</string> + <string name="touch_copy_layout">Copiar esta disposición</string> + <string name="touch_layout_copied">Disposición copiada; pégala en la disposición táctil de cualquier juego</string> + <string name="touch_paste_layout">Pegar disposición copiada</string> + <string name="touch_reset_layout">Restablecer a la de Steam</string> + <string name="touch_no_binding">Este control no tiene una acción asignada en Steam para ponerle un icono</string> + <string name="touch_icon_for">Icono para %1$s</string> + <string name="touch_layout_color">Color de la disposición táctil</string> + <string name="touch_done">Hecho</string> + <string name="touch_save_failed">No se pudo guardar la disposición táctil</string> + <string name="touch_saved">Disposición táctil guardada en Steam</string> + <string name="touch_app_id">App %1$d</string> + <string name="touch_label_hint">Etiqueta (opcional)</string> + <string name="touch_icons_missing">No se encontraron los iconos de Steam</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d icono de Steam</item> + <item quantity="many">%1$d iconos de Steam</item> + <item quantity="other">%1$d iconos de Steam</item> + </plurals> + <string name="touch_icon_color">Color del icono</string> + <string name="touch_button_color">Color del botón</string> + <string name="touch_no_icon">Sin icono</string> + + <string name="stores_sync_sign_in">Comprobando el inicio de sesión…</string> + <string name="stores_sync_library">Obteniendo tu biblioteca…</string> + <string name="stores_sync_game_list">Obteniendo la lista de juegos…</string> + <string name="stores_sync_details">Cargando datos de los juegos… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">Sincronizando %1$d juego…</item> + <item quantity="many">Sincronizando %1$d juegos…</item> + <item quantity="other">Sincronizando %1$d juegos…</item> + </plurals> + <string name="stores_sync_progress">Sincronizando… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Obteniendo las carátulas…</string> + <string name="stores_sync_failed">Error al sincronizar</string> + <string name="stores_sync_no_games">No se encontraron juegos en tu biblioteca de %1$s</string> + <string name="stores_sync_amazon_no_answer">Amazon Games no respondió al consultar tu biblioteca: actualiza o vuelve a iniciar sesión</string> + <string name="stores_sync_gog_unreachable">No se pudo conectar con GOG</string> + <string name="stores_sync_gog_unreadable">No se pudo leer tu biblioteca de GOG</string> + <string name="stores_registering">Registrando con Steam…</string> <string name="drawer_page_glasses">Gafas</string> <string name="glasses_open">Gafas VITURE</string> <string name="glasses_control">Control de las gafas</string> @@ -2196,4 +2276,34 @@ <string name="stores_hltb_completionist">Completista</string> <string name="stores_hltb_hours">%1$s h</string> <string name="stores_hltb_minutes">%1$d min</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Modo Cardboard</string> + <string name="cardboard_mode_hint">Vistas para el ojo izquierdo y el derecho para un visor de RV para móvil.</string> + <string name="cardboard_source">Imagen</string> + <string name="cardboard_source_screen">Pantalla virtual</string> + <string name="cardboard_source_sbs">3D lado a lado</string> + <string name="cardboard_viewer">Visor</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Sin lente</string> + <string name="cardboard_ipd">Distancia entre ojos</string> + <string name="cardboard_screen_width">Ancho de pantalla</string> + <string name="cardboard_screen_distance">Distancia de la pantalla</string> + <string name="cardboard_head_tracking">Seguimiento de cabeza</string> + <string name="cardboard_no_sensor">Este dispositivo no tiene sensor de rotación.</string> + <string name="cardboard_recenter">Recentrar vista</string> + <string name="cardboard_recenter_button">Recentrar</string> + <string name="cardboard_steamvr">SteamVR (experimental)</string> + <string name="cardboard_steamvr_install">Instalar</string> + <string name="cardboard_steamvr_install_hint">Descarga SteamVR para ARM64 de Valve (571 MB).</string> + <string name="cardboard_steamvr_installing">Descargando y descomprimiendo SteamVR…</string> + <string name="cardboard_steamvr_start">Iniciar</string> + <string name="cardboard_steamvr_ready_hint">Ejecuta SteamVR usando tu cabeza como visor; la imagen cambia a 3D lado a lado.</string> + <string name="cardboard_steamvr_stop">Detener</string> + <string name="cardboard_steamvr_running_hint">SteamVR está en ejecución.</string> + <string name="cardboard_steamvr_unavailable">No disponible en esta sesión.</string> + <string name="cardboard_steamvr_failed">SteamVR no respondió; revisa el registro de la sesión.</string> + <string name="drawer_component_downloads">Descargar componentes</string> + <string name="drawer_component_downloads_hint">Las descargas se comparten con la página Componentes. Elige arriba una versión descargada para usarla al iniciar el próximo juego.</string> + <string name="drawer_component_choose_download">Elige una versión para descargar</string> </resources> diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml new file mode 100644 index 000000000..fd0679c05 --- /dev/null +++ b/app/src/main/res/values-fi/strings.xml @@ -0,0 +1,2235 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>Ensimmäinen käynnistys lataa Steamin. Tämä voi kestää muutaman minuutin.</item> + <item>Oletuksena sovelluksesta poistuminen keskeyttää Steamin; se jatkuu siitä mihin jäit, kun palaat takaisin.</item> + <item>Ohjaimesi näkyy Xbox 360 -ohjaimena. Muussa tapauksessa käytä näytön ohjaimia.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>Ensimmäinen käynnistys määrittää työpöydän. Tämä voi kestää muutaman minuutin.</item> + <item>Oletuksena sovelluksesta poistuminen keskeyttää istunnon; se jatkuu siitä mihin jäit, kun palaat takaisin.</item> + <item>Kosketus toimii kosketuslevynä: vedä liikuttaaksesi osoitinta, napauta napsauttaaksesi.</item> + <item>Ohjaimesi näkyy Xbox 360 -ohjaimena. Muussa tapauksessa käytä näytön ohjaimia.</item> + </string-array> + <string name="session_channel">Istunto</string> + <string name="session_channel_description">Näkyy, kun Steam- tai työpöytäistunto on käynnissä</string> + <string name="session_notification">Steam on käynnissä</string> + <string name="session_notification_downloads">Steamin lataus käynnissä</string> + <string name="session_notification_paused">Istunto keskeytetty - napauta Jatka, kun olet valmis</string> + <string name="resume_session">Jatka</string> + <string name="suspend_now">Keskeytä nyt</string> + <string name="stop_session">Lopeta istunto</string> + <string name="frame_gen_title">Kehysten generointi</string> + <string name="frame_gen_hint">Valitse kehysten generoinnin tila</string> + <string name="frame_gen_off">Pois</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG Mukautuva %1$d</string> + <string name="lsfg_needs_steam">Vaatii Lossless Scalingin Steamista</string> + <string name="lsfg_needs_install">Asenna Lossless Scaling Steamista tai tuo Lossless.dll</string> + <string name="lsfg_import">Tuo Lossless.dll…</string> + <string name="lsfg_using_steam">Käytössä on Steamin kopio</string> + <string name="lsfg_using_import">Käytössä on tuotu kopio</string> + <string name="lsfg_pick_title">Valitse Lossless.dll</string> + <string name="lsfg_imported">Lossless.dll tuotu</string> + <string name="lsfg_import_unchanged">Tämä Lossless.dll on jo käytössä</string> + <string name="lsfg_import_not_owned">Lossless Scaling ei ole tämän Steam-tilin omistuksessa</string> + <string name="lsfg_import_invalid">Tämä tiedosto ei ole Lossless Scalingin Lossless.dll</string> + <string name="lsfg_import_failed">LSFG:n valmistelu tästä Lossless.dll-tiedostosta epäonnistui</string> + <string name="lsfg_problem_missing">LSFG tarvitsee Lossless Scalingin Steamista tai tuodun Lossless.dll-tiedoston</string> + <string name="lsfg_problem_failed">LSFG ei pystynyt valmistelemaan sävyttimiään Lossless.dll-tiedostosta</string> + <string name="hud_fg_unsupported">%1$s: ei tuettu</string> + <string name="hud_fg_failed">%1$s: moottori epäonnistui</string> + <string name="hud_fg_needs_lossless">%1$s: vaatii Lossless Scalingin</string> + <string name="hud_fg_starting">%1$s %2$s · käynnistyy</string> + <string name="second_library_import_hint">Lisää Steam-kirjaston ja etsii siitä Windows-pelejä. Omistetut pelit käyttävät omia Steam-merkintöjään; muista tulee Steamin ulkopuolisia pikakuvakkeita.</string> + <string name="game_env_title">Pelin ympäristö</string> + <string name="game_env_hint">Proton-pelien ympäristömuuttujat.</string> + <string name="game_env_edit">Muokkaa</string> + <string name="game_env_shared">Kaikki Proton-pelit</string> + <string name="game_env_scope">Profiili</string> + <string name="game_env_app_id">Steam-sovellustunnus</string> + <string name="game_env_open">Avaa</string> + <string name="game_env_profile">Peli %1$s</string> + <string name="game_env_add">Lisää muuttuja</string> + <string name="game_env_name">Muuttujan nimi</string> + <string name="game_env_value">Arvo</string> + <string name="game_env_save">Tallenna</string> + <string name="game_env_remove">Poista</string> + <string name="game_env_restore">Peri uudelleen</string> + <string name="game_env_reset">Tyhjennä ympäristön ohitukset</string> + <string name="game_env_unset">Poista tästä profiilista</string> + <string name="game_env_empty">Ei asetettuja muuttujia.</string> + <string name="game_env_inherited">Peritty</string> + <string name="game_env_custom">Mukautettu muuttuja</string> + <string name="game_env_error">Lataaminen tai tallentaminen epäonnistui. Tarkista vapaa tallennustila ja yritä uudelleen.</string> + <string name="game_env_invalid">Käytä kelvollista nimeä ja enintään 8192 merkin arvoa.</string> + <string name="game_env_unused">Tavallinen Proton-ajonaikainen ympäristö ei lue tätä muuttujaa.</string> + <string name="game_env_loading">Latautuu…</string> + <string name="game_env_feature">D3D12-ominaisuustason ohitus. 12_2 ilmoittaa DirectX 12 Ultimate -ominaisuudet; tukemattomat ominaisuudet voivat epäonnistua. Jätä tyhjäksi automaattista tunnistusta varten.</string> + <string name="game_env_shader">D3D12-sävytinmallin ohitus. 6_9 vaatii riittävän uuden VKD3D:n ja vastaavat GPU-ominaisuudet. Jätä tyhjäksi automaattista tunnistusta varten.</string> + <string name="game_env_cache">Pidä Mesan sävytinvälimuisti käytössä vähentääksesi toistuvaa kääntämistä. false ottaa välimuistin käyttöön.</string> + <string name="game_env_cache_size">Mesan sävytinvälimuistin enimmäiskoko, esimerkiksi 1G. Suuremmat välimuistit käyttävät enemmän tallennustilaa.</string> + <string name="game_env_vkd3d_config">VKD3D-asetukset D3D12:lle. nodxr poistaa säteenseurannan käytöstä; käytä vain peleissä, jotka sitä tarvitsevat.</string> + <string name="game_env_proton_log">Aseta arvoon 1 Proton-diagnostiikkalokia varten. Lokitus voi heikentää suorituskykyä ja käyttää tallennustilaa.</string> + <string name="game_env_wined3d">Aseta arvoon 1 käyttääksesi OpenGL-pohjaista WineD3D:tä DXVK:n sijaan D3D9–11-yhteensopivuuden testaamiseen.</string> + <string name="game_env_no_ntsync">Aseta arvoon 1 pitääksesi tämän pelin poissa ntsyncistä (droiddeck-ntsync tai ydinajuri); se käyttää droiddeck-esynciä, jos paketti on olemassa, muuten droiddeck-fsynciä.</string> + <string name="game_env_no_esync">Aseta arvoon 1 poistaaksesi droiddeck-esyncin käytöstä tälle pelille; se käyttää sen sijaan droiddeck-fsynciä, tai droiddeck-ntsynciä, jos fsync on myös pois käytöstä.</string> + <string name="game_env_no_fsync">Aseta arvoon 1 pitääksesi tämän pelin poissa droiddeck-fsyncistä; se käyttää droiddeck-esynciä, jos paketti on olemassa, muuten droiddeck-ntsynciä.</string> + <string name="game_env_fsync_first">Aseta arvoon 1 ajaaksesi tämän pelin droiddeck-fsyncillä, tai 0 asettaaksesi droiddeck-esyncin etusijalle, jos paketti sopii sen Protoniin.</string> + <string name="game_env_xalia">Aseta arvoon 0 poistaaksesi käytöstä Protonin ohjaimen navigointiavustimen tälle pelille.</string> + <string name="game_env_hud">DXVK-peittokuva D3D9–11:lle. fps näyttää ruudunpäivitysnopeuden; tyhjänä piilottaa sen.</string> + <string name="game_env_dxvk_config">DXVK-asetukset erotettuna puolipisteillä. Uudemmat versiot käyttävät dxvk.maxFrameRate-asetusta kuvanopeuden rajoittamiseen; käytä tavallisessa pelaamisessa ennemmin istunnon kuvanopeusrajoitinta.</string> + <string name="game_env_vkd3d_limit">D3D12-kuvanopeusraja. 0 poistaa rajan käytöstä.</string> + <string name="game_env_glthread">Mesan OpenGL-säikeistys. true voi auttaa CPU-rajoitteisissa OpenGL-peleissä; testaa pelikohtaisesti.</string> + <string name="sync_backend_title">Wine-synkronointi</string> + <string name="sync_backend_hint">Miten pelit pitävät säikeensä tahdissa. Tulee voimaan seuraavassa istunnossa.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">FEX-esiasetus</string> + <string name="fex_next_launch">Tulee voimaan seuraavalla pelin käynnistyksellä.</string> + <string name="game_settings_title">Pelit</string> + <string name="fex_default_label">FEXin oletukset</string> + <string name="fex_default_detail">Käyttää FEXin oletuksia.</string> + <string name="fex_stability_label">Vakaus</string> + <string name="fex_stability_detail">Turvallisimmat, hitaimmat asetukset.</string> + <string name="fex_compatibility_label">Yhteensopivuus</string> + <string name="fex_compatibility_detail">Täysi TSO ja multiblock.</string> + <string name="fex_intermediate_label">Keskitaso</string> + <string name="fex_intermediate_detail">Tasapainotettu TSO heikennetyllä x87-tarkkuudella.</string> + <string name="fex_performance_label">Suorituskyky</string> + <string name="fex_performance_detail">Kaikki TSO pois käytöstä; multiblock.</string> + <string name="fex_performance_tso_label">Suorituskyky + TSO</string> + <string name="fex_performance_tso_detail">Suorituskykyasetukset, pääasiallinen TSO käytössä.</string> + <string name="fex_extreme_label">Äärimmäinen</string> + <string name="fex_extreme_detail">Poistaa itseään muokkaavan koodin tarkistukset käytöstä. JIT-pelit ja DRM voivat hajota.</string> + <string name="fex_extreme_tso_label">Äärimmäinen + TSO</string> + <string name="fex_extreme_tso_detail">Äärimmäiset asetukset, pääasiallinen TSO käytössä.</string> + <string name="fex_extreme_gn_label">Extreme-gn</string> + <string name="fex_extreme_gn_detail">Suorituskyky TSC- ja metadatavihjeillä. Kokeile ennen Äärimmäistä.</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">Suorituskyky täysin koodintarkistuksin ja hypervisor-bitti piilotettuna.</string> + <string name="game_env_custom_value">Mukautettu arvo</string> + <string name="game_env_select_value">Valitse arvo</string> + <string name="user_apps_section">Emulaattorit ja sovellukset</string> + <string name="user_apps_add">Lisää</string> + <string name="user_apps_add_detail">Oma sovelluksesi tai pelisi</string> + <string name="user_apps_kind_script">Komentosarja</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d%%</string> + <string name="user_apps_starting">Käynnistyy…</string> + <string name="user_apps_added">%1$s lisätty</string> + <string name="user_apps_removed">%1$s poistettu</string> + <string name="user_apps_back">Työpöytä</string> + <string name="user_apps_gone">Tämä sovellus on kadonnut. Valitse se uudelleen Työpöydältä.</string> + <string name="user_apps_open">Avaa %1$s</string> + <string name="user_apps_remove">Poista</string> + <string name="user_apps_remove_confirm">Paina uudelleen poistaaksesi</string> + <string name="user_apps_stop_session">Lopeta istunto poistaaksesi</string> + <string name="user_apps_script_linked">Suorittaa %1$s:n omasta kansiostaan, linkitettynä istuntoon siihen paikkaan, jossa se sijaitsee. Sovelluksen poistaminen ei koske kyseistä kansiota.</string> + <string name="user_apps_script_copied">Suorittaa %1$s:n sen kansion kopiosta DroidDeckin sisällä. Sovelluksen poistaminen poistaa kopion, ei alkuperäistä.</string> + <string name="user_apps_runtime_required">Asenna Linux-ajoympäristö ensin</string> + <string name="user_apps_file_gone">Tiedosto on kadonnut</string> + <string name="user_apps_no_space">Vapaata tilaa ei ole tarpeeksi: kansion kopioiminen vaatii %1$s</string> + <string name="user_apps_copying">Kopioidaan: %1$s</string> + <string name="user_apps_failed">Sovelluksen lisääminen epäonnistui</string> + <string name="user_apps_github_checking">Etsitään julkaisua: %1$s</string> + <string name="user_apps_github_unreachable">Julkaisujen lukeminen epäonnistui: %1$s</string> + <string name="user_apps_github_no_appimage">Yhdessäkään %1$s:n julkaisussa ei ole ARM64- tai x86_64-AppImagea</string> + <string name="user_apps_downloading">Ladataan: %1$s</string> + <string name="user_apps_download_failed">Lataus epäonnistui</string> + <string name="user_apps_update_failed">Sovelluksen päivitys epäonnistui</string> + <string name="user_apps_icon_failed">Tätä kuvaketta ei voitu lukea</string> + <string name="user_apps_saved">%1$s tallennettu</string> + <string name="user_apps_edit">Muokkaa</string> + <string name="user_apps_check_updates">Tarkista päivitykset</string> + <string name="user_apps_checking_updates">Tarkistetaan päivityksiä…</string> + <string name="user_apps_update_to">Päivitä versioon %1$s</string> + <string name="user_apps_up_to_date">Ajan tasalla · %1$s</string> + <string name="user_apps_updated">%1$s päivitetty</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">Lisää sovellus</string> + <string name="add_app_tab_script">Komentosarja</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">.sh-tiedosto. Se käynnistyy omasta kansiostaan koko näytön tilassa gamescopen alla.</string> + <string name="add_app_appimage_hint">ARM64 (aarch64) -AppImage, tai x86_64-AppImage, joka suoritetaan FEXin kautta. Se puretaan Linux-ajoympäristöön kertaalleen.</string> + <string name="add_app_github_hint">Asentaa ARM64-AppImagen repositorion uusimmasta julkaisusta, tai sen x86_64-version FEXin kautta.</string> + <string name="app_fex_title">Käytä FEXiä</string> + <string name="app_fex_auto">Automaattinen</string> + <string name="app_fex_on">Päällä</string> + <string name="app_fex_off">Pois</string> + <string name="app_fex_hint">FEX suorittaa x86-Linux-ohjelmia tällä ARM64-laitteella. Automaattinen käyttää sitä, kun ohjelma on tehty x86:lle.</string> + <string name="app_fex_x86">%1$s FEXin kautta</string> + <string name="app_fex_not_ready">Tämä ohjelma on tehty %1$s-tietokoneille ja käynnistyy FEXin kautta, jota ei ole vielä määritetty. Avaa Steam kerran kirjautuneena: DroidDeck lataa sen myötä FEXin ja tarvittavan Steam Linux Runtimen, jonka jälkeen tämä avautuu.</string> + <string name="session_end_program">Tämä ohjelma</string> + <string name="session_end_fex_missing">%1$s on tehty x86-tietokoneille ja käynnistyy FEXin kautta, jota ei ole vielä asennettu. Käynnistä Steam kerran kirjautuneena ja anna sen ladata FEX ja Steam Linux Runtime, sitten käynnistä tämä uudelleen.</string> + <string name="add_app_flatpak_hint">Flathub-sovellustunnus tai sen Flathub-sivu. Flatpak otetaan käyttöön ensin, jos sitä ei ole vielä otettu.</string> + <string name="add_app_file">Tiedosto</string> + <string name="add_app_no_file">Ei valittua tiedostoa</string> + <string name="add_app_choose_file">Valitse tiedosto</string> + <string name="add_app_change_file">Vaihda</string> + <string name="add_app_pick_script">Valitse komentosarja</string> + <string name="add_app_pick_appimage">Valitse AppImage</string> + <string name="add_app_pick_icon">Valitse kuvake</string> + <string name="add_app_repository">Repositorio</string> + <string name="add_app_repository_placeholder">omistaja/repo tai github.com-linkki</string> + <string name="add_app_repository_invalid">Ei ole GitHub-repositorio</string> + <string name="add_app_flatpak_id">Sovellustunnus</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">Ei ole Flathub-sovellustunnus</string> + <string name="add_app_flatpak_bundle_hint">Tai .flatpak-tiedosto sovellukselle, jota ei ole Flathubissa.</string> + <string name="add_app_pick_flatpak">Valitse .flatpak-tiedosto</string> + <string name="add_app_source">Lähde</string> + <string name="add_app_appearance">Työpöydälläsi</string> + <string name="add_app_preview_name">Uusi sovellus</string> + <string name="add_app_name">Nimi</string> + <string name="add_app_name_placeholder">Sovelluksesta</string> + <string name="add_app_icon">Kuvake</string> + <string name="add_app_icon_default">Oletuskuvake</string> + <string name="add_app_choose_icon">Valitse kuvake</string> + <string name="add_app_clear_icon">Tyhjennä</string> + <string name="add_app_icon_suggestions">Repositoriosta</string> + <string name="add_app_icon_searching">Etsitään kuvakkeita…</string> + <string name="add_app_icon_reset">Oletus</string> + <string name="edit_app_title">Muokkaa: %1$s</string> + <string name="edit_app_save">Tallenna</string> + <string name="add_app_checking_folder">Tarkistetaan kansiota…</string> + <string name="add_app_folder_linked">Sen kansio on linkitetty istuntoon: %1$s</string> + <string name="add_app_folder_copied">Sen kansio sisältää Linux-ohjelmia, joita jaettu tallennustila ei voi suorittaa, joten se kopioidaan DroidDeckiin (%1$s).</string> + <string name="add_app_confirm">Lisää</string> + <string name="add_app_cancel">Peruuta</string> + <string name="add_app_runtime_required">Asenna Linux-ajoympäristö ensin Käyttöönotto-sivulta.</string> + <string name="add_app_busy">Odota, että %1$s valmistuu.</string> + <string name="steam_compat_label">Yhteensopiva</string> + <string name="proton_compatible_note">Steamin Yhteensopivuus-valikossa merkinnällä ”%1$s” varustetuissa versioissa on DroidDeckin etuliitekorjaukset, ja pelit käynnistyvät niillä täällä. Oletus on Proton Experimental ARM64 (%1$s), tai Proton 11.0 ARM64 (%1$s) kun tuo uudempi versio on asennettu. Valven omat Proton-merkinnät, joissa ei ole merkintää, eivät voi käynnistää peliä tällä laitteella.</string> + <string name="proton_installed_compatible">Asennettu %1$s · %2$s</string> + <string name="common_cancel">Peruuta</string> + <string name="common_auto">Automaattinen</string> + <string name="common_never">Ei koskaan</string> + <string name="common_always">Aina</string> + <string name="common_uninstall">Poista asennus</string> + <string name="common_check">Tarkista</string> + <string name="common_update">Päivitä</string> + <string name="common_checking">Tarkistetaan…</string> + <string name="common_working">Käsitellään…</string> + <string name="common_forget">Unohda</string> + <string name="common_add_ellipsis">Lisää…</string> + <string name="common_use">Käytä</string> + <string name="common_applies_next_session">Tulee voimaan seuraavassa istunnossa.</string> + <string name="mode_steam_title">Steam-istunto</string> + <string name="mode_desktop_title">Työpöytäistunto</string> + <string name="mode_display">Näyttö</string> + <string name="mode_resolution">Resoluutio</string> + <string name="mode_res_720">Enintään 720p</string> + <string name="mode_res_900">Enintään 900p</string> + <string name="mode_res_1080">Enintään 1080p</string> + <string name="mode_res_native">Alkuperäinen</string> + <string name="mode_res_default">%1$s (oletus)</string> + <string name="mode_res_custom_value">Mukautettu · %1$d×%2$d</string> + <string name="mode_res_custom">Mukautettu…</string> + <string name="mode_res_note">720p voi tehdä valikoista reagoivampia.</string> + <string name="mode_ratio">Näytön kuvasuhde</string> + <string name="mode_ratio_custom">Määräytyy mukautetun resoluution mukaan.</string> + <string name="mode_ratio_auto">Automaattinen käyttää vähintään 16:9:ää.</string> + <string name="mode_fps">Kuvanopeusraja</string> + <string name="mode_fps_note">Rajoittaa koko istuntoa. Näyttö vaihtaa virkistystaajuuteen, jonka rajoitus jakaa tasan.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">HDR10-lähtö</string> + <string name="mode_hdr_unavailable">Ei käytettävissä: %1$s.</string> + <string name="mode_hdr_restart">Käynnistä sovellus uudelleen, jotta muutos tulee voimaan.</string> + <string name="mode_drivers">Ajurit</string> + <string name="mode_gpu_drivers">GPU-ajurit</string> + <string name="mode_gpu_drivers_hint">Jaettu Steamin ja Työpöydän kesken. Asetetaan Komponentit-sivulla.</string> + <string name="mode_touch_controls">Kosketus ja ohjaimet</string> + <string name="mode_touch">Kosketus</string> + <string name="mode_touch_touchpad">Kosketuslevy</string> + <string name="mode_touch_direct">Suora</string> + <string name="mode_touch_note">Automaattinen: kosketuslevy Työpöydällä, suora Steamissa.</string> + <string name="mode_osc">Näytön ohjaimet</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">Automaattinen näyttää ohjaimet, kun yhtään ohjainta ei ole kytketty.</string> + <string name="osc_steam_touch">Steam-kosketus (beta)</string> + <string name="osc_style_next_session">Steamin kosketusohjaimet otetaan käyttöön seuraavassa Steam-istunnossa</string> + <string name="osc_steam_layout">Steamin kosketusasettelu</string> + <string name="osc_steam_layout_hint">Siirrä, muuta kokoa tai piilota tämän pelin kosketusohjaimet; tallennetaan Steamiin</string> + <string name="mode_controller">Ohjain</string> + <string name="mode_controller_hint">Miten Steam näkee ohjaimesi. Tulee voimaan seuraavassa istunnossa.</string> + <string name="mode_controller_deck">Steam Deck -ohjain</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Xbox 360 -ohjain</string> + <string name="mode_controller_note">Deck: pikavalikkopainike ja gyroskooppi. Steam Controller: samat, ja takapainikkeissa puristustunnistus. DualShock 4 ja DualSense: gyroskooppi, ja kosketuslevyt toimivat kosketuslevynä. Xbox 360: avaa pikavalikko Guide+A:lla.</string> + <string name="mode_back">Back</string> + <string name="back_menu_then_qam">Valikko → QAM</string> + <string name="back_qam_then_menu">QAM → Valikko</string> + <string name="mode_session">Istunto</string> + <string name="mode_suspend">Taustatoiminta</string> + <string name="mode_suspend_hint">Kun sovellus on piilossa tai näyttö sammuu.</string> + <string name="mode_suspend_manual">Manuaalinen</string> + <string name="mode_suspend_native">Natiivi</string> + <string name="mode_suspend_steam_note">Automaattinen keskeyttää ja jatkaa, kun sovellus tulee näkyviin. Natiivi valmistelee Steamin lepotilaan ensin. Manuaalinen odottaa Jatka-painiketta. Ei koskaan pitää sovelluksen aina käynnissä.</string> + <string name="mode_background_downloads">Pidä lataukset käynnissä</string> + <string name="mode_background_downloads_hint">Viivästytä taustakeskeytystä, kun Steam lataa aktiivisesti.</string> + <string name="mode_suspend_note">Automaattinen keskeyttää taustalla ja jatkaa, kun sovellus tulee näkyviin. Manuaalinen odottaa Jatka-painiketta. Ei koskaan pitää sovelluksen aina käynnissä.</string> + <string name="mode_startup">Käynnistys</string> + <string name="mode_steam_startup">Käynnistä Steam DroidDeckin kanssa</string> + <string name="mode_steam_startup_hint">Avaa Steam-istunnon, kun DroidDeck käynnistyy.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">Tarkistetaan julkaisuja…</string> + <string name="mode_decky_ready">Valmiina asentamaan %1$s.</string> + <string name="mode_decky_none">Yhteensopivaa versiota ei löytynyt. Yritä myöhemmin uudelleen.</string> + <string name="mode_decky_installed">Asennettu · %1$s</string> + <string name="mode_decky_update">Päivitys saatavilla · %1$s</string> + <string name="mode_decky_current">Ajan tasalla · %1$s</string> + <string name="mode_decky_loader">Loader</string> + <string name="mode_decky_install_latest">Asenna uusin</string> + <string name="mode_decky_on">Käynnistyy Steamin kanssa. Päällä ollessaan muut tämän laitteen sovellukset voivat pystyä ohjaamaan Steamia.</string> + <string name="mode_decky_off">Pois. Steamin etävianmääritysportti pysyy suljettuna.</string> + <string name="mode_decky_remove_title">Poistetaanko Decky Loader?</string> + <string name="mode_decky_remove_text">Poistaa loaderin. Laajennuksesi ja asetuksesi säilyvät.</string> + <string name="mode_client">Asiakasohjelma</string> + <string name="mode_mangoapp">Suorituskyvyn peittokuva</string> + <string name="mode_mangoapp_hint">Poista käytöstä, jos pelit kaatuvat tai näyttö mustuu Deck-tilassa. Tulee voimaan seuraavassa istunnossa.</string> + <string name="mode_steam_repair">Korjaa Steam-asiakasohjelma</string> + <string name="mode_steam_repair_hint">Lataa Steam-asiakasohjelman uudelleen, kun Steam käynnistyy seuraavan kerran. Käytä tätä, jos Steam jää lataus­näytölle. Pelit, kirjautuminen ja asetukset säilyvät.</string> + <string name="mode_steam_repair_queued">Steam-asiakasohjelma ladataan uudelleen, kun Steam käynnistyy seuraavan kerran.</string> + <string name="mode_steam_repair_button">Korjaa</string> + <string name="mode_added_art">Kuvitus</string> + <string name="mode_sgdb_key">SteamGridDB-API-avain</string> + <string name="mode_sgdb_key_yours">Oma avaimesi</string> + <string name="mode_sgdb_edit">Muokkaa…</string> + <string name="mode_added_art_hint">Täyttää puuttuvan kuvituksen Steamista ja sen jälkeen SteamGridDB:stä. Pelin kansiossa oleva oma cover.jpg, hero.jpg, logo.png tai icon.png menee näiden edelle.</string> + <string name="summary_remove_game">Poista %1$s</string> + <string name="mode_added_remove_confirm">Poistetaanko?</string> + <string name="games_add">Lisää peli</string> + <string name="games_add_title">Lisää peli:</string> + <string name="games_pick_exe_title">Valitse exe-tiedosto:</string> + <string name="games_add_unreachable">Tämä sijainti ei ole käytettävissä istunnossa</string> + <string name="mode_added_exe_check">Tarkista tämä exe-tiedosto</string> + <string name="wincomp_title">Windows-komponentit</string> + <string name="wincomp_pad_hint">LB/RB siirtyvät osioiden välillä.</string> + <string name="wincomp_card_none">Ei mitään</string> + <string name="wincomp_applies">Windows-DLL-tiedostoja, joita Proton ei sisällä, komponenttiluettelosta. Ota käyttöön ne, joita tämä peli tarvitsee: ne asennetaan pelin omaan Windows-kansioon, eivät koskaan sen tiedostoihin, ja säilyvät Protonista riippumatta.</string> + <string name="wincomp_offline">Komponenttiluetteloa ei tavoitettu. Jo ladattuja komponentteja voi silti vaihtaa.</string> + <string name="wincomp_recommended">Suositellaan tälle pelille</string> + <string name="wincomp_checking">Tarkistetaan, mitä tämä peli tarvitsee…</string> + <string name="wincomp_when_online">Käytettävissä verkkoyhteyden kanssa</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">Otetaan käyttöön %1$d komponentti…</item> + <item quantity="other">Otetaan käyttöön %1$d komponenttia…</item> + </plurals> + <string name="wincomp_auto_section">Käytössä tässä pelissä</string> + <string name="wincomp_auto_steam">Steam asentaa %1$s:n %2$s:n mukana (sovellus %3$s)</string> + <string name="wincomp_auto_gog">GOGin luettelosta tälle pelille</string> + <string name="wincomp_auto_epic">Epicin vaatimuksista tälle pelille</string> + <string name="wincomp_auto_amazon">Amazonin asennusvaiheista tälle pelille</string> + <string name="wincomp_steam_by_name">Steam asentaa %1$s:n sovelluksella %2$s, nimen perusteella täsmäytettynä</string> + <string name="wincomp_suggestions">Myös listalla</string> + <string name="wincomp_suggestion_reason">%1$s listaa tämän; ei komponentti tässä</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">Pelissä on mukana %1$s</string> + <string name="wincomp_found_shipped">Pelissä on %1$s (toimii usein sellaisenaan)</string> + <string name="wincomp_already_here">Jo tässä pelissä</string> + <string name="wincomp_no_recommendation">Mikään pelin kansiossa ei viittaa komponenttiin.</string> + <string name="wincomp_active">Asennettu tälle pelille (%1$d)</string> + <string name="wincomp_all">Kaikki komponentit (%1$d)</string> + <string name="wincomp_installers">Vaativat Windows-asennusohjelman (%1$d)</string> + <string name="wincomp_installers_note">Näillä on omat asennusohjelmansa, joiden sisältä ei voi purkaa pakettia, ja niiden asentamisesta ei ole vielä tallennettua tietoa. Useimmat ovat vanhoja ajoympäristöjä, joiden tilalla toimii jo Protonin sisäinen Mono; tallenteet lisätään sitä mukaa kun niitä tehdään.</string> + <string name="wincomp_show">Näytä</string> + <string name="wincomp_hide">Piilota</string> + <string name="wincomp_needs_installer">Käynnistää oman asennusohjelmansa (ei tallennetta vielä)</string> + <string name="wincomp_unsupported">Ei tueta tässä vielä</string> + <string name="wincomp_loading">Ladataan komponentteja…</string> + <string name="wincomp_failed">Asennus epäonnistui: %1$s</string> + <string name="wincomp_downloaded">Ladattu</string> + <string name="wincomp_downloading">Ladataan: %1$s</string> + <string name="wincomp_installing">Asennetaan: %1$s</string> + <string name="wincomp_reverting">Poistetaan käytöstä…</string> + <string name="wincomp_download_size">Lataus %1$s</string> + <string name="wincomp_next_launch">Tulee voimaan, kun peli käynnistyy seuraavan kerran.</string> + <string name="mode_stretch169">Venytä 16:9-pelit</string> + <string name="mode_stretch169_hint">Näytöillä, jotka ovat neliömäisempiä kuin 16:9, venyttää pelit täyttämään ruudun palkkien näyttämisen sijaan. Tulee voimaan seuraavassa istunnossa.</string> + <string name="mode_audio">Ääni</string> + <string name="mode_directaudio">DirectAudio peleille</string> + <string name="mode_directaudio_hint">Ohittaa PulseAudion pienempää viivettä varten.</string> + <string name="mode_mic">Mikrofoni</string> + <string name="mode_mic_hint">Äänichattia varten.</string> + <string name="mode_network">Verkko</string> + <string name="mode_wifi_discovery">Wi-Fi-haku</string> + <string name="mode_wifi_discovery_hint">Näytä Wi-Fi-nimet ja lähellä olevat verkot Steamissa.</string> + <string name="mode_wifi_location_off">Sijaintipalvelut ovat pois käytöstä.</string> + <string name="mode_wifi_permission_denied">Sijaintilupaa ei ole myönnetty.</string> + <string name="mode_wifi_open_settings">Avaa asetukset</string> + <string name="mode_wifi_continue">Jatka</string> + <string name="mode_wifi_explain_title">Otetaanko Wi-Fi-haku käyttöön?</string> + <string name="mode_wifi_explain_text">Android vaatii sijaintiluvan Wi-Fi-nimien lukemiseen ja lähellä olevien verkkojen löytämiseen. DroidDeck käyttää tätä oikeutta näyttääkseen nämä verkot Steamissa. Emme seuraa sijaintiasi. Internet-yhteys toimii myös ilman tätä lupaa.</string> + <string name="mode_storage">Pelien tallennustila</string> + <string name="mode_storage_auto">Automaattinen - SD-kortti, kun käytettävissä, sekä Games-kansiosi</string> + <string name="mode_storage_auto_none">Automaattinen - Games-kansiosi, sekä SD-kortti, kun käytettävissä (ei nyt käytettävissä)</string> + <string name="mode_storage_internal">Vain sisäinen</string> + <string name="mode_storage_folder">Kansio: %1$s</string> + <string name="mode_second_library">Toinen kirjasto</string> + <string name="mode_storage_note">Pelit, jotka striimaavat materiaalia SD-kortilta tai jaetulta tallennustilalta, voivat nykiä.</string> + <string name="mode_storage_diagnostics">Tallennustilan diagnostiikka</string> + <string name="mode_storage_diagnostics_hint">Tallentaa varaustulokset ja otetut tallennustilan ajoitusnäytteet Jaa lokit -toimintoon. Voi hidastaa asennuksia käytössä ollessaan. Tulee voimaan seuraavassa istunnossa.</string> + <string name="mode_choose_folder">Valitse kansio…</string> + <string name="mode_custom_res_title">Mukautettu resoluutio</string> + <string name="mode_custom_res_text">Korvaa rajan ja kuvasuhteen. Näyttöön sopimaton koko saa palkit.</string> + <string name="mode_width">Leveys</string> + <string name="mode_height">Korkeus</string> + <string name="mode_custom_res_range">Väliltä 320×240–3840×2160.</string> + <plurals name="mode_added_count"> + <item quantity="one">%1$d peli</item> + <item quantity="other">%1$d peliä</item> + </plurals> + <string name="common_ok">OK</string> + <string name="common_delete">Poista</string> + <string name="common_restore">Palauta</string> + <string name="common_download">Lataa</string> + <string name="common_delete_named">Poista %1$s</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Ajonaikainen ajuri</string> + <string name="comp_runtime_driver_hint">Käytössä Steamissa, peleissä ja työpöydällä. Tulee voimaan seuraavassa istunnossa.</string> + <string name="comp_import_turnip">Tuo Turnip-zip…</string> + <string name="comp_display_driver">Näyttöajuri</string> + <string name="comp_display_driver_hint">Piirtää kuvat näytölle molemmissa tiloissa. Käynnistä sovellus uudelleen ottaaksesi käyttöön.</string> + <string name="comp_import_adrenotools">Tuo AdrenoTools-zip…</string> + <string name="comp_never">ei koskaan</string> + <string name="comp_nightlies_checked">Nightlies tarkistettu %1$s</string> + <string name="comp_title">Komponentit</string> + <string name="comp_about">Tietoja komponenteista</string> + <string name="comp_check_drivers">Tarkista uudet ajurit</string> + <string name="comp_check_nightlies">Tarkista Nightlies uusien pakettien varalta</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">Tässä valittu Proton käynnistää kaikki pelit, jotka eivät ole valinneet omaa Protoniaan Steamissa, ja Steamissa tehty muutos näkyy myös tässä. Steam korvaa Valven Protonin päivittäessään; alkuperäiset säilytetään versiokohtaisesti.</string> + <string name="comp_scope">Määritys</string> + <string name="comp_profile_configure">Määritä profiili</string> + <string name="comp_profile">Profiili</string> + <string name="comp_scope_general">Oletusprofiili</string> + <string name="comp_scope_general_detail">Käytetään automaattisesti peleissä, joilla ei ole ohituksia.</string> + <string name="comp_profile_game_detail">Tallennetut asetukset käytetään automaattisesti, kun %1$s käynnistyy.</string> + <string name="comp_game_hint">Valitse vain tämän pelin tarvitsemat asetukset. Kaikki muu käyttää edelleen Oletusprofiilia.</string> + <string name="comp_game_fex">FEX-esiasetus</string> + <string name="comp_game_fex_inherited">Käytössä on Oletusprofiilin esiasetus.</string> + <string name="comp_game_fex_override">Tälle pelille on tallennettu mukautettu esiasetus.</string> + <string name="comp_game_fex_custom">Mukautetut FEX-muuttujat</string> + <string name="comp_game_use_general">Käytä oletusta</string> + <string name="comp_game_compatibility">Pelin yhteensopivuus</string> + <string name="comp_game_environment_hint">Tämän pelin Proton-, DXVK-, VKD3D- ja synkronointiympäristön asetukset.</string> + <string name="comp_game_proton_inherited">Käytössä on Oletusprofiilin Proton.</string> + <string name="comp_game_proton_override">Tälle pelille on tallennettu mukautettu Proton Steamin yhteensopivuusmäärityksen kautta.</string> + <string name="comp_no_proton_short">Ei Protonia</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Käyttää Oletusprofiilin pakettia kohteesta %1$s, jollei tämä peli valitse toista pakettia.</string> + <string name="comp_game_component_general">Käyttää Oletusprofiilin komponenttipakettia.</string> + <string name="comp_game_component_override">Käyttää pakettia %1$s vain tässä pelissä.</string> + <string name="comp_game_runtime_options">Ajonaikaiset asetukset</string> + <string name="comp_game_environment_other_hint">Muut pelikohtaiset Proton-, DXVK-, VKD3D-Proton- ja synkronointiympäristön asetukset.</string> + <string name="comp_game_saved_next">%1$s:n profiili on tallennettu seuraavaa pelin käynnistystä varten.</string> + <string name="game_no_steam_title">Käynnistä ilman Steamia</string> + <string name="game_no_steam_detail">Pelille, joka sulkeutuu heti: käynnistää sen ilman Protonin Steam-asetuksia. Steamin ominaisuudet, kuten overlay, on pois käytöstä sille.</string> + <string name="game_no_steam_on">Päällä</string> + <string name="comp_game_proton_live">%1$s:n Proton-valinta on tallennettu ja käytettävissä Steamissa nyt.</string> + <string name="comp_game_proton_next">%1$s:n Proton-valinta on tallennettu. Käynnistä Steam uudelleen ennen pelin käynnistämistä.</string> + <string name="comp_game_proton_not_runnable">%1$s ei voi käyttää valittua Protonia tällä laitteella.</string> + <string name="comp_game_load_failed">%1$s:n profiilin lataaminen epäonnistui: %2$s</string> + <string name="comp_game_save_failed">Pelin profiilia ei voitu tallentaa: %1$s</string> + <string name="comp_game_reset">Palauta pelin profiili</string> + <string name="comp_game_reset_title">Palautetaanko %1$s:n profiili?</string> + <string name="comp_game_reset_body">Poistetaanko tämän pelin Proton, FEX, komponenttipaketti, ympäristö, tekstuurisuodatus ja Windows-komponenttien ohitukset? Peli käyttää tämän jälkeen Oletusprofiilia.</string> + <string name="comp_game_reset_done">%1$s käyttää nyt Oletusprofiilia.</string> + <string name="comp_game_reset_failed">Pelin profiilia ei voitu palauttaa kokonaan: %1$s</string> + <string name="comp_default_live">%1$s käynnistää nyt kaikki pelit, jotka eivät ole valinneet omaa Protoniaan Steamissa.</string> + <string name="comp_default_next">%1$s käynnistää kaikki pelit, jotka eivät ole valinneet omaa Protoniaan, siitä lähtien kun Steam käynnistyy seuraavan kerran.</string> + <string name="comp_default_not_runnable">%1$s ei voi käynnistää pelejä tällä laitteella, joten se ei voi olla oletus.</string> + <string name="comp_default_failed">Oletus-Protonia ei voitu tallentaa: %1$s</string> + <string name="comp_swapped">%1$s · %2$d vaihdettu</string> + <string name="comp_all_original">%1$s · kaikki alkuperäisiä</string> + <string name="comp_game_running_suffix">%1$s · peli käynnissä</string> + <string name="comp_esync_pack">%1$s · droiddeck-esync-paketti</string> + <string name="comp_esync_wanted">%1$s · ei droiddeck-esync-pakettia vielä</string> + <string name="comp_esync_builtin">%1$s · esync sisäänrakennettu, ei pakettia tarvita</string> + <string name="comp_gpu_drivers">GPU-ajurit</string> + <string name="comp_reapplied">%1$s · otettu uudelleen käyttöön käynnistyksessä %2$s</string> + <string name="comp_next_queued">Seuraavaksi: %1$s · pelin sulkeutumisen jälkeen</string> + <string name="comp_in_use">%1$s käytössä: %2$s</string> + <string name="comp_waits">Odottaa, kunnes peli sulkeutuu</string> + <string name="comp_applies_next_game">Tulee voimaan, kun peli käynnistyy seuraavan kerran</string> + <string name="comp_import_zip">Tuo .zip</string> + <string name="comp_reading">Luetaan Protoneja…</string> + <string name="comp_no_proton">Protonia ei ole vielä asennettu. Käynnistä Steam kerran, jotta sen ARM64-Proton latautuu, tai lisää GE-Proton, CachyOS tai DroidDeck-Proton Käyttöönotto-sivulta.</string> + <string name="comp_original">Alkuperäinen · %1$s</string> + <string name="comp_this_build">%1$s · tämä versio</string> + <string name="comp_earlier_build">%1$s · aiempi versio</string> + <string name="comp_installed_count">Asennettu · %1$d</string> + <string name="comp_nothing_stored">Mitään ei ole tallennettu %1$s:lle vielä. Tämän Protonin omat tiedostot tallennetaan sen alkuperäisenä.</string> + <string name="comp_waits_long">Tällä Protonilla on peli käynnissä, joten tämä odottaa, kunnes se sulkeutuu.</string> + <string name="comp_applies_long">Tulee voimaan, kun peli käynnistyy seuraavan kerran.</string> + <string name="comp_swap_title">Vaihdetaanko %1$s?</string> + <string name="comp_swap_body">Asetetaanko %1$s kohteeseen %2$s? Sen alkuperäiset tiedostot pysyvät tallessa. %3$s</string> + <string name="comp_restore_title">Palautetaanko %1$s?</string> + <string name="comp_restore_body">Palautetaanko %1$s:n alkuperäinen kohteeseen %2$s? %3$s</string> + <string name="comp_delete_package_body">Poistaa paketin sovelluksesta. Voit latata sen uudelleen.</string> + <string name="comp_delete_original_title">Poistetaanko tämä alkuperäinen?</string> + <string name="comp_delete_original_body">Tämä %1$s:n alkuperäinen on aiemmasta %2$s-versiosta. Nykyisen version alkuperäinen säilytetään aina.</string> + <string name="comp_nightlies_count">Nightliesissä · %1$d</string> + <string name="comp_refresh_to_list">Päivitä nähdäksesi Nightliesin paketit.</string> + <string name="comp_nothing_new">Ei mitään uutta viimeisimmän tarkistuksen jälkeen.</string> + <string name="comp_import_wcp">Tuo .wcp</string> + <string name="comp_restore_original">Palauta tämän Protonin alkuperäinen</string> + <string name="comp_restore_original_body">Palautetaanko %1$s:n alkuperäinen %2$s?</string> + <string name="comp_swap">Vaihda</string> + <string name="comp_about_text">FEX, DXVK ja VKD3D-Proton kullekin Protonille, jolla Steam käynnistää pelejä.\n\nValitse asennettu rivi ottaaksesi sen käyttöön. Se tulee voimaan, kun peli käynnistyy seuraavan kerran, tai kun käynnissä oleva peli sulkeutuu.\n\nValintasi tarkistetaan uudelleen ennen jokaista käynnistystä ja palautetaan, jos jokin on muuttanut niitä. Kunkin Protonin alkuperäiset tiedostot säilytetään, niin Steamin päivitykset eivät koskaan hävitä niitä.</string> + <string name="comp_tag_original">Alkuperäinen</string> + <string name="comp_tag_in_use">Käytössä</string> + <string name="comp_tag_new">Uusi</string> + <string name="comp_tag_stored">Tallennettu</string> + <string name="perf_title">Suorituskyky</string> + <string name="perf_lede">CPU-ytimet, istunnon korjaukset ja isäntänimi. Tulee voimaan seuraavassa istunnossa.</string> + <string name="perf_client_cores_group">Steam-asiakasohjelman ytimet</string> + <string name="perf_override">Ohita Steamin ydinvalinta</string> + <string name="perf_override_hint">Kiinnittää Steamin, sen käyttöliittymäavustajan ja gamescopen alla oleviin ytimiin.</string> + <string name="perf_client_cores">Asiakasohjelman ytimet</string> + <string name="perf_client_cores_on">Ytimet Steamille.</string> + <string name="perf_client_cores_off">Ota ohitus käyttöön valitaksesi.</string> + <string name="perf_game_cores_group">Pelin ytimet</string> + <string name="perf_game_cores">Pelin ytimet</string> + <string name="perf_game_cores_hint">Ytimet peleille. Kun kaikki on valittuna, Android ajoittaa vapaasti.</string> + <string name="perf_client_ui">Asiakasohjelman käyttöliittymä</string> + <string name="perf_glthread">Säikeistetty GL</string> + <string name="perf_glthread_hint">Voi tehdä Steamin valikoista reagoivampia.</string> + <string name="perf_zink">Zink: laiskat deskriptorit</string> + <string name="perf_zink_hint">Suositellaan ajureille, joissa ei ole deskriptoripuskureita.</string> + <string name="perf_noglerror">Ohita GL-virhetarkistukset</string> + <string name="perf_noglerror_hint">Poistaa käytöstä kutsukohtaisen GL-validoinnin.</string> + <string name="perf_gsrealtime">gamescope: reaaliaikainen GPU-jono</string> + <string name="perf_gsrealtime_hint">Priorisoi kompositorin GPU-työtä. Voi tasoittaa kuvanopeuden tahditusta pelien kustannuksella.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">Pidä GPU huippukellotaajuudessaan</string> + <string name="perf_gpuclock_hint">Vain Adreno. Vähemmän nykäyksiä, enemmän akun kulutusta ja lämpöä.</string> + <string name="perf_fixes">Istunnon korjaukset</string> + <string name="perf_sysmem">Turnip: sysmem-renderöinti</string> + <string name="perf_sysmem_hint">Nopeampi Steamille ja useimmille peleille. Pakollinen Adreno 710/720/722:lla.</string> + <string name="perf_xalia">Ohita Steamin xalia-avustaja</string> + <string name="perf_fastsync">droiddeck-ntsync (kokeellinen)</string> + <string name="perf_fastsync_hint">Käyttäjätilainen korvike Linuxin ntsync-ajurille, testausta varten. Kun se on päällä, Proton käyttää sitä droiddeck-esyncin ja droiddeck-fsyncin sijaan. Toimii Proton Experimental ARM64:n, GE-Protonin ja proton-cachyosin kanssa. Tulee voimaan seuraavassa istunnossa.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync etusijalle</string> + <string name="perf_fsync_hint">Käynnistää Protonin omaa fsynciä vakiotiedostoilla, futex_waitv tarjottuna käyttäjätilassa, niin se ei tarvitse pakettia ja toimii edelleen heti kun Steam päivittää Protonin. Kun se on päällä, pelit käyttävät sitä droiddeck-esyncin edellä, paitsi harvat, jotka Proton pitää pois fsyncistä. Tulee voimaan seuraavassa istunnossa.</string> + <string name="perf_esync_hint">Pelien Windows-säikeet odottavat toisiaan eventfd:n ja jaetun muistin avulla sen sijaan, että kiertäisivät wineserverin kautta. Käyttää juuri tälle Proton-versiolle tehtyä droiddeck-esync-pakettia; paketit haetaan automaattisesti. Kunnes sellainen on olemassa uudelle Proton-versiolle, droiddeck-fsync toimii sen tilalla. Tulee voimaan seuraavassa istunnossa.</string> + <string name="perf_xalia_hint">Poistaa käytöstä Protonin ohjaimen navigointiavustimen, joka kuluttaa suoritintehoa jokaisessa pelissä.</string> + <string name="perf_seccomp">Suorita proot ilman seccompia</string> + <string name="perf_seccomp_hint">Voi korjata puuttuvan järjestelmäkutsun virheitä joissakin ytimissä. Voi heikentää suorituskykyä.</string> + <string name="perf_fastpath">proot-pikareitti</string> + <string name="perf_fastpath_needs_seccomp">Vaatii prootin seccomp-suodattimen.</string> + <string name="perf_fastpath_hint">Nopeampi tiedostojen hakeminen ja vähemmän nykimistä. Poista käytöstä, jos ohjelma ei löydä tiedostojaan.</string> + <string name="perf_identity">Istunnon identiteetti</string> + <string name="perf_hostname">Isäntänimi</string> + <string name="perf_hostname_hint">Nimi, jonka Steam näkee tälle laitteelle. Tyhjä palauttaa arvon %1$s.</string> + <string name="perf_hostname_invalid">Vain kirjaimia, numeroita ja sisäisiä yhdysmerkkejä, enintään 63 merkkiä.</string> + <string name="perf_phantom_warning">Android on asetettu lopettamaan tämä istunto</string> + <string name="driver_installed">Asennettu</string> + <string name="driver_available">Ladattavissa</string> + <string name="driver_nothing_new">Ei mitään uutta viimeisimmän tarkistuksen jälkeen.</string> + <string name="driver_restore_builtin">Palauta sisäänrakennetut ajurit</string> + <string name="driver_delete_builtin">Sisäänrakennettu sovellukseen: tämä poistaa sen puretut tiedostot ja piilottaa sen. Palauta sisäänrakennetut ajurit tuo sen takaisin.</string> + <string name="driver_delete_bundle">Puolikas Android + Linux -paketista: molemmat puoliskot poistetaan. Käytössä oleva ajuri palaa oletukseen.</string> + <string name="driver_delete_plain">Sen tiedostot poistetaan sovelluksesta.</string> + <string name="driver_delete_in_use">Sen tiedostot poistetaan. Se on käytössä, joten oletus ottaa sen paikan.</string> + <string name="drawer_page_display">Näyttö</string> + <string name="drawer_page_effects">Tehosteet</string> + <string name="drawer_page_controls">Ohjaimet</string> + <string name="drawer_page_components">Komponentit</string> + <string name="drawer_page_controller">Ohjain</string> + <string name="drawer_page_settings">Asetukset</string> + <string name="drawer_page_desc">%1$s-sivu</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Työpöytä</string> + <string name="drawer_steam_menu">Steam-valikko</string> + <string name="drawer_quick_access">Pikavalikko</string> + <string name="drawer_open_qam">Avaa pikavalikko</string> + <string name="drawer_prev_page">Edellinen sivu</string> + <string name="drawer_next_page">Seuraava sivu</string> + <string name="drawer_hud">Suorituskyky-HUD</string> + <string name="drawer_scaling">Skaalaustila</string> + <string name="drawer_scaling_sharpness">Skaalauksen terävyys</string> + <string name="drawer_effects">Näyttötehosteet</string> + <string name="drawer_effects_hint">Reaaliaikainen. Kuluttaa hieman GPU-aikaa, kun jokin tehoste on päällä.</string> + <string name="drawer_look">Esiasetukset</string> + <string name="drawer_look_custom">Mukautettu</string> + <string name="drawer_effects_advanced">Lisäasetukset</string> + <string name="widgets_expanded">Laajennettu</string> + <string name="widgets_collapsed">Supistettu</string> + <string name="drawer_cas">Terävöinti (CAS)</string> + <string name="drawer_cas_level">Terävöinnin taso</string> + <string name="drawer_fake_hdr">Keinotekoinen HDR</string> + <string name="drawer_deband">Raidanpoisto</string> + <string name="drawer_deband_strength">Raidanpoiston voimakkuus</string> + <string name="drawer_brightness">Kirkkaus</string> + <string name="drawer_contrast">Kontrasti</string> + <string name="drawer_gamma">Gamma</string> + <string name="drawer_saturation">Kylläisyys</string> + <string name="drawer_fxaa">FXAA (reunan pehmennys)</string> + <string name="drawer_toon">Piirrosääriviivat</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Tekstuurisuodatus</string> + <string name="drawer_texture_hint">DirectX 9–11 -pelit, niiden seuraavasta käynnistyksestä alkaen.</string> + <string name="drawer_anisotropy">Anisotrooppinen suodatus</string> + <string name="drawer_texture_sharpness">Tekstuurien terävyys</string> + <string name="drawer_texture_sharpness_note">Negatiivinen mip-poikkeama: terävämmät tekstuurit, jonkin verran kimmellystä. Automaattinen vastaa sitä, kuinka paljon istunto on suurennettu näytölle.</string> + <string name="drawer_touch_auto">Automaattinen (%1$s)</string> + <string name="drawer_keyboard">Näppäimistö</string> + <string name="drawer_pc_keyboard">PC-näppäimistö</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">Toinen näyttö</string> + <string name="drawer_second_screen_mode">Tila</string> + <string name="drawer_second_screen_shows">Näyttää</string> + <string name="drawer_android_apps">Android-sovellukset</string> + <string name="drawer_launch_app">Käynnistä sovellus</string> + <string name="common_hide">Piilota</string> + <string name="common_show">Näytä</string> + <string name="drawer_no_apps">Ei käynnistettäviä sovelluksia</string> + <string name="drawer_session_behavior">Istunnon toiminta</string> + <string name="drawer_suspend_hint">Tulee voimaan nyt ja tuleviin istuntoihin tässä tilassa.</string> + <string name="drawer_next_session">Seuraava istunto</string> + <string name="drawer_support">Tuki</string> + <string name="drawer_logs">Istunnon lokit</string> + <string name="drawer_logs_hint">Liitä virheraporttiin</string> + <string name="drawer_share_logs">Jaa lokit</string> + <string name="drawer_background">Tausta</string> + <string name="drawer_stop">Lopeta</string> + <string name="drawer_stop_title">Lopetetaanko istunto?</string> + <string name="drawer_running_now">Käynnissä nyt</string> + <string name="drawer_no_game">Mikään peli ei ole käynnissä Protonilla</string> + <string name="drawer_selected_profile">Pelin profiili</string> + <string name="drawer_selected_profile_none">Profiilin tiedot eivät ole vielä saatavilla.</string> + <string name="drawer_profile_refresh_failed_initial">Pelien profiileja ei voitu ladata. Yritetään automaattisesti uudelleen.</string> + <string name="drawer_profile_refresh_failed_stale">Pelien profiileja ei voitu päivittää. Näytetään viimeisin saatavilla oleva tieto; yritetään automaattisesti uudelleen.</string> + <string name="drawer_profile_inspect_hint">Tarkastele ja muokkaa tallennettuja asetuksia, joita käytetään seuraavassa käynnistyksessä.</string> + <string name="drawer_profile_default">Käytössä on Oletusprofiili</string> + <string name="drawer_profile_custom">Mukautettu profiili</string> + <string name="drawer_component_inherited">Käytössä on %1$s Oletusprofiilista.</string> + <string name="drawer_game_components_note">Muutokset tallennetaan %1$s:n profiiliin ja tulevat voimaan, kun peli käynnistyy seuraavan kerran.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d pelikohtainen ohitus</item> + <item quantity="other">%1$d pelikohtaista ohitusta</item> + </plurals> + <string name="drawer_no_proton">Protonia ei ole vielä asennettu.</string> + <string name="drawer_game_running">peli käynnissä</string> + <string name="drawer_reapplied">otettu uudelleen käyttöön käynnistyksessä %1$s</string> + <string name="drawer_original">Alkuperäinen</string> + <string name="drawer_components_note">Tuo ja poista Komponentit-sivulla.</string> + <string name="setup_check_done">Valmis</string> + <string name="setup_check_warn">Vaatii huomiota</string> + <string name="setup_check_busy">Käsitellään</string> + <string name="setup_limit_adb_failed">Tarkista Langattoman virheenkorjauksen IP-osoite ja portti, tai pariuta uudelleen, jos Android poisti tämän laitteen.</string> + <string name="setup_install">Asenna</string> + <string name="setup_manage">Hallitse</string> + <string name="setup_tab_overview">Yleiskatsaus</string> + <string name="setup_tab_controller">Ohjain</string> + <string name="setup_tab_session">Istunto</string> + <string name="setup_tab_launcher">Käynnistin</string> + <string name="setup_title">Käyttöönotto</string> + <string name="setup_all_set">● Kaikki valmiina</string> + <string name="setup_n_ready">%1$d/%2$d valmiina</string> + <string name="setup_system_check">Järjestelmätarkistus</string> + <string name="setup_gpu_supported">Laite tuettu</string> + <string name="setup_gpu_untested">Testaamaton GPU</string> + <string name="setup_gpu_unsupported">GPU ei ole tuettu</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">Steam tarvitsee Adreno-ajurin; %1$s saattaa näyttää mustan ruudun</string> + <string name="setup_runtime">Linux-ajoympäristö</string> + <string name="setup_runtime_progress">%1$s · %2$d%%</string> + <string name="setup_runtime_missing">Ei asennettu · noin 3 GB, asennetaan ensimmäisellä Pelaa-painalluksella</string> + <string name="setup_installed">Asennettu</string> + <string name="setup_runtime_update">%1$s · päivitys saatavilla</string> + <string name="setup_runtime_current">%1$s · ajan tasalla</string> + <string name="setup_limit">Lapsiprosessien raja</string> + <string name="setup_limit_on">Päällä · Android voi sulkea Steamin. Pois ottaminen kestää minuutin.</string> + <string name="setup_limit_unset">Ei asetettu · Android voi sulkea Steamin. Pois ottaminen kestää minuutin.</string> + <string name="setup_limit_unknown">Ei voitu tarkistaa · pois ottaminen kestää minuutin.</string> + <string name="setup_fix_it">Korjaa</string> + <string name="setup_details">Tiedot</string> + <string name="setup_dev_options">Kehittäjäasetukset</string> + <string name="setup_use_wireless">Käytä Langatonta virheenkorjausta</string> + <string name="setup_check_again">Tarkista uudelleen</string> + <string name="setup_limit_turn_on">Ota raja käyttöön</string> + <string name="setup_account">Steam-tili</string> + <string name="setup_signed_in_offline">Kirjautuneena nimellä %1$s · offline-tila</string> + <string name="setup_signed_in">Kirjautuneena nimellä %1$s</string> + <string name="setup_sign_in">Paina Pelaa ja kirjaudu Steamiin</string> + <string name="setup_tools">Työkalut</string> + <string name="setup_controller">Ohjain</string> + <string name="setup_controller_unavailable">Ohjaimen asetukset eivät ole käytettävissä.</string> + <string name="setup_session">Istunto</string> + <string name="setup_logs">Istunnon lokit</string> + <string name="setup_logs_hint">Tallennetaan joka istunnon jälkeen</string> + <string name="setup_agent_commands">Vianmääritystyökalut</string> + <string name="setup_latest_logs">Viimeisimmät istunnon lokit</string> + <string name="setup_saved_logs">Tallennetut istunnon lokit</string> + <string name="setup_saved_logs_hint">Säilyttää %1$d uusinta</string> + <string name="setup_clear_logs">Tyhjennä lokit</string> + <string name="setup_offline">Offline-tila</string> + <string name="setup_sign_in_first">Kirjaudu Steamiin ensin</string> + <string name="setup_launcher">Käynnistin</string> + <string name="setup_language">Kieli</string> + <string name="setup_language_hint">Steam käyttää sitä myös, seuraavasta käynnistyksestä alkaen.</string> + <string name="setup_language_system">Järjestelmän oletus (%1$s)</string> + <string name="setup_language_system_short">Järjestelmän oletus</string> + <string name="setup_app_scale">Sovelluksen skaalaus</string> + <string name="setup_app_scale_hint">DroidDeckin tekstit ja säätimet</string> + <string name="setup_app_scale_default">%1$d %% (Oletus)</string> + <string name="setup_theme">Teema</string> + <string name="setup_theme_hint">Käynnistimen ulkoasu</string> + <string name="setup_home">Käytä Aloitusnäyttönä</string> + <string name="setup_home_on">DroidDeck voi olla kotisovellus</string> + <string name="setup_home_off">DroidDeckiä ei koskaan tarjota kotisovellukseksi</string> + <string name="setup_orientation">Suunta</string> + <string name="setup_orientation_auto">Automaattinen vaakasuunta</string> + <string name="setup_orientation_landscape">Vaakasuunta</string> + <string name="setup_orientation_reverse">Käänteinen vaakasuunta</string> + <string name="setup_fullscreen">Koko näyttö</string> + <string name="setup_fullscreen_on">Piilottaa Androidin tila- ja navigointipalkit</string> + <string name="setup_fullscreen_off">Näyttää Androidin tila- ja navigointipalkit</string> + <string name="setup_default_home">Oletuskotisovellus</string> + <string name="setup_choose_home">Valitse kotisovellus</string> + <string name="setup_choose">Valitse</string> + <string name="setup_linux_apps">Linux-sovellukset (beta)</string> + <string name="setup_tool_files">Tiedostot</string> + <string name="setup_tool_files_hint">Selaa ja hallitse tiedostoja</string> + <string name="setup_tool_protons">Proton-versiot</string> + <string name="setup_tool_protons_hint">Asenna ARM64-Proton-versioita</string> + <string name="setup_tool_performance">Suorituskyky</string> + <string name="setup_tool_performance_hint">CPU-ydinten määritys</string> + <string name="setup_tool_roms">ROM-kansio</string> + <string name="setup_tool_roms_hint">Mihin emulaattoripelit tallennetaan</string> + <string name="setup_card_components">Komponentit</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Ohjaimet</string> + <string name="setup_card_controls_hint">Painikkeiden kartoitus</string> + <string name="store_tab_discover">Löydä</string> + <string name="store_tab_search">Hae</string> + <string name="store_tab_installed">Asennetut</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Kauppa</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak valmis</string> + <string name="store_needs_runtime">Asenna Linux-ajoympäristö ensin Käyttöönotto-sivulta.</string> + <string name="store_setup_title">Ota Flatpak käyttöön</string> + <string name="store_setup_text">Lisää Flatpakin ja yhdistää sen Flathubiin (noin 20 MB). Ensimmäinen sovellus lataa myös oman ajoympäristönsä.</string> + <string name="store_setting_up">Otetaan käyttöön…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Päivitykset</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d%%</string> + <string name="store_starting">Käynnistyy…</string> + <string name="store_categories">Luokat</string> + <string name="store_unreachable">Flathubia ei tavoitettu. Tarkista yhteytesi ja yritä uudelleen.</string> + <string name="store_try_again">Yritä uudelleen</string> + <string name="store_search_label">Hae Flathubista</string> + <string name="store_search_placeholder">Firefox, pelit, emulaattorit…</string> + <string name="store_search">Hae</string> + <string name="store_all">Kaikki</string> + <string name="store_searching">Haetaan…</string> + <string name="store_search_hint">Hae nimellä tai valitse luokka. Vain ARM64-sovellukset näkyvät listalla.</string> + <string name="store_no_results">Flathubista ei löytynyt tätä vastaavia ARM64-sovelluksia.</string> + <string name="store_results">Tulokset</string> + <string name="store_check_updates">Tarkista päivitykset</string> + <string name="store_update_all">Päivitä kaikki (%1$d)</string> + <string name="store_all_current">● Kaikki on ajan tasalla</string> + <string name="store_no_apps">Ei sovelluksia vielä. Löydä niitä Löydä- tai Hae-välilehdeltä.</string> + <string name="store_setup_first_note">Ota Flatpak käyttöön asentaaksesi sovelluksia.</string> + <string name="store_update_available_prefix">Päivitys saatavilla · %1$s</string> + <string name="store_open">Avaa</string> + <string name="store_details">Tiedot</string> + <string name="store_remove">Poista</string> + <string name="store_press_again">Paina uudelleen poistaaksesi</string> + <string name="store_installing">Asennetaan…</string> + <string name="store_install">Asenna</string> + <string name="store_setup_first">Ota Flatpak käyttöön ensin</string> + <string name="store_download_size">Lataus %1$s</string> + <string name="store_installed_chip">● Asennettu</string> + <string name="store_details_failed">Tämän sovelluksen Flathub-sivua ei voitu ladata.</string> + <string name="store_loading">Ladataan Flathubista…</string> + <string name="store_no_arm">Flathubissa ei ole ARM64-versiota, joten sitä ei voi asentaa tässä.</string> + <string name="store_screenshots">Kuvakaappaukset</string> + <string name="store_about">Tietoja</string> + <string name="store_app_id">Sovellustunnus</string> + <string name="store_license">Lisenssi</string> + <string name="store_runtime">Ajoympäristö</string> + <string name="store_installed_size">Asennuksen koko</string> + <string name="store_installed_size_value">%1$s (lisäksi jaettu ajoympäristö)</string> + <string name="store_website">Verkkosivu</string> + <string name="store_verified">Vahvistettu</string> + <string name="upd_title">Päivitykset</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Tarkistettu %1$s</string> + <string name="upd_not_checked">Ei tarkistettu vielä</string> + <string name="upd_perm_title">Anna DroidDeckin päivittää itsensä</string> + <string name="upd_perm_text">Ota seuraavalla näytöllä käyttöön Salli tästä lähteestä ja palaa takaisin. Päivitys jatkuu itsekseen.</string> + <string name="upd_open_settings">Avaa asetukset</string> + <string name="upd_not_now">Ei nyt</string> + <string name="upd_signed_status">Allekirjoitettu eri tavalla</string> + <string name="upd_signed_headline">Ei voida päivittää paikan päällä</string> + <string name="upd_signed_detail">Tätä kopiota ei ole allekirjoitettu DroidDeckin julkaisuavaimella, niin päivitykset eivät voi asentua sen päälle. Poista se, jos haluat vaihtaa.</string> + <string name="upd_available">Päivitys saatavilla</string> + <string name="upd_blocked_status">Ei voida asentaa</string> + <string name="upd_switch">Valmis vaihdettavaksi</string> + <string name="upd_ahead_status">Stable-versiota edellä</string> + <string name="upd_ahead_headline">Olet Stable-versiota edellä</string> + <string name="upd_ahead_detail">Tämä build on uudempi kuin viimeisin Stable. Siirryt Stableen sen seuraavan julkaisun myötä.</string> + <string name="upd_gone_status">Testi päättyi</string> + <string name="upd_gone_headline">Tämä testi on päättynyt</string> + <string name="upd_gone_detail">Sen korjaus yhdistettiin tai hylättiin. Seuraa Previewiä saadaksesi uusimmat korjaukset.</string> + <string name="upd_current_status">Ajan tasalla</string> + <string name="upd_current_headline">Sinulla on uusin %1$s</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d %%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Päivitä%1$s</string> + <string name="upd_install_button">Asenna%1$s</string> + <string name="upd_install_anyway">Asenna Stable %1$s silti</string> + <string name="upd_follow_preview">Seuraa Previewiä</string> + <string name="upd_stop_session">Lopeta käynnissä oleva istunto päivittääksesi.</string> + <string name="upd_less">Vähemmän</string> + <string name="upd_more">Lisää</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Uusi Preview-build</string> + <string name="upd_new_test">PR #%1$d -testibuild</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">PÄIVITYSKANAVA</string> + <string name="upd_stable">Stable</string> + <string name="upd_stable_hint">Testatut julkaisut, sopii useimmille</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Preview</string> + <string name="upd_preview_hint">Uusimmat korjaukset, ennen Stablea</string> + <string name="upd_preview_current">Ajan tasalla</string> + <string name="upd_tests">Testibuildit</string> + <string name="upd_tests_hint">Kokeile korjausta ennen sen julkaisua</string> + <string name="upd_tests_none">Ei yhtään juuri nyt</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Ei rakennettu tälle DroidDeck-kopiolle</string> + <string name="upd_updated">Päivitettiin %1$s</string> + <string name="upd_test_name">PR #%1$d -testi</string> + <string name="upd_while_ago">hetki sitten</string> + <string name="upd_just_now">juuri nyt</string> + <plurals name="upd_tests_count"> + <item quantity="one">%1$d kokeiltavana</item> + <item quantity="other">%1$d kokeiltavana</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="one">Muutoksia buildisi jälkeen · %1$d build jäljessä</item> + <item quantity="other">Muutoksia buildisi jälkeen · %1$d buildia jäljessä</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="one">Viimeisimmät muutokset · %1$d build</item> + <item quantity="other">Viimeisimmät muutokset · %1$d buildia</item> + </plurals> + <string name="gate_title">Yksi Android-asetus muutettavaksi</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Android pysäyttää sovelluksia, jotka käynnistävät paljon taustaprosesseja, ja Steam käynnistää niitä kymmeniä. Kunnes tämä muuttuu, pelit sulkeutuvat ilman virheilmoitusta.</string> + <string name="gate_fix_for_me">Korjaa se puolestani</string> + <string name="gate_toggle_steps">Ota käyttöön ”Disable child process restrictions” ja palaa takaisin. Jätä Kehittäjäasetukset päälle, tai kytkin kytkeytyy taas pois päältä.</string> + <string name="gate_open_dev">Avaa Kehittäjäasetukset</string> + <string name="gate_no_toggle">Tässä Android-versiossa ei ole kytkintä tälle, niin DroidDeck muuttaa sen Langattoman virheenkorjauksen kautta. Kestää noin minuutin, ei tarvita tietokonetta.</string> + <string name="gate_needs_wifi">Langaton virheenkorjaus vaatii Wi-Fi-yhteyden. Mikä tahansa verkko käy; internet-yhteyttä ei tarvita.</string> + <string name="gate_needs_notifications">Pariutuskoodi tulee ilmoitukseen. Salli ilmoitukset DroidDeckille, tai käytä Muita tapoja.</string> + <string name="gate_allow_notifications">Salli ilmoitukset</string> + <string name="gate_step1">Avaa Asetukset → Kehittäjäasetukset → Langaton virheenkorjaus ja kytke se päälle</string> + <string name="gate_step2">Napauta ”Pariuta laite pariutuskoodilla”</string> + <string name="gate_step3">Kirjoita koodi DroidDeckin ilmoitukseen</string> + <string name="gate_waiting">Odotetaan pariutuksen ponnahdusikkunaa…</string> + <string name="gate_code_needed">Ponnahdusikkuna löytyi. Syötä sen koodi ilmoitukseen.</string> + <string name="gate_status">Tila</string> + <string name="gate_polling">Tarkistetaan 2 sekunnin välein. Tämä sivu sulkeutuu, kun se on pois päältä.</string> + <string name="gate_other_ways">Muita tapoja</string> + <string name="gate_other_toggle">Langaton virheenkorjaus, manuaalinen osoite, tietokoneen ADB</string> + <string name="gate_other_no_toggle">Manuaalinen osoite, tietokoneen ADB, Kehittäjäasetukset</string> + <string name="gate_use_wireless_instead">Käytä sen sijaan Langatonta virheenkorjausta</string> + <string name="gate_some_roms">Jotkin ROM-versiot lisäävät kytkimen joka tapauksessa. Etsi ”child process” Kehittäjäasetuksista.</string> + <string name="gate_manual_hint">Jos ilmoitus ei toimi, syötä ponnahdusikkunan osoite ja koodi itse.</string> + <string name="gate_enter_manually">Syötä osoite manuaalisesti</string> + <string name="gate_from_computer">Tietokoneelta ADB:llä:</string> + <string name="gate_copy_command">Kopioi komento</string> + <string name="gate_choose_display">Valitse, millä näytöllä Android-asetukset avataan.</string> + <string name="gate_main_screen">Päänäyttö</string> + <string name="wadb_finding_port">Pariutui. Etsitään Langattoman virheenkorjauksen porttia…</string> + <string name="wadb_enter_port">Pariutui. Syötä portti Asetuksista.</string> + <string name="wadb_applying">Laite löytyi. Otetaan asetus käyttöön…</string> + <string name="wadb_apply_failed">Automaattinen käyttöönotto epäonnistui. Tarkista alla oleva osoite ja yritä uudelleen. %1$s</string> + <string name="wadb_title_wireless">Langaton virheenkorjaus</string> + <string name="wadb_title_done">Lapsiprosessien raja päivitetty</string> + <string name="wadb_title">Muuta lapsiprosessien rajaa</string> + <string name="wadb_lede_compact0">Jos asetus puuttuu, pariuta Langattoman virheenkorjauksen kautta.</string> + <string name="wadb_lede_compact1">Pariutui. Tarkista IP-osoite ja portti Langattomasta virheenkorjauksesta.</string> + <string name="wadb_lede_done">Lapsiprosessien raja päivitettiin.</string> + <string name="wadb_lede0">Käytä vaihetta 2, jos Kehittäjäasetuksissa ei ole lapsiprosessiasetusta. Avaa Kehittäjäasetukset → Langaton virheenkorjaus → Pariuta laite pariutuskoodilla.</string> + <string name="wadb_lede1">Pariutui. Käytä alla olevaa osoitetta, tai päivitä se Langaton virheenkorjaus → IP-osoite ja portti -kohdasta.</string> + <string name="wadb_lede_done_long">Lapsiprosessien raja päivitettiin Langattoman virheenkorjauksen kautta.</string> + <string name="wadb_confirmed_on">Android vahvisti, että raja on päällä.</string> + <string name="wadb_confirmed_off">Android vahvisti, että raja on pois päältä.</string> + <string name="wadb_pair_group">2 · Pariuta Langaton virheenkorjaus</string> + <string name="wadb_pair_address">Pariutuksen ponnahdusikkunan IP-osoite ja portti</string> + <string name="wadb_pin">Pariutuskoodi (PIN)</string> + <string name="wadb_pin_placeholder">6 numeroa</string> + <string name="wadb_pair">Pariuta</string> + <string name="wadb_apply_group">2 · Ota käyttöön Langattoman virheenkorjauksen kautta</string> + <string name="wadb_connect_address">Yhteyden IP-osoite ja portti</string> + <string name="wadb_pair_again">Pariuta uudelleen</string> + <string name="wadb_turn_off">Kytke raja pois päältä</string> + <string name="wadb_other_ways">Muita tapoja muuttaa sitä</string> + <string name="wadb_dev_try_first">1 · Kehittäjäasetukset (kokeile ensin)</string> + <string name="wadb_restrict_on">Aseta ”Restrict child processes” päälle.</string> + <string name="wadb_restrict_off">Aseta ”Restrict child processes” pois päältä.</string> + <string name="wadb_computer_last">3 · Tietokoneen ADB (viimeinen keino)</string> + <string name="wadb_only_if">Vain jos Langaton virheenkorjaus ei ole käytettävissä.</string> + <string name="wadb_computer_group">3 · Tietokoneen ADB (viimeinen keino)</string> + <string name="ctrl_osc_hint">Kun kosketusohjaimet näkyvät istunnossa</string> + <string name="ctrl_color">Väri</string> + <string name="ctrl_color_hint">Näytön painikkeiden sävy</string> + <string name="ctrl_custom">Mukautettu</string> + <string name="ctrl_opacity">Peittävyys</string> + <string name="ctrl_percent">%1$d %%</string> + <string name="ctrl_size">Painikkeiden koko</string> + <string name="ctrl_size_hint" formatted="false">100% on vakiokoko</string> + <string name="ctrl_stick_click">Sauvan napsautus</string> + <string name="ctrl_stick_click_hint">Kaksoisnapauta ja pidä sauvaa pohjassa saadaksesi L3:n tai R3:n</string> + <string name="ctrl_adaptive">Mukautuvat sauvat</string> + <string name="ctrl_rumble">Pelin tärinä</string> + <string name="ctrl_steam_button">Steam-painike</string> + <string name="ctrl_qam_button">QAM-painike</string> + <string name="ctrl_keyboard_button">Näppäimistöpainike</string> + <string name="ctrl_select_steam">Avaa Steam-valikko Select-painikkeella</string> + <string name="ctrl_select_steam_hint">View/Select-painikkeen sijaan.</string> + <string name="setup_animations">Animaatiot</string> + <string name="ctrl_adaptive_hint">Sauvat tulevat näkyviin siihen, mihin kosketat, ja piiloutuvat, kun vapautat</string> + <string name="ctrl_layout">Asettelu</string> + <string name="ctrl_layout_custom">Mukautetut sijainnit tallennettu</string> + <string name="ctrl_layout_default">Sovitettu tähän näyttöön</string> + <string name="ctrl_edit">Muokkaa</string> + <string name="ctrl_reset">Palauta</string> + <string name="ctrl_mapping">Painikkeiden kartoitus</string> + <string name="ctrl_mapping_default">Jokainen painike lähettää oman syötteensä</string> + <string name="ctrl_mapping_count">%1$d/%2$d painiketta uudelleenkartoitettu</string> + <string name="ctrl_configure">Määritä</string> + <string name="ctrl_reset_all">Palauta ohjain</string> + <string name="ctrl_reset_all_hint">Palauttaa kaikki ohjaimen asetukset ja asettelun</string> + <string name="ctrl_mapping_lede">Mitä mikä näytön painike lähettää pelille. Piilotettu poistaa painikkeen.</string> + <string name="ctrl_onscreen_buttons">Näytön painikkeet</string> + <string name="ctrl_defaults">Oletukset</string> + <string name="ctrl_reset_mapping">Palauta kartoitus</string> + <string name="content_android_apps">Android-sovellukset</string> + <string name="content_apps_count">%1$d sovellusta</string> + <string name="content_no_android_apps">Käynnistettäviä Android-sovelluksia ei löytynyt.</string> + <string name="content_desktop_installed">● Työpöytä asennettu</string> + <string name="content_desktop_first_open">Asentuu ensimmäisellä avauksella</string> + <string name="content_available">Saatavilla asennettavaksi</string> + <string name="content_not_installed">Ei asennettu.</string> + <string name="content_not_installed_suffix">%1$s · ei asennettu</string> + <string name="content_open_named">Avaa %1$s</string> + <string name="content_games">Pelit</string> + <string name="content_choose_roms">Valitse ROM-kansio.</string> + <string name="content_retroarch_roms">Selaa kohteeseen /root/ROMs RetroArchissa.</string> + <string name="content_add_roms">Lisää %1$s-pelit kansioon ROMs/%2$s.</string> + <string name="content_install_named">Asenna %1$s</string> + <string name="content_stop_to_install">Lopeta istunto asentaaksesi</string> + <string name="content_runtime_required">Ajoympäristö vaaditaan</string> + <string name="content_loading_details">Ladataan asennustietoja…</string> + <string name="content_details_unavailable">Asennustiedot eivät ole käytettävissä. Yritä myöhemmin uudelleen.</string> + <string name="content_needs_runtime">Asenna Linux-ajoympäristö ensin Käyttöönotto-sivulta.</string> + <string name="content_stop_first">Lopeta käynnissä oleva istunto asentaaksesi työpöytäsovelluksia.</string> + <string name="content_install_into">Asentaa %1$s Linux-työpöydälle.</string> + <string name="store_busy_percent_short">%1$s · %2$d %%</string> + <string name="content_hide_note">Piilottaminen poistaa sen Työpöydältä. Sen tiedostot pysyvät Linux-ajoympäristössä.</string> + <string name="content_rom_gone">Tämä peli ei ole enää ROM-kansiossa.</string> + <string name="content_desktop_system">Työpöytä · %1$s</string> + <string name="content_launch_in">Käynnistä kohteessa %1$s</string> + <string name="content_also_in">Myös kohteessa %1$s</string> + <string name="content_page_gone">Tätä sivua ei ole enää. Valitse osio vasemmalta.</string> + <string name="content_open_desktop">Avaa työpöytä</string> + <string name="content_install_open_desktop">Asenna ja avaa työpöytä</string> + <string name="desktop_remove_title">Poista työpöytä</string> + <string name="desktop_remove_text">Poistaa työpöydän ja siihen kuuluvat sovellukset. Säilyttää pelit, lisätyt sovellukset, asetukset ja jaetut kirjastot.</string> + <string name="desktop_removing">Poistetaan työpöytää</string> + <string name="desktop_removed">Työpöytä poistettu</string> + <string name="desktop_cleaning_legacy">Poistetaan vanhoja LXQt-tiedostoja</string> + <string name="desktop_removal_unknown">Päivitä DroidDeck ennen tämän työpöytäversion poistamista.</string> + <string name="content_linux_desktop">Linux-työpöytä</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox ja emulaattorisi</string> + <string name="games_empty">Steamissa asentamasi pelit näkyvät tässä istunnon jälkeen.</string> + <string name="games_play_steam">Pelaa Steamissa</string> + <string name="games_launch_settings">Käynnistysasetukset</string> + <string name="games_launch">Käynnistä</string> + <string name="games_files">Pelitiedostot</string> + <string name="games_prefix">Proton-prefix</string> + <string name="games_saves_looking">Etsitään tallennuksia…</string> + <string name="games_saves_none">Nimellä löydettävää tallennuskansiota ei löytynyt. Vienti ottaa koko käyttäjäkansion.</string> + <string name="games_saves_folders">%1$d tallennuskansiota</string> + <string name="games_saves_summary">%1$s · %2$d tiedostoa · %3$s</string> + <string name="games_kb">%1$d kt</string> + <string name="games_manage_saves">Hallitse tallennuksia</string> + <string name="games_saves">Pelitallennukset</string> + <string name="games_import_saves">Tuo tallennuksia…</string> + <string name="games_export_gamehub">Vie GameHub-zipinä…</string> + <string name="games_export_winlator">Vie Winlator-zipinä…</string> + <string name="games_open_save_folder">Avaa tallennuskansio</string> + <string name="games_never_played">Ei pelattu koskaan</string> + <string name="games_runtime_busy_percent">Ajoympäristö varattu · %1$d %%</string> + <string name="games_runtime_busy">Ajoympäristö varattu</string> + <string name="games_runtime_first_play">Ajoympäristö asentuu ensimmäisellä Pelaa-painalluksella</string> + <string name="games_runtime_update">Ajoympäristön päivitys saatavilla</string> + <string name="games_runtime_ready">● Ajoympäristö valmis</string> + <string name="games_last_played">Viimeksi pelattu %1$s</string> + <string name="games_internal">Sisäinen tallennustila</string> + <string name="games_added">Lisätty peli</string> + <string name="games_custom">Mukautettu peli</string> + <string name="games_store_storage">Pelien tallennustila</string> + <string name="gpu_auto_hint">Parhaiten tähän GPU:hun sopiva pari, pidetään ajan tasalla</string> + <string name="gpu_manual_hint">Valitse pari itse</string> + <string name="gpu_checking">Tarkistetaan ajureita…</string> + <string name="gpu_none_listed">Ei ajureita listattuna vielä. Päivitä etsiäksesi niitä.</string> + <string name="gpu_hide_other">Piilota muiden GPU:iden ajurit</string> + <string name="gpu_show_other">Näytä muiden GPU:iden ajurit</string> + <string name="gpu_advanced">Lisäasetukset</string> + <string name="gpu_set_with_display">Asetetaan yhdessä näyttöajurin kanssa: %1$s kattaa molemmat.</string> + <string name="gpu_set_with_runtime">Asetetaan yhdessä ajoympäristöajurin kanssa: %1$s kattaa molemmat.</string> + <string name="gpu_runtime_hint">Käytetään peleissä ja työpöydällä.</string> + <string name="gpu_runtime_default">Ajoympäristön oletus</string> + <string name="gpu_display_hint">Piirtää kuvat näytölle. Käytössä DroidDeckin uudelleenkäynnistyksen jälkeen.</string> + <string name="gpu_auto_by_gpu">Automaattinen - valittu GPU:n mukaan</string> + <string name="gpu_manage">Lataa, tuo tai poista…</string> + <string name="gpu_in_use_caps">KÄYTÖSSÄ</string> + <string name="gpu_builtin">DroidDeckiin sisäänrakennetut ajurit</string> + <string name="gpu_matched_pair">Ajoympäristö- ja näyttöajurit asetetaan yhdessä toisiinsa sopivana parina.</string> + <string name="gpu_recommended_for">Suositellaan tälle GPU:lle: %1$s.</string> + <string name="gpu_refresh_hint">Päivitä etsiäksesi ajureita tälle GPU:lle.</string> + <string name="gpu_recommended_caps">SUOSITELTU</string> + <string name="gpu_unavailable">Ei saatavilla</string> + <string name="load_runtime">Linux-ajoympäristö</string> + <string name="load_runtime_detail">Valmistellaan Linux-ajoympäristöä</string> + <string name="load_session">Istunto</string> + <string name="load_session_detail">Käynnistetään näyttöä ja ääntä</string> + <string name="load_steam">Steam-asiakasohjelma</string> + <string name="load_steam_detail">Tarkistetaan Steamia ja sen yhteensopivuustyökaluja</string> + <string name="load_opening_steam">Avataan Steam</string> + <string name="load_opening_steam_detail">Odotetaan Steamin ensimmäistä kuvaa</string> + <string name="load_desktop">Työpöytä</string> + <string name="load_desktop_detail">Valmistellaan työpöytää</string> + <string name="load_opening">Avataan</string> + <string name="load_opening_detail">Odotetaan ensimmäistä kuvaa</string> + <string name="load_starting_steam">Käynnistetään Steamia</string> + <string name="load_ended">Istunto päättyi</string> + <string name="load_step">Vaihe %1$d/%2$d</string> + <string name="load_steam_paused">Steam on keskeytetty</string> + <string name="deck_tab_grips">Kahvat</string> + <string name="deck_tab_left">Vasen pad</string> + <string name="deck_tab_right">Oikea pad</string> + <string name="deck_tab_both">Molemmat</string> + <string name="deck_tab_gamepad">Peliohjain</string> + <string name="deck_surface_stick">Tatti</string> + <string name="deck_surface_trackpad">Kosketuslevy</string> + <string name="deck_surface_switch">Vaihda tatin ja kosketuslevyn välillä</string> + <string name="deck_more_controls">Lisää Deck-ohjaimia</string> + <string name="deck_panels">PANEELIT</string> + <string name="deck_close">Sulje toisen näytön ohjaimet</string> + <string name="deck_open_steam_menu">Avaa Steam-valikko</string> + <string name="deck_left_upper">Vasen · ylä</string> + <string name="deck_left_lower">Vasen · ala</string> + <string name="deck_right_upper">Oikea · ylä</string> + <string name="deck_right_lower">Oikea · ala</string> + <string name="deck_click">Napsautus</string> + <string name="deck_right_trackpad">Oikea kosketuslevy</string> + <string name="deck_left_trackpad">Vasen kosketuslevy</string> + <string name="second_trackpad">Kosketuslevy</string> + <string name="second_show_keyboard">Näytä näppäimistö</string> + <string name="second_hide_keyboard">Piilota näppäimistö</string> + <string name="second_terminal">Linux-pääte</string> + <string name="second_terminal_not_ready">Linux-istunto ei ole vielä valmis päätteelle.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Pelit</string> + <string name="rail_desktop">Työpöytä</string> + <string name="rail_store">Kauppa</string> + <string name="rail_components">Komponentit</string> + <string name="rail_apps">Sovellukset</string> + <string name="rail_setup">Asetukset</string> + <string name="rail_updates">Päivitykset</string> + <string name="rail_percent">%1$d %%</string> + <string name="rail_resume_named">Jatka: %1$s</string> + <string name="roms_text">Näkyy sessiossa polussa /root/ROMs. Sisäinen tallennustila on /root/Storage.</string> + <string name="roms_none">Kansiota ei valittu</string> + <string name="roms_applies">Vaikuttaa seuraavaan istuntoon. SD-kortit toimivat.</string> + <string name="roms_choose">Valitse kansio</string> + <string name="roms_change">Vaihda kansio</string> + <string name="proton_lede">Asenna ARM64-Proton-versio ja valitse se pelikohtaisesti Steamissa kohdasta Properties → Compatibility.</string> + <string name="proton_stop_first">Pysäytä käynnissä oleva istunto, jotta voit asentaa tai poistaa yhteensopivuustyökalun.</string> + <string name="proton_available">Saatavilla olevat versiot</string> + <string name="proton_installed">Asennettu: %1$s</string> + <string name="proton_queued">Odottaa aiempaa pyyntöä</string> + <string name="proton_not_installed">Ei asennettu</string> + <string name="proton_update_available">Päivitys saatavilla → %1$s</string> + <string name="proton_remove_older">Poista vanhat %1$s-versiot</string> + <string name="proton_remove_older_body">Poistaa %1$s. Näihin versioihin asetetut pelit on vaihdettava toiseen Protoniin.</string> + <string name="proton_remove_body">Tähän versioon asetetut pelit on vaihdettava toiseen Protoniin.</string> + <string name="proton_install_now">Asenna nyt</string> + <string name="proton_cancel_queued">Peruuta jonotus</string> + <string name="pckb_android">Android-näppäimistö</string> + <string name="pckb_hide">Piilota ✕</string> + <string name="main_launch_named">Käynnistä %1$s</string> + <string name="main_choose_screen">Valitse näyttö. Toinen näyttö: %1$s.</string> + <string name="main_secondary_gone">Toinen näyttö ei ole enää käytettävissä.</string> + <string name="main_primary">Ensisijainen näyttö</string> + <string name="main_secondary">Toinen näyttö</string> + <string name="widgets_prev_tab">Edellinen välilehti</string> + <string name="widgets_next_tab">Seuraava välilehti</string> + <string name="widgets_on">Päällä</string> + <string name="widgets_all_n">Kaikki %1$d</string> + <string name="widgets_none">Ei mitään</string> + <string name="widgets_n_of_m">%1$d/%2$d</string> + <string name="widgets_off">Pois</string> + <string name="screen_bottom">Alanäyttö</string> + <string name="game_env_applies">Vaikuttaa seuraavaan pelin käynnistykseen. Pelin omat asetukset ohittavat yhteiset.</string> + <string name="game_env_other">Toinen peli sovellustunnuksella…</string> + <string name="game_env_done">Valmis</string> + <string name="game_env_default">Oletus</string> + <string name="game_env_off">Pois</string> + <string name="game_env_change">Muuta…</string> + <string name="game_env_change_title">Muuta muuttujaa</string> + <string name="game_env_restore_default">Palauta oletus</string> + <string name="game_env_eyebrow">Proton-pelit</string> + <string name="pip_title">Kuva kuvassa</string> + <string name="pip_auto">Siirry automaattisesti kuva kuvassa -tilaan</string> + <string name="unfolded_controls">Ohjaimet avattuna näytöllä</string> + <string name="unfolded_controls_hint">Avattuna peli sijaitsee Steam Deckin kosketuslevyjen ja takakahvojen yläpuolella. Taitettuna peli on koko näytön kokoinen.</string> + <string name="pip_suspended">Keskeytetty</string> + <string name="pip_not_ready">Kuva kuvassa -tila on käytettävissä, kun istunto on valmis.</string> + <string name="pip_permission">Salli kuva kuvassa -tila DroidDeckille Android-asetuksissa.</string> + <string name="pip_unavailable">Android ei pystynyt avaamaan kuva kuvassa -tilaa.</string> + <string name="game_shortcut">Pikakuvake</string> + <string name="game_shortcut_add">Lisää aloitusnäytölle</string> + <string name="game_shortcut_choose">Valitse peli</string> + <string name="game_shortcut_scanning">Etsitään asennettuja pelejä…</string> + <string name="game_link_copy">Kopioi käynnistyslinkki</string> + <string name="game_link_copied">Käynnistyslinkki kopioitu</string> + <string name="game_shortcut_unsupported">Tämä käynnistin ei voi lisätä pikakuvakkeita. Käytä sen sijaan Kopioi käynnistyslinkki -toimintoa.</string> + <string name="game_link_invalid">Virheellinen pelin käynnistyslinkki.</string> + <string name="game_link_missing">Tämä peli ei ole enää DroidDeck-kirjastossasi.</string> + <string name="game_link_session_busy">Pysäytä nykyinen istunto ennen Steam-pelin käynnistämistä.</string> + <string name="game_link_failed">Pelin lähettäminen Steamiin epäonnistui. Yritä käynnistää se uudelleen.</string> + <string name="setup_retry_removal">Yritä poistoa uudelleen</string> + <string name="runtime_removing">Poistetaan</string> + <string name="runtime_removal_incomplete">Poisto kesken</string> + <string name="game_file_export">Vie pelitiedosto</string> + <string name="game_file_sync">Synkronoi kaikki pelitiedostot kansioon</string> + <string name="game_file_stop_sync">Pysäytä pelitiedostojen synkronointi</string> + <string name="game_file_folder">Valitse kansio käyttöliittymän pelitiedostoille</string> + <string name="game_file_exported">Pelitiedostot tallennettu kohteeseen %1$s</string> + <string name="game_frontend_files">Käyttöliittymän tiedostot</string> + + <string name="main_wait_runtime">Odota, että ajoympäristötoiminto päättyy</string> + <string name="main_wait_proton_install">Odota, että yhteensopivuustyökalun asennus päättyy</string> + <string name="main_wait_desktop_install">Odota, että työpöytäsovelluksen asennus päättyy</string> + <string name="main_wait_install_game">Odota, että asennus päättyy ennen pelin käynnistämistä</string> + <string name="main_saves_close_session">Sulje Steam-istunto ensin, jotta peli ei voi tallentaa tuonnin päälle</string> + <string name="main_saves_importing">Tuodaan kohteeseen %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="one">Tuotiin %1$d tiedosto %2$s-muodosta kohteeseen %3$s</item> + <item quantity="other">Tuotiin %1$d tiedostoa %2$s-muodosta kohteeseen %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">Tuotiin %1$d tiedosto %2$s-muodosta kohteeseen %3$s. Vanhat tallennukset varmuuskopioitu kansioon Download/DroidDeck/Saves/backups</item> + <item quantity="other">Tuotiin %1$d tiedostoa %2$s-muodosta kohteeseen %3$s. Vanhat tallennukset varmuuskopioitu kansioon Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Valitse tallennusten zip-tiedosto kohteelle %1$s</string> + <string name="main_saves_exporting">Viedään %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="one">Vietiin %1$d tiedosto %2$s-muotoisena: %3$s</item> + <item quantity="other">Vietiin %1$d tiedostoa %2$s-muotoisena: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Valitse, minne %1$s (%2$s) tallennetaan</string> + <string name="main_action_failed">%1$s epäonnistui: %2$s</string> + <string name="main_no_logs_yet">Ei istuntolokeja vielä: suorita istunto ensin.</string> + <string name="main_no_logs_to_clear">Ei tyhjennettäviä istuntolokeja.</string> + <plurals name="main_logs_cleared"> + <item quantity="one">Tyhjennettiin %1$d istuntolokikansio.</item> + <item quantity="other">Tyhjennettiin %1$d istuntolokikansiota.</item> + </plurals> + <string name="main_wadb_pair_failed">Langattoman virheenkorjauksen laitepariutus epäonnistui</string> + <string name="main_wadb_command_failed">Langattoman virheenkorjauksen komento epäonnistui</string> + <string name="main_wadb_unavailable">Langaton virheenkorjaus ei ole käytettävissä</string> + <string name="main_adb_copied">ADB-komento kopioitu</string> + <string name="main_pick_roms">Valitse ROMs-kansio</string> + <string name="main_non_adreno_title">Ei Adreno-grafiikkapiiri</string> + <string name="main_non_adreno_text">Turnip tukee Adreno-grafiikkapiirejä. Laitteella %1$s Steam voi näyttää mustan ruudun. Latauksen koko: %2$.0f MB.</string> + <string name="main_install_anyway">Asenna silti</string> + <string name="main_remove_runtime_title">Poista Linux-ajoympäristö</string> + <string name="main_remove_runtime_text">Tämä poistaa ajoympäristön, siihen asennetun Steam-asiakasohjelman ja kaikki sinne asennetut pelit.</string> + <string name="main_applied">Otettu käyttöön: %1$s</string> + <string name="android_app_open_failed_primary">Sovelluksen %1$s avaaminen ensisijaisella näytöllä epäonnistui</string> + <string name="android_app_open_failed_on">Sovelluksen %1$s avaaminen näytöllä %2$s epäonnistui</string> + <string name="android_app_open_failed_display">Sovelluksen %1$s avaaminen näytöllä %2$d epäonnistui</string> + <string name="comp_busy_importing">Tuodaan</string> + <string name="comp_busy_swapping">Vaihdetaan</string> + <string name="comp_busy_restoring">Palautetaan</string> + <string name="comp_busy_cancelling">Perutaan</string> + <string name="comp_busy_deleting">Poistetaan</string> + <string name="comp_cannot_read">tiedostoa ei voi lukea</string> + <string name="comp_imported">Tuotiin %1$s</string> + <string name="comp_swap_cancelled">Odottava vaihto peruttiin.</string> + <string name="comp_pick_wcp">Valitse komponenttipaketti (-linux .wcp)</string> + <string name="comp_nightlies_unreachable">Nightlies-julkaisuihin ei saatu yhteyttä</string> + <string name="comp_stored">Tallennettu %1$s</string> + <string name="comp_download_failed">Lataus epäonnistui: %1$s</string> + <string name="drivers_pick_linux">Valitse Linux-ajoympäristön ajuri (-Linux-zip tai Android + Linux -paketti)</string> + <string name="drivers_pick_android">Valitse näyttöajuri (AdrenoTools-zip tai Android + Linux -paketti)</string> + <string name="drivers_pick_any">Valitse ajuri-zip tai Android + Linux -paketti</string> + <string name="main_pkg_problem">%1$s: %2$s</string> + <string name="main_removing_named">Poistetaan %1$s…</string> + <string name="main_forgetting_named">Unohdetaan %1$s…</string> + <string name="main_pick_game_storage">Valitse pelien tallennustilakansio</string> + <string name="main_pick_decky_zip">Valitse Decky-laajennuksen ZIP-tiedosto</string> + <string name="main_storage_not_usable">Ei käytettävissä: %1$s</string> + <string name="main_running_program">Ohjelma</string> + <string name="main_removing_runtime">Poistetaan Linux-ajoympäristöä</string> + <string name="main_settings_main_screen">Asetuksia ei voitu avata alanäytöllä; avattiin sen sijaan päänäytöllä.</string> + <string name="main_open_dev_options">Avaa Kehittäjäasetukset Asetuksissa</string> + + <string name="upd_unknown_apps_failed">Androidin \"Tuntemattomien sovellusten asentaminen\" -asetusta ei voitu avata</string> + <string name="upd_couldnt_check">Tarkistus epäonnistui: %1$s</string> + <string name="upd_no_download">Tälle versiolle ei ole latausta tähän DroidDeck-kopioon</string> + <string name="upd_stop_first">Pysäytä käynnissä oleva istunto ennen päivitystä</string> + <string name="upd_stage_downloading">Latausta</string> + <string name="upd_stage_installing">Asennusta</string> + <string name="upd_stage_confirm">Odotetaan vahvistustasi</string> + <string name="upd_failed">Päivitys epäonnistui</string> + <string name="upd_not_offered">Android ei tarjonnut päivityksen asentamista</string> + + <string name="decky_install_failed">Deckyn asennus epäonnistui: %1$s</string> + <string name="decky_plugin_starting">Aloitetaan laajennuksen tuontia…</string> + <string name="decky_plugin_failed">Laajennuksen tuonti epäonnistui: %1$s</string> + <string name="decky_plugin_installed">Laajennus asennettu. Käynnistä Steam uudelleen, jotta se latautuu.</string> + + <string name="drivers_not_checked">Ei tarkistettu vielä - napauta päivitä etsiäksesi uusia ajureita</string> + <string name="drivers_bundle_sets_display">Android + Linux: asettaa näyttöajurin sen mukana</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: asettaa ajoympäristön ajurin sen mukana</string> + <string name="drivers_runtime_builtin">ajoympäristöön sisäänrakennettu Turnip</string> + <string name="drivers_system_vulkan">järjestelmän Vulkan: ei paketoitua versiota tälle grafiikkapiirille</string> + <string name="drivers_bundled">%1$s (mukana)</string> + <string name="drivers_manual">Manuaalinen</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">Vain toinen puolisko on julkaistu juuri nyt</string> + <string name="drivers_pair_size">%1$.0f MB molemmille</string> + <string name="drivers_none_to_set">Ei asetettavia ajureita: %1$s</string> + <string name="drivers_checking_latest">Tarkistetaan uusimpia ajureita…</string> + <string name="drivers_up_to_date_check_failed">Ajan tasalla (tarkistus epäonnistui: %1$s)</string> + <string name="drivers_recommended_unpublished">Suositeltuja ajureita ei ole julkaistu juuri nyt</string> + <string name="drivers_downloading_pair">Ladataan %1$s %2$s…</string> + <string name="drivers_updated_to">Päivitetty versioon %1$s %2$s.</string> + <string name="drivers_updated_to_restart">Päivitetty versioon %1$s %2$s. Näyttöajuri otetaan käyttöön, kun DroidDeck käynnistyy uudelleen.</string> + <string name="drivers_switched_to">Vaihdettu versioon %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Vaihdettu versioon %1$s %2$s. Näyttöajuri otetaan käyttöön, kun DroidDeck käynnistyy uudelleen.</string> + <string name="drivers_using">Käytössä %1$s %2$s.</string> + <string name="drivers_using_restart">Käytössä %1$s %2$s. Näyttöajuri otetaan käyttöön, kun DroidDeck käynnistyy uudelleen.</string> + <string name="drivers_not_a_driver">Ei ajuri-zip: ei AdrenoTools-ajuri, -Linux-Turnip eikä Android + Linux -paketti</string> + <string name="drivers_import_failed">Tuonti epäonnistui: %1$s</string> + <string name="drivers_imported_unnamed">Ajuri tuotu</string> + <string name="drivers_using_bundle">Käytetään %1$s sekä ajoympäristön että näytön ajurina.</string> + <string name="drivers_using_bundle_restart">Käytetään %1$s sekä ajoympäristön että näytön ajurina. Näyttöajuri otetaan käyttöön, kun DroidDeck käynnistyy uudelleen.</string> + <string name="drivers_deleted_bundle">Poistettu %1$s (molemmat ajurit)</string> + <string name="drivers_deleted">Poistettu %1$s</string> + <string name="drivers_release_status">Uusimmat: %1$s · tarkistettu %2$s</string> + <string name="drivers_release_status_failed">Uusimmat: %1$s · %2$s ei tavoitettavissa · tarkistettu %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">%1$d min sitten</item> + <item quantity="other">%1$d min sitten</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">%1$d h sitten</item> + <item quantity="other">%1$d h sitten</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">%1$d päivä sitten</item> + <item quantity="other">%1$d päivää sitten</item> + </plurals> + <string name="drivers_checking_sources">Tarkistetaan Banners-Turnip, WinNative ja DroidDeck…</string> + <string name="drivers_check_failed">tarkistus epäonnistui</string> + <string name="drivers_installed_pick_either">Asennettu %1$s - valitse se kummasta tahansa listasta asettaaksesi molemmat ajurit</string> + <string name="drivers_installed_pick">Asennettu %1$s - valitse se valikosta</string> + + <string name="session_wait_removal">Odota, että ajoympäristön poisto päättyy</string> + <string name="session_title_game">Peli</string> + <string name="session_touch_auto_touchpad">kosketuslevy</string> + <string name="session_touch_auto_direct">suora</string> + <string name="session_swap_failed">Vaihto epäonnistui: %1$s</string> + <string name="session_profile_save_failed">Profiilin tallennus epäonnistui: %1$s</string> + <string name="session_desktop_paused">Työpöytä on keskeytetty</string> + <string name="session_named_paused">%1$s on keskeytetty</string> + <string name="session_program_paused">Ohjelma on keskeytetty</string> + <string name="session_starting_desktop">Käynnistetään työpöytää</string> + <string name="session_starting_named">Käynnistetään %1$s</string> + <string name="session_starting_program">Käynnistetään ohjelmaa</string> + <string name="session_no_logs">Ei lokeja tälle istunnolle.</string> + <string name="session_logs_archive_failed">Istuntolokien arkistoa ei voitu luoda.</string> + <string name="session_logs_share_failed">Istuntolokeja ei voitu jakaa.</string> + <string name="session_starting_session">Käynnistetään istuntoa…</string> + <string name="session_runtime_downloading">ladataan Linux-ajoympäristöä</string> + <string name="session_runtime_downloading_mb">ladataan Linux-ajoympäristöä · %1$d/%2$d MB</string> + <string name="session_runtime_checking">tarkistetaan Linux-ajoympäristöä</string> + <string name="session_runtime_unpacking">puretaan Linux-ajoympäristöä</string> + <string name="session_desktop_downloading">ladataan työpöytää</string> + <string name="session_desktop_downloading_mb">ladataan työpöytää · %1$d/%2$d MB</string> + <string name="session_desktop_checking">tarkistetaan työpöytää</string> + <string name="session_desktop_unpacking">puretaan työpöytää</string> + <string name="session_proton_downloading">ladataan Proton Experimentalia (ARM64)</string> + <string name="session_proton_downloading_mb">ladataan Proton Experimentalia (ARM64) · %1$d/%2$d MB</string> + <string name="session_proton_checking">tarkistetaan Proton Experimentalia (ARM64)</string> + <string name="session_proton_unpacking">puretaan Proton Experimentalia (ARM64)</string> + <string name="session_runtime_catalog_unreachable">Ajoympäristöluetteloon ei saatu yhteyttä. Tarkista yhteys ja paina Play uudelleen.</string> + <string name="session_runtime_install_failed">Linux-ajoympäristö ei asentunut. Tarkista ajoympäristö Asetuksissa ja paina Play uudelleen.</string> + <string name="session_desktop_install_failed">Työpöytä ei asentunut (%1$s). Tarkista yhteys ja paina Työpöytä uudelleen.</string> + <string name="session_ended_named">%1$s pysähtyi odottamatta. Jaa lokit virheraportin mukana, tai yritä uudelleen.</string> + <string name="session_ended_program">Ohjelma pysähtyi odottamatta. Jaa lokit virheraportin mukana, tai yritä uudelleen.</string> + <string name="session_ended_desktop">Työpöytä pysähtyi odottamatta. Jaa lokit virheraportin mukana, tai yritä uudelleen.</string> + <string name="session_ended_detail">Poistumistila %1$d · %2$s</string> + <string name="session_ended_no_log">ei lokia</string> + <plurals name="session_hint_enosys"> + <item quantity="one">Loki näyttää %1$d ”Function not implemented” -virheen: prootin seccomp-kiihdytys epäonnistuu laitteen apuohjelmassa. Kokeile %2$s → ”%3$s” (tai ”%4$s”) ja käynnistä uudelleen.</item> + <item quantity="other">Loki näyttää %1$d ”Function not implemented” -virhettä: prootin seccomp-kiihdytys epäonnistuu laitteen apuohjelmassa. Kokeile %2$s → ”%3$s” (tai ”%4$s”) ja käynnistä uudelleen.</item> + </plurals> + + <string name="mode_decky_plugins">Laajennukset</string> + <string name="mode_decky_plugins_hint">Asenna laajennuksen ZIP-tiedosto seuraavaa Steam-istuntoa varten.</string> + <string name="mode_decky_plugin_downloading">Latautuu…</string> + <string name="mode_decky_plugin_install_zip">Asenna ZIP-tiedostosta</string> + <string name="store_cat_games">Pelit</string> + <string name="store_cat_audio_video">Ääni ja video</string> + <string name="store_cat_graphics">Grafiikka</string> + <string name="store_cat_internet">Internet</string> + <string name="store_cat_office">Toimisto</string> + <string name="store_cat_developer">Kehitys</string> + <string name="store_cat_education">Koulutus</string> + <string name="store_cat_science">Tiede</string> + <string name="store_cat_system">Järjestelmä</string> + <string name="store_cat_utilities">Apuohjelmat</string> + <string name="store_section_popular">Suositut</string> + <string name="store_section_trending">Nousussa</string> + <string name="store_section_new">Uudet</string> + <string name="store_section_updated">Päivitetyt</string> + <string name="store_section_loading">latautuu…</string> + <string name="store_flatpak_setup">Flatpak-asennus</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Flathubista ei voitu tarkistaa päivityksiä</string> + <string name="content_rom_installed">asennettu</string> + <string name="content_rom_folder">kansio</string> + <string name="theme_graphite">Grafiitti</string> + <string name="theme_graphite_detail">Grafiitinväriset pinnat, ja logon oma pallonsininen sävy kaikessa valitussa, kohdistetussa tai painetussa.</string> + <string name="theme_paper">Paperi mustalla</string> + <string name="theme_paper_detail">Valkoinen on pääväri, kuvakkeen sininen on merkki viivoille, renkaille ja pisteille.</string> + <string name="theme_icon_blue">Kuvakkeen sininen</string> + <string name="theme_icon_blue_detail">Musta pohja, valkoinen teksti, sininen kaikessa valitussa, kohdistetussa tai painetussa.</string> + <string name="theme_electric">Sähköinen laivastonsininen</string> + <string name="theme_electric_detail">Sininen kirkkaampana pinnoilla, jotka taittuvat harmaan sijaan laivastonsiniseen.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Space</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <string name="game_sync_err_runtime">Asenna ajoympäristö ensin</string> + <string name="game_sync_err_steam_library">Steam-kirjasto ei ole käytettävissä</string> + <string name="game_sync_err_previous_sd">Aiempi SD-kirjasto ei ole käytettävissä</string> + <string name="game_sync_err_game_storage">Pelien tallennustila ei ole käytettävissä</string> + <string name="game_sync_err_added_games">Lisättyjen pelien kansio ei ole käytettävissä</string> + <string name="game_sync_err_manifests">Steam-manifestit eivät ole käytettävissä</string> + <string name="content_system_many">Useita järjestelmiä</string> + <string name="fm_drive_internal">Sisäinen</string> + <string name="fm_drive_linux">Linux-järjestelmä</string> + <string name="fm_drive_sd">SD-kortti</string> + <string name="fm_drive_storage">Tallennustila</string> + <string name="fm_select">Valitse</string> + <string name="fm_properties">Ominaisuudet</string> + <string name="fm_favorite_add">Lisää suosikkeihin</string> + <string name="fm_favorite_remove">Poista suosikeista</string> + <string name="fm_rename">Nimeä uudelleen</string> + <string name="fm_copy">Kopioi</string> + <string name="fm_cut">Leikkaa</string> + <string name="fm_actions">Toiminnot</string> + <string name="fm_favorites">Suosikit</string> + <string name="fm_pin_current">Kiinnitä nykyinen kansio</string> + <string name="fm_no_favorites">Ei suosikkeja vielä - kiinnitä kansio sen ⋮-valikosta palataksesi tähän nopeasti.</string> + <string name="fm_unpin_favorite">Poista suosikeista</string> + <string name="fm_size_zero">0 tavua</string> + <string name="fm_unit_bytes">tavua</string> + <string name="fm_prop_location">Sijainti</string> + <string name="fm_prop_type">Tyyppi</string> + <string name="fm_prop_size">Koko</string> + <string name="fm_prop_modified">Muokattu</string> + <string name="fm_type_folder">Kansio</string> + <string name="fm_type_file">Tiedosto</string> + <string name="fm_type_ext_file">%1$s-tiedosto</string> + <string name="fm_read_only">Vain luku</string> + <string name="fm_read_only_hint">Estää pelejä (ja Winea) ylikirjoittamasta tai poistamasta tätä tiedostoa.</string> + <string name="fm_read_only_failed">Vain luku -tilaa ei voitu muuttaa</string> + <string name="fm_hidden">Piilotettu</string> + <string name="fm_hidden_unsupported">Tämä tallennustila ei tue piilotusmääritettä.</string> + <string name="fm_hidden_hint">Merkitsee tiedoston piilotetuksi Windowsissa (Winen piilotusmääritteenä).</string> + <string name="fm_hidden_failed">Piilotusta ei voitu muuttaa tällä tallennustilalla</string> + <string name="fm_done">Valmis</string> + <string name="fm_op_deleting">Poistetaan...</string> + <string name="fm_delete_failed">Poisto epäonnistui</string> + <string name="fm_op_moving">Siirretään...</string> + <string name="fm_op_copying">Kopioidaan...</string> + <string name="fm_op_moving_item">Siirretään - %1$s</string> + <string name="fm_op_copying_item">Kopioidaan - %1$s</string> + <string name="fm_op_moving_batch">Siirretään %1$d/%2$d - %3$s</string> + <string name="fm_op_copying_batch">Kopioidaan %1$d/%2$d - %3$s</string> + <string name="fm_paste_failed">%1$d valmiina, %2$d epäonnistui</string> + <string name="fm_paste_skipped">%1$d valmiina, %2$d ohitettu</string> + <plurals name="fm_paste_moved"> + <item quantity="one">%1$d kohde siirretty</item> + <item quantity="other">%1$d kohdetta siirretty</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">%1$d kohde kopioitu</item> + <item quantity="other">%1$d kohdetta kopioitu</item> + </plurals> + <string name="fm_already_exists">”%1$s” on jo olemassa</string> + <string name="fm_op_renaming">Nimetään uudelleen...</string> + <string name="fm_rename_failed">Uudelleennimeäminen epäonnistui</string> + <string name="fm_op_creating_folder">Luodaan kansiota...</string> + <string name="fm_create_folder_failed">Kansiota ei voitu luoda</string> + <string name="fm_new_folder">Uusi kansio</string> + <string name="fm_folder_name">Kansion nimi</string> + <string name="fm_create">Luo</string> + <string name="fm_new_name">Uusi nimi</string> + <string name="fm_delete_title">Poistetaanko?</string> + <string name="fm_delete_text">Poistetaanko ”%1$s” pysyvästi?</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">Poistetaanko %1$d kohde?</item> + <item quantity="other">Poistetaanko %1$d kohdetta?</item> + </plurals> + <string name="fm_cannot_undo">Tätä ei voi perua.</string> + <plurals name="fm_and_more"> + <item quantity="one">…ja %1$d lisää</item> + <item quantity="other">…ja %1$d lisää</item> + </plurals> + <string name="fm_op_deleting_batch">Poistetaan %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">%1$d kohdetta ei voitu poistaa</item> + <item quantity="other">%1$d kohdetta ei voitu poistaa</item> + </plurals> + <string name="fm_conflict_merge_hint">Yhdistäminen lisää ja korvaa tiedostoja olemassa olevan kansion sisällä.</string> + <string name="fm_conflict_overwrite_hint">Korvaaminen korvaa olemassa olevan tiedoston.</string> + <string name="fm_conflict_apply_all">Käytä kaikkiin ristiriitoihin</string> + <string name="fm_conflict_merge">Yhdistä</string> + <string name="fm_conflict_overwrite">Korvaa</string> + <string name="fm_conflict_keep_both">Säilytä molemmat</string> + <string name="fm_conflict_skip">Ohita</string> + <string name="fm_select_this_folder">Valitse tämä kansio</string> + <string name="fm_back">Takaisin</string> + <string name="fm_drive_not_readable">%1$s on liitetty, mutta ei ole luettavissa juuri nyt</string> + <string name="fm_list_view">Luettelonäkymä</string> + <string name="fm_grid_view">Ruudukkonäkymä</string> + <string name="fm_search">Hae</string> + <string name="fm_sort">Järjestä</string> + <string name="fm_sort_name">Nimi</string> + <string name="fm_sort_date">Muokkauspäivä</string> + <string name="fm_rows_comfortable">Väljät rivit</string> + <string name="fm_rows_compact">Tiiviit rivit</string> + <string name="fm_hide_hidden">Piilota piilotiedostot</string> + <string name="fm_show_hidden">Näytä piilotiedostot</string> + <string name="fm_hide_favorites">Piilota suosikit</string> + <string name="fm_show_favorites">Näytä suosikit</string> + <string name="fm_filter_folder">Suodata tätä kansiota</string> + <string name="fm_free_space">%1$s vapaana</string> + <string name="fm_selected_count">%1$d valittu</string> + <string name="fm_select_none">Ei mitään</string> + <string name="fm_select_all">Kaikki</string> + <string name="fm_paste">Liitä</string> + <plurals name="fm_items"> + <item quantity="one">%1$d kohde</item> + <item quantity="other">%1$d kohdetta</item> + </plurals> + <string name="fm_paste_here">Liitä %1$s tähän</string> + <string name="fm_paste_move_here">Liitä %1$s (siirrä) tähän</string> + <string name="fm_rail_title">Tiedostot</string> + <string name="fm_rail_storage">TALLENNUSTILA</string> + <string name="fm_rail_quick">PIKAVALINNAT</string> + <string name="fm_rail_favorites">SUOSIKIT</string> + <string name="fm_rail_downloads">Lataukset</string> + <string name="fm_rail_downloads_short">Latauks.</string> + <string name="fm_rail_roms">ROMs</string> + <string name="fm_rail_session_logs">Istuntolokit</string> + <string name="fm_rail_pictures">Kuvat</string> + <string name="fm_favorite_added">”%1$s” lisätty suosikkeihin</string> + <string name="fm_favorite_removed">”%1$s” poistettu suosikeista</string> + <string name="fm_empty_directory">Tyhjä kansio</string> + <string name="fm_up_one_level">Ylös yksi taso</string> + <string name="fm_choose_folder">Valitse kansio</string> + <string name="fm_choose_file">Valitse tiedosto</string> + <string name="ctrl_tint_steam_blue">Steam-sininen</string> + <string name="ctrl_tint_sky">Taivaansininen</string> + <string name="ctrl_tint_white">Valkoinen</string> + <string name="ctrl_tint_violet">Violetti</string> + <string name="ctrl_tint_green">Vihreä</string> + <string name="ctrl_tint_amber">Meripihka</string> + <string name="ctrl_tint_red">Punainen</string> + <string name="ctrl_tint_pink">Pinkki</string> + <string name="ctrl_button_a">A-painike</string> + <string name="ctrl_button_b">B-painike</string> + <string name="ctrl_button_x">X-painike</string> + <string name="ctrl_button_y">Y-painike</string> + <string name="ctrl_button_lb">Vasen olkapainike</string> + <string name="ctrl_button_rb">Oikea olkapainike</string> + <string name="ctrl_button_lt">Vasen liipaisin</string> + <string name="ctrl_button_rt">Oikea liipaisin</string> + <string name="ctrl_button_view">View-painike</string> + <string name="ctrl_button_menu">Menu-painike</string> + <string name="ctrl_target_view">View</string> + <string name="ctrl_target_menu">Menu</string> + <string name="ctrl_target_up">D-padi ylös</string> + <string name="ctrl_target_down">D-padi alas</string> + <string name="ctrl_target_left">D-padi vasen</string> + <string name="ctrl_target_right">D-padi oikea</string> + <string name="ctrl_target_hidden">Piilotettu</string> + <string name="ctrl_editor_drag">Vedä siirtääksesi</string> + <string name="ctrl_editor_snap">Kohdistus</string> + <string name="ctrl_editor_save">Tallenna</string> + <string name="second_mode_none">Ei mitään</string> + <string name="second_mode_keyboard_trackpad">Näppäimistö + kosketuslevy</string> + <string name="second_mode_terminal">Pääte</string> + <string name="second_mode_deck_controls">Deck-kahvat + kosketuslevyt</string> + + <string name="rtinst_removing">Poistetaan Linux-ajoympäristöä</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">Poistetaan Linux-ajoympäristöä · %1$d kohta</item> + <item quantity="other">Poistetaan Linux-ajoympäristöä · %1$d kohtaa</item> + </plurals> + <string name="rtinst_removed">Linux-ajoympäristö poistettu</string> + <string name="rtinst_removal_failed">Linux-ajoympäristön poistoa ei voitu viimeistellä. Yritä poistoa uudelleen Asetuksissa.</string> + <string name="rtinst_removing_leftovers">Poistetaan ajoympäristön jäänteitä</string> + <string name="rtinst_downloading">Ladataan</string> + <string name="rtinst_verifying">Tarkistetaan</string> + <string name="rtinst_extracting">Puretaan</string> + <string name="rtinst_channel_description">Näytetään, kun Linux-ajoympäristöä asennetaan tai poistetaan</string> + <string name="rtinst_title_installing">Asennetaan Linux-ajoympäristöä</string> + <string name="rtinst_title_removing">Poistetaan Linux-ajoympäristöä</string> + + <string name="deskpkg_runtime_missing">Linux-ajoympäristöä ei ole asennettu</string> + <string name="deskpkg_no_checksum">Luettelossa ei ole tarkistussummaa kohteelle %1$s</string> + <string name="deskpkg_checksum_mismatch">Tarkistussumma ei täsmää - mitään ei muutettu</string> + <string name="deskpkg_installing">Asennetaan %1$s</string> + <string name="deskpkg_place_failed">AppImagea ei voitu sijoittaa</string> + <string name="deskpkg_extract_failed">Purkaminen epäonnistui</string> + <string name="deskpkg_install_failed">Asennus epäonnistui</string> + + <string name="flatpakmgr_setup_failed">Flatpak-asennus epäonnistui (poistumiskoodi %1$d)</string> + <string name="flatpakmgr_setup_unfinished">Flatpak-asennus ei valmistunut</string> + <string name="flatpakmgr_busy">Toinen kauppatehtävä on käynnissä</string> + <string name="flatpakmgr_not_ready">Asenna Flatpak ensin</string> + <string name="flatpakmgr_starting">Käynnistetään</string> + <string name="flatpakmgr_exit_status">Flatpak sulkeutui tilakoodilla %1$d</string> + <string name="flatpakmgr_installing">Asennetaan %1$s</string> + <string name="flatpakmgr_updating">Päivitetään %1$s</string> + <string name="flatpakmgr_removing">Poistetaan %1$s</string> + <string name="flatpakmgr_installing_step">Asennetaan %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_updating_step">Päivitetään %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_removing_step">Poistetaan %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_ref_translations">käännökset</string> + <string name="flatpakmgr_ref_graphics_drivers">näyttöajurit</string> + <string name="flatpakmgr_ref_media_codecs">mediakoodekit</string> + <string name="flatpakmgr_ref_runtime">%1$s-ajoympäristö</string> + <string name="flatpakmgr_ref_runtime_branch">%1$s-ajoympäristö %2$s</string> + + <string name="appimg_too_small">Tiedosto on liian pieni ollakseen AppImage</string> + <string name="appimg_unreadable">Tiedostoa ei voida lukea: %1$s</string> + <string name="appimg_not_linux">Tämä ei ole AppImage (se ei ole Linux-ohjelma)</string> + <string name="appimg_wrong_arch">Tämä AppImage ei ole tehty ARM64:lle (aarch64) eikä x86-tietokoneille</string> + <string name="appimg_not_appimage">Tämä on Linux-ohjelma, mutta ei AppImage</string> + <string name="appimg_wrong_type">Vain tyypin 2 AppImageja voi tuoda (tämä on tyyppiä %1$d)</string> + <string name="appimg_no_space_import">Tilaa ei ole riittävästi: tuonti vaatii noin %1$s</string> + <string name="appimg_no_space_update">Tilaa ei ole riittävästi: päivitys vaatii noin %1$s</string> + <string name="appimg_import_failed">Tuonti epäonnistui</string> + <string name="appimg_gone">Tämä AppImage on poissa</string> + <string name="appimg_update_failed">Päivitys epäonnistui</string> + <string name="appimg_extracting">Puretaan %1$s</string> + <string name="appimg_extract_failed">AppImagea ei voitu purkaa</string> + <string name="appimg_extract_failed_detail">AppImagea ei voitu purkaa: %1$s</string> + + <string name="deckymgr_stop_session_loader">Pysäytä käynnissä oleva istunto ennen Decky Loaderin asentamista</string> + <string name="deckymgr_download_failed">Decky Loaderin lataus epäonnistui</string> + <string name="deckymgr_size_mismatch">Decky Loaderin koko ei vastannut julkaisun metatietoja</string> + <string name="deckymgr_checksum_download_failed">Julkaisun tarkistussummaa ei voitu latata</string> + <string name="deckymgr_checksum_missing">Julkaisun tarkistussumma puuttuu</string> + <string name="deckymgr_checksum_invalid">Julkaisun tarkistussumma on virheellinen</string> + <string name="deckymgr_checksum_mismatch">Decky Loaderin tarkistussumma ei täsmää</string> + <string name="deckymgr_not_elf">Julkaisun tiedosto ei ole yhteensopiva 64-bittinen Linux-ohjelma</string> + <string name="deckymgr_session_started_loader">Istunto käynnistyi latauksen aikana; pysäytä se ennen Decky Loaderin asentamista</string> + <string name="deckymgr_loader_not_executable">PluginLoaderia ei voitu merkitä suoritettavaksi</string> + <string name="deckymgr_cef_failed">Steamin CEF-etävirheenkorjausta ei voitu ottaa käyttöön</string> + <string name="deckymgr_stage_failed">PluginLoaderia ei voitu valmistella</string> + <string name="deckymgr_loader_preserve_failed">Nykyistä PluginLoaderia ei voitu säilyttää</string> + <string name="deckymgr_loader_activate_failed">Uutta PluginLoaderia ei voitu ottaa käyttöön</string> + <string name="deckymgr_stop_session_plugin">Pysäytä käynnissä oleva istunto ennen laajennuksen asentamista</string> + <string name="deckymgr_loader_required">Asenna Decky Loader ennen laajennuksen tuomista</string> + <string name="deckymgr_zip_unreadable">Valittua ZIP-tiedostoa ei voitu lukea</string> + <string name="deckymgr_plugins_dir_failed">Deckyn laajennuskansiota ei voitu luoda</string> + <string name="deckymgr_extracting_zip">Puretaan laajennuksen ZIP-tiedostoa</string> + <string name="deckymgr_zip_one_folder">Laajennuksen ZIP-tiedostossa täytyy olla yksi ylätason laajennuskansio</string> + <string name="deckymgr_zip_missing_files">Laajennuksen ZIP-tiedostosta puuttuu plugin.json, package.json tai dist/index.js</string> + <string name="deckymgr_plugin_name_missing">Laajennuksen nimi puuttuu</string> + <string name="deckymgr_session_started_plugin">Istunto käynnistyi asennuksen aikana; pysäytä se ennen laajennuksen asentamista</string> + <string name="deckymgr_plugin_installed">Laajennus asennettu · käynnistä Steam uudelleen, jotta se latautuu</string> + <string name="deckymgr_plugin_install_failed">Laajennuksen ZIP-tiedoston asennus epäonnistui</string> + <string name="deckymgr_zip_too_many">Laajennuksen ZIP-tiedostossa on liian monta kohdetta</string> + <string name="deckymgr_zip_invalid_path">Laajennuksen ZIP-tiedostossa on virheellinen polku</string> + <string name="deckymgr_zip_duplicate_paths">Laajennuksen ZIP-tiedostossa on päällekkäisiä polkuja</string> + <string name="deckymgr_zip_unsafe_path">Laajennuksen ZIP-tiedostossa on turvaton polku</string> + <string name="deckymgr_folder_failed">Laajennuskansiota ei voitu luoda</string> + <string name="deckymgr_zip_too_large">Laajennuksen ZIP-tiedosto purkautuu yli 1 GiB:n rajan</string> + <string name="deckymgr_bundled_not_executable">Mukana tulevaa laajennuksen binääriä ei voitu merkitä suoritettavaksi</string> + <string name="deckymgr_remote_entry_invalid">Virheellinen remote_binary-merkintä</string> + <string name="deckymgr_remote_name_invalid">Virheellinen etäbinäärin nimi</string> + <string name="deckymgr_remote_url_https">Etäbinäärin URL-osoitteen täytyy käyttää HTTPS:ää</string> + <string name="deckymgr_remote_hash_invalid">Etäbinäärillä %1$s on virheellinen SHA-256-tiiviste</string> + <string name="deckymgr_bin_dir_failed">Laajennuksen bin-kansiota ei voitu luoda</string> + <string name="deckymgr_downloading_binary">Ladataan laajennuksen binääriä · %1$s</string> + <string name="deckymgr_binary_download_failed">Laajennuksen binääriä %1$s ei voitu latata</string> + <string name="deckymgr_binary_checksum_mismatch">Laajennuksen binäärin tarkistussumma ei täsmää · %1$s</string> + <string name="deckymgr_binary_not_executable">Laajennuksen binääriä ei voitu merkitä suoritettavaksi · %1$s</string> + <string name="deckymgr_binary_replace_failed">Laajennuksen binääriä ei voitu korvata · %1$s</string> + <string name="deckymgr_binary_install_failed">Laajennuksen binääriä ei voitu asentaa · %1$s</string> + <string name="deckymgr_folder_taken">Laajennuskansio kuuluu toiselle laajennukselle</string> + <string name="deckymgr_preserve_failed">Nykyistä laajennusta ei voitu säilyttää</string> + <string name="deckymgr_activate_failed">Tuotua laajennusta ei voitu ottaa käyttöön</string> + <string name="deckymgr_settings_failed">Deckyn laajennusasetuksia ei voitu päivittää</string> + <string name="deckymgr_restore_failed">Nykyistä laajennusta ei voitu palauttaa</string> + + <string name="adbfix_save_failed">Langattoman virheenkorjauksen tunnistetta ei voitu tallentaa</string> + <string name="adbfix_command_failed">ADB-komento epäonnistui (%1$d)</string> + <string name="adbfix_not_confirmed">Android ei vahvistanut, että lapsiprosessirajaa muutettiin: %1$s</string> + <string name="adbfix_no_pairing">Langattoman virheenkorjauksen laiteparitusta ei ole tallennettu</string> + <string name="adbfix_enter_address">Anna langattoman virheenkorjauksen nykyinen IP-osoite ja portti</string> + + <string name="adbpair_timed_out">Paritusikkunan odotus aikakatkaistiin. Yritä uudelleen DroidDeckistä.</string> + <string name="adbpair_discovery_failed_code">Paritusikkunaa ei voitu etsiä (%1$d). Tarkista, että Wi-Fi on päällä.</string> + <string name="adbpair_discovery_failed">Paritusikkunaa ei voitu etsiä: %1$s</string> + <string name="adbpair_six_digits">Anna koodin kaikki 6 numeroa</string> + <string name="adbpair_pairing">Paritetaan…</string> + <string name="adbpair_code_rejected">Tämä koodi ei toiminut. Avaa paritusikkuna uudelleen ja syötä sen uusi koodi.</string> + <string name="adbpair_connecting">Paritettu. Yhdistetään…</string> + <string name="adbpair_port_not_found">Paritettu, mutta langattoman virheenkorjauksen porttia ei löytynyt. Pidä langaton virheenkorjaus päällä ja yritä uudelleen.</string> + <string name="adbpair_applying">Otetaan asetusta käyttöön…</string> + <string name="adbpair_command_failed">Langattoman virheenkorjauksen komento epäonnistui</string> + <string name="adbpair_channel_name">Langattoman virheenkorjauksen käyttöönotto</string> + <string name="adbpair_channel_description">Kysyy langattoman virheenkorjauksen parituskoodia Asetusten ollessa auki</string> + <string name="adbpair_title_pair">Paritä langaton virheenkorjaus</string> + <string name="adbpair_waiting_text">Ota langaton virheenkorjaus käyttöön ja napauta ”Yhdistä laite paritunnuksella”.</string> + <string name="adbpair_waiting_long">Ota langaton virheenkorjaus käyttöön ja napauta ”Yhdistä laite paritunnuksella”. Pysy Asetuksissa: koodi syötetään tähän ilmoitukseen.</string> + <string name="adbpair_code_label">6-numeroinen paritunnus</string> + <string name="adbpair_code_hint">Napauta Syötä koodi ja kirjoita Asetuksissa näkyvät 6 numeroa.</string> + <string name="adbpair_title_code">Anna Wi-Fi-paritunnus</string> + <string name="adbpair_title_retry">Yritä koodia uudelleen</string> + <string name="adbpair_enter_code">Syötä koodi</string> + <string name="adbpair_title_working">Otetaan Steamia käyttöön</string> + <string name="adbpair_title_done">Steam on valmis</string> + <string name="adbpair_done_text">Napauta palataksesi DroidDeckiin. Voit sammuttaa langattoman virheenkorjauksen nyt.</string> + <string name="adbpair_title_failed">Langattoman virheenkorjauksen käyttöönotto pysähtyi</string> + + <string name="selfinst_http">Lataus vastasi HTTP %1$d</string> + <string name="selfinst_size_mismatch">Latauksen koko ei vastannut julkaistua APK-tiedostoa, ja se hylättiin - yritä uudelleen</string> + <string name="selfinst_checksum_mismatch">Lataus ei vastannut tarkistussummaansa, ja se hylättiin - yritä uudelleen</string> + <string name="selfinst_wrong_package">Päivitys on Android-paketille %1$s, ei %2$s</string> + <string name="selfinst_metadata_signer">Päivityksen metatiedot eivät mainitse DroidDeckin julkaisun allekirjoitusavainta</string> + <string name="selfinst_unreadable">Android ei pystynyt lukemaan ladattua APK-tiedostoa</string> + <string name="selfinst_apk_wrong_package">Ladattu APK on sovellukselle %1$s, ei %2$s</string> + <string name="selfinst_version_mismatch">Ladattu APK ilmoittaa versionCode-arvon %1$d, ei julkaistua %2$d</string> + <string name="selfinst_downgrade">Android ei asenna versionCode-arvoa %1$d asennetun %2$d päälle</string> + <string name="selfinst_no_signing">Ladatussa APK-tiedostossa ei ole allekirjoitustietoja</string> + <string name="selfinst_wrong_signer">Ladattu APK ei ole allekirjoitettu DroidDeckin julkaisuavaimella</string> + <string name="appupd_http">Päivitysluettelo vastasi HTTP %1$d</string> + + <string name="loadstate_starting">Käynnistetään istuntoa…</string> + <string name="loadstate_elapsed">%1$d:%2$02d kulunut · yhä käynnissä</string> + <string name="loadstate_steam_update">Ladataan Steam-asiakasohjelman päivitystä · %1$d %%</string> + <string name="loadstate_steam_client">Ladataan Steam-asiakasohjelmaa · %1$s</string> + + <string name="pextra_busy">Yhteensopivuustyökalun asennus on jo käynnissä</string> + <string name="pextra_stop_session">Pysäytä käynnissä oleva istunto ennen yhteensopivuustyökalujen asentamista</string> + <string name="pextra_no_release">ARM64-julkaisua kohteelle %1$s ei löytynyt</string> + <string name="pextra_no_space">Kohteen %1$s asentaminen vaatii vähintään 4 GB vapaata tilaa latauksen koon lisäksi</string> + <string name="pextra_downloading">Ladataan %1$s %2$s</string> + <string name="pextra_retrying">Yritetään latausta uudelleen · yritys %1$d/%2$d</string> + <string name="pextra_interrupted">Lataus keskeytyi</string> + <string name="pextra_download_failed">Lataus epäonnistui; osittainen tiedosto säilytetään, jotta uusi yritys voi jatkaa siitä</string> + <string name="pextra_too_big">Lataus ylitti julkaisun koon ja poistettiin</string> + <string name="pextra_incomplete">Lataus jäi kesken; osittainen tiedosto säilytetään jatkettavaa yritystä varten</string> + <string name="pextra_verifying">Tarkistetaan latausta</string> + <string name="pextra_checksum_unreadable">Julkaisun tarkistussummaa ei voitu lukea; yritä uudelleen</string> + <string name="pextra_no_checksum">%1$s %2$s ei julkaise tarkistussummaa; mitään ei asennettu</string> + <string name="pextra_checksum_mismatch">Tarkistussumma ei täsmää; lataus poistettiin</string> + <string name="pextra_session_started">Istunto käynnistyi latauksen aikana; pysäytä se ennen yhteensopivuustyökalujen asentamista</string> + <string name="pextra_installing">Asennetaan %1$s</string> + <string name="pextra_install_failed">%1$s-asennus epäonnistui (poistumiskoodi %2$d)</string> + <string name="pextra_could_not_install">Kohdetta %1$s ei voitu asentaa</string> + <string name="esync_fetching">Haetaan droiddeck-esync-pakettia</string> + + <string name="cmgr_fex_linux_helpers">Linux-apuohjelmien kanssa</string> + <string name="cmgr_fex_dlls_only">Vain DLL-tiedostot</string> + <string name="cmgr_original_proton_updated">Alkuperäinen (Proton päivitetty viimeisimmän vaihdon jälkeen)</string> + <string name="cmgr_changed_outside">Muutettu Komponenttien ulkopuolella</string> + <string name="cmgr_not_present">puuttuu</string> + <string name="cmgr_original_version">Alkuperäinen %1$s</string> + <string name="cmgr_original_from">Alkuperäinen versiosta %1$s</string> + <string name="cmgr_swap_queued">%1$s %2$s siirtyy kohteeseen %3$s, kun sillä käynnissä oleva peli suljetaan.</string> + <string name="cmgr_swap_done">%1$s %2$s on nyt kohteessa %3$s. Se otetaan käyttöön seuraavan kerran, kun peli käynnistyy.</string> + <string name="cmgr_original_missing">Tätä alkuperäistä pakettia ei ole tallennettu</string> + <string name="cmgr_restore_queued">%2$s:n %1$s palautuu alkuperäiseen, kun sillä käynnissä oleva peli suljetaan.</string> + <string name="cmgr_restore_done">%2$s:n %1$s palautettu %3$s-alkuperäisestä.</string> + <string name="cmgr_package_in_use">%1$s on käytössä kohteessa %2$s. Vaihda tämä Proton ensin joksikin toiseksi.</string> + <string name="cmgr_package_not_stored">%1$s ei ole tallennettu</string> + <string name="cmgr_deleted">Poistettu %1$s.</string> + <string name="cmgr_original_kept">Asennetun version alkuperäinen säilytetään: se on tie takaisin.</string> + <string name="cmgr_not_stored">Ei tallennettu</string> + <string name="cmgr_deleted_original">Poistettu %2$s:n %1$s-alkuperäinen.</string> + <string name="cmgr_nightlies_only">Lataukset tulevat vain Nightlies-julkaisuista</string> + <string name="cmgr_list_predates_checksums">Tämä pakettiluettelo on tarkistussummia vanhempi - päivitä se ja yritä uudelleen</string> + <string name="cmgr_checksum_mismatch">Tarkistussumma ei täsmää</string> + + <string name="sprefs_upscaler_linear">Lineaarinen</string> + <string name="sprefs_upscaler_nearest">Lähin</string> + <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (laatu)</string> + <string name="sprefs_upscaler_sharpen">Vain terävöinti</string> + <string name="sprefs_lod_bias_auto">Automaattinen (sovita skaalaukseen)</string> + + <string name="look_off_desc">Ei mitään käytössä.</string> + <string name="look_game_clarity">Pelin terävyys</string> + <string name="look_game_clarity_desc">Terävöinti hieman kontrastilla - arkikäyttöön.</string> + <string name="look_vivid">Eloisa</string> + <string name="look_vivid_desc">Väri viritetty voimakkaaksi. Deband päällä, jotta taivaat eivät raidoitu.</string> + <string name="look_cinematic">Elokuvamainen</string> + <string name="look_cinematic_desc">Syvempi kontrasti, viileämpi väri, reunat pehmennetty.</string> + <string name="look_competitive">Kilpailullinen</string> + <string name="look_competitive_desc">Varjot nostettuna ja voimakkaalla terävöinnillä - huomaa pelaajat ensin.</string> + <string name="look_adaptive_sharpen">Mukautuva terävöinti</string> + <string name="look_adaptive_sharpen_desc">Vain terävöinti, väri koskematta.</string> + <string name="look_filmic">Filmimäinen</string> + <string name="look_filmic_desc">Filmimäinen käyrä, hieman vaimennettu.</string> + <string name="look_arcade">Arcade</string> + <string name="look_arcade_desc">Räväkkä ja iskevä.</string> + <string name="look_retro_crt">Retro CRT</string> + <string name="look_retro_crt_desc">CRT-putken ja analogisen signaalin vaikutukset yhdessä.</string> + <string name="look_upscale_sharp">Terävä skaalaus</string> + <string name="look_upscale_sharp_desc">Käytettäväksi, kun ajetaan näytön tarkkuutta pienemmällä resoluutiolla.</string> + <string name="look_pixel_clean">Puhtaat pikselit</string> + <string name="look_pixel_clean_desc">Terävät pikselit 2D- ja vanhoille peleille.</string> + <string name="look_anime_edge">Anime-reunat</string> + <string name="look_anime_edge_desc">Sarjakuvamaiset ääriviivat ja terävöinti.</string> + + <string name="gpuinfo_not_adreno">Ei Adreno-grafiikkapiiri</string> + <string name="gpuinfo_supported">Tuettu</string> + <string name="gpuinfo_experimental">Kokeellinen: sen ajurit ovat testiversioita</string> + <string name="gpuinfo_below_tested">Testatun laitteiston alapuolella (Adreno 725 ja uudemmat): ei välttämättä toimi</string> + <string name="gpuinfo_unsupported">Ei tuettu: DroidDeck vaatii Adreno (Snapdragon) -grafiikkapiirin</string> + + <string name="turnip_rate_limited">GitHubin käyttörajoitus tuli vastaan - yritä myöhemmin uudelleen</string> + <string name="turnip_github_http">GitHub vastasi HTTP %1$d</string> + <string name="turnip_download_http">lataus vastasi HTTP %1$d</string> + <string name="turnip_list_predates_checksums">Tämä ajuriluettelo on tarkistussummia vanhempi - päivitä se ja yritä uudelleen</string> + <string name="turnip_checksum_mismatch">Tarkistussumma ei täsmää - lataus hylättiin</string> + <string name="turnip_not_adrenotools">Ei AdrenoTools-ajuri: %1$s</string> + <string name="turnip_reject_no_meta">ei meta.json-tiedostoa</string> + <string name="turnip_reject_meta_unreadable">meta.json ei ole luettavissa (%1$s)</string> + <string name="turnip_reject_linux_driver">tämä on Linux-ajoympäristön ajuri (tuo se kohdasta ”Linux-ajoympäristön ajuri”)</string> + <string name="turnip_reject_wayland">tämä on Wayland-peliajuri, jolle tällä sovelluksella ei ole käyttöä</string> + <string name="turnip_reject_no_library_name">meta.json ei nimeä libraryName-arvoa, joten mitään ei voitu antaa AdrenoToolsille</string> + <string name="turnip_reject_library_missing">meta.json nimeää %1$s, jota ei ole zip-tiedostossa</string> + <string name="turnip_reject_not_aarch64">%1$s ei ole 64-bittinen AArch64 ELF-jaettu kirjasto</string> + <string name="turnip_reject_glibc">%1$s linkittää glibc:n - tämä on ”-Linux”-Turnip, joka kuuluu kohtaan ”Linux-ajoympäristön ajuri”</string> + <string name="lvd_no_library">Tässä zip-tiedostossa ei ole libvulkan_freedreno*.so-tiedostoa. Android (AdrenoTools)- tai -Wayland-Turnip-zip ei ole Linux-ajoympäristön ajuri.</string> + <string name="lvd_not_aarch64">%1$s ei ole 64-bittinen AArch64 ELF-jaettu kirjasto.</string> + <string name="lvd_not_glibc">%1$s ei ole glibc-ajuri - se linkittää Androidin libc:n. Linux-ajoympäristö tarvitsee ”-Linux”-zipin; tavallista tai ”-Wayland”-Turnipia Steam-asiakasohjelma ei voi ladata lainkaan.</string> + <string name="bundle_no_manifest">Ei ajuripaketti: ei manifest.json-tiedostoa, joka nimeäisi android- ja linux-ajurin</string> + <string name="bundle_entry_outside">Ei ajuripaketti: %1$s osoittaa sen ulkopuolelle</string> + <string name="bundle_too_large">Ei ajuripaketti: se purkautuu yli %1$d MB:n kokoiseksi</string> + + <string name="gstore_sd_card">SD-kortti</string> + <string name="gstore_folder">Kansio</string> + + <string name="logshare_chooser">Jaa istuntolokit</string> + + <string name="hdr_needs_android_10">vaatii Android 10:n tai uudemman (näyttötasot väriavaruudella)</string> + <string name="hdr_none">tämä näyttö ei ilmoita HDR-tukea</string> + <string name="hdr_not_hdr10">tämä näyttö tukee %1$s, ei HDR10:tä</string> + + <string name="gpu_this_gpu">tämä grafiikkapiiri</string> + <string name="phantom_title_on">Lapsiprosessiraja on päällä</string> + <string name="phantom_title_unset">Lapsiprosessirajaa ei ole asetettu</string> + <string name="phantom_title_unreadable">Lapsiprosessirajaa ei voitu tarkistaa</string> + <string name="phantom_title_off">Lapsiprosessiraja on pois päältä</string> + <string name="phantom_title_not_required">Lapsiprosessirajaa ei vaadita</string> + <string name="phantom_instructions_fix">%1$s Jos se ei ole mahdollista, yhdistä tämä laite tietokoneeseen ADB:llä ja suorita alla oleva komento. Palaa tähän ja tarkista uudelleen.</string> + <string name="phantom_instructions_off">Steam-istunnot voivat käynnistyä.</string> + <string name="phantom_instructions_not_applicable">Tämä Android-versio ei käytä tätä rajaa.</string> + <string name="phantom_fix_toggle">Ota käyttöön ”Poista käytöstä lapsiprosessien rajoitukset” Kehittäjäasetuksissa, ja pidä Kehittäjäasetukset päällä sen jälkeen.</string> + <string name="phantom_fix_no_toggle">Tässä Android-versiossa ei ole kytkintä sille. Käytä langatonta virheenkorjausta muuttaaksesi sitä tältä laitteelta.</string> + <string name="upd_block_no_apk">Tälle versiolle ei ole latausta tähän DroidDeck-kopioon.</string> + <string name="upd_block_version_code">Android ei asenna sitä tämän version päälle, koska sen versionCode %1$d on pienempi kuin asennettu %2$d.</string> + <string name="upd_fail_downgrade">Android ei asenna vanhempaa versiota uudemman päälle. Pysy tässä versiossa, kunnes kanava saavuttaa sen.</string> + <string name="upd_fail_signature">Tämä DroidDeck-kopio on allekirjoitettu eri tavalla (paikallinen käännös?), joten Android ei päivitä sitä paikan päällä.</string> + <string name="upd_fail_storage">Vapaata tilaa ei ole riittävästi päivitykselle.</string> + <string name="upd_fail_aborted">Päivitys peruutettiin.</string> + <string name="upd_fail_with_message">Päivitys ei asentunut: %1$s</string> + <string name="upd_fail_generic">Päivitys ei asentunut.</string> + <string name="saves_layout_zip">%1$s-zip</string> + <string name="saves_zip">zip</string> + <string name="saves_not_launched">%1$s ei ole käynnistetty vielä</string> + <string name="saves_none_found">Tallennustiedostoja ei löytynyt kohteelle %1$s</string> + <string name="saves_not_save_zip">%1$s ei sisällä drive_c-kansiota, joten se ei ole GameHubin tai Winlatorin tallennuszip</string> + <string name="gstore_cannot_create">kansioita ei voida luoda kohteeseen %1$s</string> + <string name="gstore_cannot_write">kohteeseen %1$s ei voida kirjoittaa</string> + <string name="gstore_cannot_write_detail">kohteeseen %1$s ei voida kirjoittaa (%2$s)</string> + <string name="gfiles_folder_unavailable">Kansio ei ole käytettävissä</string> + <string name="gfiles_name_taken">Toinen tiedosto käyttää jo nimeä %1$s</string> + <string name="gfiles_art_unavailable">Kuvituskansio ei ole käytettävissä</string> + <string name="gfiles_unrelated_inventory">Kansiossa on muuhun synkronointiin liittyvä luettelo</string> + <string name="gfiles_remove_failed">Kohdetta %1$s ei voitu poistaa</string> + <string name="gfiles_outside_folder">Tiedosto siirtyy vientikansion ulkopuolelle</string> + <string name="cmgr_fex_unnamed">FEX (versio, jolla ei ole nimeä)</string> + <string name="cmgr_no_profile">ei profile.json-tiedostoa kohteessa %1$s</string> + <string name="cmgr_not_component_package">%1$s ei ole Linux-komponenttipaketti (tyyppi %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s vapaana</string> + <string name="mode_storage_auto_short">Automaattinen</string> + <string name="appupd_catalog_rejected">Päivitysluettelo ei läpäissyt tarkistuksia (%1$s)</string> + <string name="main_adb_clip_label">DroidDeckin lapsiprosessiasetus</string> + <string name="setup_signed_in_offline_unnamed">Kirjautunut · offline-tila</string> + <string name="setup_signed_in_unnamed">Kirjautunut</string> + <string name="driver_tag_bundled">MUKANA</string> + <string name="driver_tag_downloaded">LADATTU</string> + <string name="driver_tag_imported">TUOTU</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Istunnon näyttö</string> + <string name="display_resolution">Tarkkuus</string> + <string name="display_game_fit">16:9-pelin sovitus</string> + <string name="display_game_fit_hint">Miten 16:9-pelit sovitetaan tähän näyttöön. Seuraava istunto.</string> + <string name="display_game_fit_unavailable">Valitse Sovita näyttöön tai mukautettu koko, joka on 16:9:ää kapeampi.</string> + <string name="display_preserve">Säilytä kuvasuhde</string> + <string name="display_stretch">Venytä näyttöön</string> + <string name="display_image_scaling">Kuvan skaalaus</string> + <string name="display_filter">Skaalaussuodatin</string> + <string name="display_filter_hint">Yhteinen Steamille ja Työpöydälle.</string> + <string name="display_filter_live">Vaikuttaa välittömästi. Yhteinen Steamille ja Työpöydälle.</string> + <string name="display_filter_note">FSR, GSR ja NIS vaikuttavat, kun istunnon kuvaa suurennetaan. Vain terävöinti toimii myös suurentamatta.</string> + <string name="display_sharpness">Suodattimen terävyys</string> + <string name="display_frame_rate">Kuvanopeus</string> + <string name="display_look_filter">%1$s Asettaa skaalauksen tilaan %2$s.</string> + <string name="display_match_screen">Sovita näyttöön (%1$d×%2$d)</string> + <string name="display_follow_screen">Seuraa näyttöä (koko muuttuu taitettuna tai avattuna)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Näytä laitteen tiedot</string> + <string name="device_info_hide">Piilota laitteen tiedot</string> + <string name="device_info_loading">Luetaan laitetta…</string> + <string name="device_info_copy">Kopioi</string> + <string name="device_info_copy_hint">Kopioi näitä tietoja virheraporttiin.</string> + <string name="device_info_copied">Kopioitu.</string> + <string name="force_ssbs_title">Pakota SSBS</string> + <string name="force_ssbs_hint">Pitää päällä suoritintehoasetuksen, jonka Wine jatkuvasti sammuttaa. Voi nopeuttaa Windows-pelejä Snapdragon 8 Elite -piirillä; vanhemmilla piireillä vaikutus on pieni. Vaikuttaa seuraavasta pelin käynnistyksestä alkaen.</string> + + <string name="rail_stores">Kaupat</string> + <string name="source_steam">Steam</string> + <string name="source_added">Mukautettu</string> + <string name="setup_stores">Kaupat</string> + <string name="setup_stores_open_on">Avaa kauppa näytöllä</string> + <string name="setup_stores_speed">Latausnopeuden taso</string> + <string name="setup_stores_engine">Kauppojen moottori</string> + <string name="setup_stores_engine_chip_ready">● Moottori valmis</string> + <string name="setup_stores_engine_chip_missing">Ei koottu</string> + <string name="stores_tier_balanced">Tasapainotettu</string> + <string name="stores_tier_fast">Nopea</string> + <string name="stores_tier_max">Maksimi</string> + <string name="stores_title">Kaupat</string> + <string name="stores_settings">Kauppojen asetukset</string> + <string name="stores_settings_eyebrow">Asetukset</string> + <string name="stores_chip_signed_out">(ei kirjautunut)</string> + <string name="stores_tab_store">Kauppa</string> + <string name="stores_tab_installed">Asennetut</string> + <string name="stores_tab_library">Kirjasto</string> + <string name="stores_tab_all">Kaikki</string> + <string name="stores_tab_downloads">Lataukset</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Kirjaudu sisään</string> + <string name="stores_signout_named">%1$s · kirjaudu ulos</string> + <string name="stores_signout_title">Kirjaudutaanko ulos %1$s:stä?</string> + <string name="stores_signout_note">Asennetut pelit säilyvät. Kirjasto haetaan uudelleen, kun kirjaudut takaisin sisään.</string> + <string name="stores_signout_stay">Pysy kirjautuneena</string> + <string name="stores_signout_go">Kirjaudu ulos</string> + <string name="stores_search_catalog">Hae %1$s-luettelosta…</string> + <string name="stores_search_installed">Hae asennetuista peleistä…</string> + <string name="stores_search_library">Hae kirjastostasi…</string> + <string name="stores_search_open">Hae</string> + <string name="stores_installed_empty">Ei mitään asennettuna</string> + <string name="stores_library_loading">Latautuu</string> + <string name="stores_nothing_matches">Ei pelejä</string> + <string name="stores_shelves_loading">Ladataan kauppaa…</string> + <string name="stores_amazon_no_catalog">Ei luetteloa</string> + <string name="stores_shelf_new">Uutuudet</string> + <string name="stores_shelf_deals">Tarjoukset</string> + <string name="stores_shelf_free">Ilmaiset pelit</string> + <string name="stores_shelf_trending">Nousussa</string> + <string name="stores_shelf_library">Kirjastosi</string> + <string name="stores_play">Pelaa</string> + <string name="stores_install">Asenna</string> + <string name="stores_resume_install">Jatka asennusta</string> + <string name="stores_dl_interrupted">Keskeytynyt</string> + <string name="stores_install_size">Asenna · %1$s</string> + <string name="stores_get_free">Hanki ilmaiseksi</string> + <string name="stores_view_on">Näytä %1$s:ssä</string> + <string name="stores_buy_price">Osta · %1$s</string> + <string name="stores_installed_chip">Asennettu</string> + <string name="stores_in_library">Kirjastossa</string> + <string name="stores_free">Ilmainen</string> + <string name="stores_resume">Jatka</string> + <string name="stores_refresh">Päivitä</string> + <string name="stores_show_mature">Näytä aikuisille tarkoitettu sisältö</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Offline</string> + <string name="epic_card_none">Pois</string> + <string name="epic_eos">Epic-kirjautuminen (EOS)</string> + <string name="epic_offline">Käynnistä offline-tilassa</string> + <string name="epic_resolve">Ratkaise Epic-kirjautuminen</string> + <string name="store_cloud_saves">Pilvitallennukset</string> + <string name="cloud_upload">Lähetä pilveen</string> + <string name="cloud_download">Lataa pilvestä</string> + <string name="cloud_none">Ei pilvitallennuksia</string> + <string name="cloud_conflict">Pilviristiriita</string> + <string name="cloud_conflict_files">Pilviristiriita · %1$d tiedostoa</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Säilytä pilvi</string> + <string name="cloud_keep_local">Säilytä paikallinen</string> + <string name="cloud_checking">Tarkistetaan pilveä…</string> + <string name="cloud_last_sync">Viimeisin synkronointi %1$s</string> + <string name="cloud_never">Ei synkronoitu vielä</string> + <string name="cloud_done">%1$d tiedostoa</string> + <string name="stores_eyebrow_installed">asennettu</string> + <string name="stores_eyebrow_library">kirjastossasi</string> + <string name="stores_eyebrow_store">kauppa</string> + <string name="achievements_title">Saavutukset</string> + <string name="achievements_count">%1$d / %2$d avattu</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Avattu · %1$d</string> + <string name="achievements_group_locked">Lukittu · %1$d</string> + <string name="achievements_group_hidden">Piilotettu · %1$d</string> + <string name="achievements_hidden">Piilotettu saavutus</string> + <string name="achievements_hidden_detail">Tiedot näytetään, kun se on avattu.</string> + <string name="achievements_all">Kaikki saavutukset avattu</string> + <string name="achievements_reading">Ladataan…</string> + <string name="achievements_signed_out">Kirjaudu sisään %1$s-palveluun nähdäksesi saavutuksesi.</string> + <string name="achievements_unreachable">Yhteyttä %1$s-palveluun ei saatu. Tarkista uudelleen, kun olet verkossa.</string> + <string name="achievements_none">%1$s ei listaa saavutuksia tälle pelille.</string> + <string name="achievements_show_all">Näytä kaikki %1$d</string> + <string name="achievements_dialog_title">Saavutukset · %1$s</string> + <string name="stores_in_steam">✓ Steamissa</string> + <string name="stores_uninstall">Poista asennus</string> + <string name="stores_uninstalling">Poistetaan asennusta…</string> + <string name="stores_uninstall_title">Poistetaanko %1$s:n asennus?</string> + <string name="stores_uninstall_body_size">Vapauttaa %1$s ja poistaa sen Steamista.</string> + <string name="stores_uninstall_body">Poistaa sen tiedostot ja Steam-pikakuvakkeen.</string> + <string name="stores_launch_not_registered">Steam huomaa pelin seuraavan käynnistyksensä yhteydessä; käynnistä se siellä, tai käynnistä Steam nyt.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Steamin valinta pelikohtaisesti</string> + <string name="stores_card_audio">Äänet</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Komponentit</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Moottori</string> + <string name="stores_card_engine_value">%1$s · natiivi</string> + <string name="stores_card_engine_java">%1$s · sisäänrakennettu</string> + <string name="stores_how_it_runs">Miten se toimii</string> + <string name="stores_kv_fetched">Haettu</string> + <string name="stores_kv_fetched_value">%1$s, tilillasi</string> + <string name="stores_kv_installed_to">Asennettu kohteeseen</string> + <string name="stores_kv_installed_to_value">Pelien tallennustila (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Käynnistäjä</string> + <string name="stores_kv_launched_by_value">Steam ja Proton, sama reitti kuin Steam-peleillä</string> + <string name="stores_kv_launched_by_epic">Steam ja Proton · Epic-kirjautuminen joka käynnistyksellä, EOS-overlay pois</string> + <string name="stores_kv_launched_by_amazon">Steam ja Proton · Amazon Games Services -muuttujat asetettu</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">lisätty Steamiin asennuksen yhteydessä (Deck-käyttöliittymä + Steam Input)</string> + <string name="stores_downloads_empty">Ei latauksia</string> + <string name="stores_dl_paused">Keskeytetty</string> + <string name="stores_dl_queued">Jonossa</string> + <string name="stores_dl_queued_at">Jonossa · #%1$d</string> + <string name="stores_dl_cancelled">Peruutettu</string> + <string name="stores_dl_failed">Epäonnistui</string> + <string name="stores_dl_failed_reason">Epäonnistui · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d %%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d %%</string> + <string name="stores_dl_pause">Keskeytä</string> + <string name="stores_dl_resume">Jatka</string> + <string name="stores_dl_cancel">Peruuta</string> + <string name="stores_dl_retry">Yritä uudelleen</string> + <string name="stores_dl_clear">Tyhjennä</string> + <string name="stores_dl_clear_all">Tyhjennä valmiit</string> + <string name="stores_stage_manifest">Manifesti</string> + <string name="stores_stage_download">Lataus</string> + <string name="stores_stage_verify">Tarkistus</string> + <string name="stores_stage_install">Asennus</string> + <string name="stores_stage_done">Valmis</string> + <string name="stores_stage_checking">Tarkistetaan</string> + <string name="stores_stage_downloading">Latautuu</string> + <string name="stores_dl_cancel_confirm">Poistetaanko lataus?</string> + <string name="stores_dl_failed_kept">Epäonnistui · %1$s säilytetty</string> + <string name="stores_stage_verifying">Tarkistetaan</string> + <string name="stores_stage_installing">Asennetaan</string> + <string name="stores_dl_active_percent">%1$s %2$d %%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d tiedostoa</string> + <string name="stores_download_manager">Latausten hallinta</string> + <string name="stores_parallel">Samanaikaisia latauksia</string> + <string name="stores_install_where">Asenna kohteeseen</string> + <string name="stores_install_title">Asenna %1$s:</string> + <string name="stores_target_internal">Sisäinen</string> + <string name="stores_target_free">%1$s vapaana</string> + <string name="stores_notification_channel">Kauppalataukset</string> + <string name="stores_notification_channel_hint">Näyttää pelin latauksen etenemisen ja pitää lataukset käynnissä taustalla</string> + <string name="stores_notification_one">Latautuu</string> + <string name="stores_notification_many">%1$d latausta</string> + <string name="stores_notification_waiting">Valmistellaan…</string> + <string name="stores_notification_done_channel">Valmiit kauppalataukset</string> + <string name="stores_notification_installed">%1$s asennettu</string> + <string name="stores_notification_failed">%1$s:n lataus epäonnistui</string> + <string name="stores_notification_delete">Poista</string> + <string name="stores_notification_keep">Säilytä</string> + <string name="stores_login_title">Kirjaudu sisään palveluun %1$s</string> + <string name="stores_login_provider_title">Kirjaudu sisään palveluntarjoajallasi</string> + <string name="stores_login_reload">Lataa uudelleen</string> + <string name="stores_login_close">Sulje</string> + <string name="stores_login_retry">Yritä uudelleen</string> + <string name="stores_login_finishing">Viimeistellään kirjautumista…</string> + <string name="stores_login_error_network">Kirjautumissivua ei voitu latata: %1$s</string> + <string name="stores_login_error_idp_blocked">Tämä palveluntarjoaja ei salli kirjautumista upotetulta sivulta. Kirjaudu sisään kaupan salasanalla sen sijaan.</string> + <string name="stores_login_error_http">Kirjautumissivu vastasi virheellä (HTTP %1$d).</string> + <string name="stores_login_error_ssl">Yhteys kirjautumissivulle ei ole suojattu.</string> + <string name="stores_login_error_verification">Kirjautumista ei voitu vahvistaa. Yritä uudelleen.</string> + <string name="stores_login_error_generic">Kirjautuminen epäonnistui. Yritä uudelleen.</string> + <string name="common_close">Sulje</string> + <string name="common_back">Takaisin</string> + <string name="common_edit">Muokkaa</string> + <string name="games_add_all">Lisää kaikki tämän kansion pelit</string> + <string name="games_added_none">Ei mitään uutta tässä kansiossa</string> + <string name="edit_name">Nimi</string> + <string name="edit_target">Kohde</string> + <string name="edit_start_in">Käynnistyskansio</string> + <string name="edit_launch_options">Käynnistysasetukset</string> + <string name="edit_artwork">Kuvitus</string> + <string name="edit_other_exe">Toinen exe…</string> + <string name="edit_remove">Poista</string> + <string name="art_cover">Kansikuva</string> + <string name="art_background">Tausta</string> + <string name="art_logo">Logo</string> + <string name="art_icon">Kuvake</string> + <string name="art_src_folder">Pelin kansio</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">Oma tiedostosi</string> + <string name="art_src_exe">Exe-tiedostosta</string> + <string name="art_src_auto">Automaattinen</string> + <string name="art_pick_image">Valitse kuva…</string> + <string name="art_pick_title">Valitse kuva: %1$s</string> + <string name="art_reset">Palauta automaattiseksi</string> + <plurals name="games_added_n"> + <item quantity="one">%1$d peli lisätty</item> + <item quantity="other">%1$d peliä lisätty</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="one">%1$d on jo Peleissä</item> + <item quantity="other">%1$d on jo Peleissä</item> + </plurals> + <string name="hop_back">B · Takaisin %1$s</string> + <string name="hop_from_games">peliin</string> + <string name="hop_from_play">Käynnistysasetuksiin</string> + <string name="sheet_graphics_drivers">Näyttöajurit</string> + <string name="comp_this_proton">Tämä Proton</string> + <string name="comp_every_proton">Kaikki Protonit</string> + <string name="comp_for_every_proton">Kaikille Protoneille</string> + <string name="comp_available_count">Saatavilla · %1$d</string> + <string name="comp_checked">Tarkistettu %1$s</string> + <string name="comp_check_all">Tarkista uudet komponentit ja ajurit</string> + <string name="comp_get">Hae</string> + <string name="comp_proton_bar">%1$s · alla olevat DXVK, VKD3D-Proton ja FEX ovat tämän Protonin</string> + <string name="comp_device_tested">%1$s · testattu</string> + <string name="comp_device_untested">%1$s · testaamaton</string> + <string name="comp_device_unsupported">%1$s · ei tuettu</string> + <string name="gpu_auto_keeps">Pitää suositellun parin ajan tasalla</string> + <string name="gpu_bar_auto">Automaattinen · %1$s</string> + <string name="gpu_separately">Ajoympäristön ja näytön ajurit erikseen</string> + <string name="gpu_use_title">Käytetäänkö %1$s?</string> + <string name="proton_remove_title">Poistetaanko %1$s?</string> + <string name="setup_tab_diagnostics">Diagnostiikka</string> + <string name="setup_ready">Valmis pelattavaksi</string> + <string name="setup_not_ready">Ei vielä valmis pelattavaksi</string> + <string name="setup_ready_runtime">ajoympäristö %1$s</string> + <string name="setup_ready_limit_off">lapsiprosessiraja pois</string> + <string name="setup_ready_signed_in">kirjautuneena nimellä %1$s</string> + <string name="setup_needs_you">Kun jokin tarvitsee sinua</string> + <string name="setup_group_look">Ulkoasu</string> + <string name="setup_group_device">Tämä laite</string> + <string name="setup_group_art">Kirjaston kuvitus</string> + <string name="flathub_eyebrow">LINUX-SOVELLUKSET</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Työpöytäsovelluksia Linux-työpöydälle, avataan Työpöydältä.</string> + <string name="flathub_setup">Ota Flathub käyttöön</string> + <string name="flathub_remove_title">Poistetaanko %1$s?</string> + <string name="flathub_remove_note">Sen asetukset ja tiedostot säilyvät.</string> + <string name="flathub_keep">Säilytä</string> + <string name="desktop_size">Koko levyllä</string> + <string name="desktop_uninstall_title">Poistetaanko työpöydän asennus?</string> + <string name="desktop_all_systems">Kaikki</string> + <string name="gog_achievements">GOG-saavutukset</string> + <string name="gog_achievements_hint">Synkronoi saavutukset tuetuille GOG-peleille. Tulee voimaan seuraavalla käynnistyksellä.</string> + <string name="gog_achievements_off">Saavutukset pois käytöstä</string> + <string name="gog_achievements_on">Saavutukset käytössä</string> + <string name="gog_achievements_signin">Kirjaudu GOG:iin uudelleen</string> + <string name="gog_achievements_uploaded">Saavutukset lähetetty</string> + <string name="gog_achievements_pending">Saavutuksen lähetys odottaa</string> + <string name="gog_achievements_enable_store">Ota GOG-saavutukset käyttöön kauppa-asetuksissa</string> + <string name="gog_achievements_unavailable">Saavutukset ei käytettävissä; tarkista istunnon lokit</string> + <string name="achievement_unlocked">Saavutus avattu</string> + <string name="touch_paste">Liitä</string> + <string name="touch_dpad">Ristiohjain</string> + <string name="touch_left_stick">Vasen tatti</string> + <string name="touch_right_stick">Oikea tatti</string> + <string name="touch_menu">Valikko</string> + <string name="touch_keyboard">Näppäimistö</string> + <string name="touch_dpad_up">Ristiohjain ylös</string> + <string name="touch_dpad_down">Ristiohjain alas</string> + <string name="touch_dpad_left">Ristiohjain vasen</string> + <string name="touch_dpad_right">Ristiohjain oikea</string> + <string name="touch_title">Kosketusohjaimet</string> + <string name="touch_controller">Kosketusohjain</string> + <string name="touch_mouse">Hiiri</string> + <string name="touch_on">Päällä</string> + <string name="touch_off">Pois</string> + <string name="touch_speed_slow">Hidas</string> + <string name="touch_speed_normal">Normaali</string> + <string name="touch_speed_fast">Nopea</string> + <string name="touch_edit_layout">Muokkaa asettelua</string> + <string name="touch_droiddeck_menu">DroidDeck-valikko</string> + <string name="touch_drag_hint">Vedä ohjaimia näytölle</string> + <string name="touch_arrange">Järjestä</string> + <string name="touch_discard">Hylkää</string> + <string name="touch_saving">Tallennetaan…</string> + <string name="touch_icon">Kuvake</string> + <string name="touch_narrower">L−</string> + <string name="touch_wider">L+</string> + <string name="touch_shorter">K−</string> + <string name="touch_taller">K+</string> + <string name="touch_hide">Piilota</string> + <string name="touch_color_opacity">Väri ja peittävyys…</string> + <string name="touch_copy_layout">Kopioi tämä asettelu</string> + <string name="touch_layout_copied">Asettelu kopioitu; liitä se minkä tahansa pelin kosketusasetteluun</string> + <string name="touch_paste_layout">Liitä kopioitu asettelu</string> + <string name="touch_reset_layout">Palauta Steamin oletus</string> + <string name="touch_no_binding">Tällä ohjaimella ei ole sidosta Steamissa, jolle voisi antaa kuvakkeen</string> + <string name="touch_icon_for">Kuvake kohteelle %1$s</string> + <string name="touch_layout_color">Kosketusasettelun väri</string> + <string name="touch_done">Valmis</string> + <string name="touch_save_failed">Kosketusasettelua ei voitu tallentaa</string> + <string name="touch_saved">Kosketusasettelu tallennettu Steamiin</string> + <string name="touch_app_id">Sovellus %1$d</string> + <string name="touch_label_hint">Nimike (valinnainen)</string> + <string name="touch_icons_missing">Steamin kuvakkeita ei löytynyt</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d kuvake Steamista</item> + <item quantity="other">%1$d kuvaketta Steamista</item> + </plurals> + <string name="touch_icon_color">Kuvakkeen väri</string> + <string name="touch_button_color">Painikkeen väri</string> + <string name="touch_no_icon">Ei kuvaketta</string> + + <string name="stores_sync_sign_in">Tarkistetaan kirjautumista…</string> + <string name="stores_sync_library">Haetaan kirjastoasi…</string> + <string name="stores_sync_game_list">Haetaan peliluetteloa…</string> + <string name="stores_sync_details">Ladataan pelien tietoja… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">Synkronoidaan %1$d peliä…</item> + <item quantity="other">Synkronoidaan %1$d peliä…</item> + </plurals> + <string name="stores_sync_progress">Synkronoidaan… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Haetaan kansikuvia…</string> + <string name="stores_sync_failed">Synkronointi epäonnistui</string> + <string name="stores_sync_no_games">%1$s-kirjastostasi ei löytynyt pelejä</string> + <string name="stores_sync_amazon_no_answer">Amazon Games ei vastannut kirjastosi suhteen: päivitä sivu tai kirjaudu uudelleen</string> + <string name="stores_sync_gog_unreachable">GOG:iin ei saatu yhteyttä</string> + <string name="stores_sync_gog_unreadable">GOG-kirjastoasi ei voitu lukea</string> + <string name="stores_registering">Rekisteröidytään Steamiin…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Cardboard-tila</string> + <string name="cardboard_mode_hint">Vasemman ja oikean silmän kuvat puhelimen VR-laseille.</string> + <string name="cardboard_source">Kuva</string> + <string name="cardboard_source_screen">Virtuaalinen näyttö</string> + <string name="cardboard_source_sbs">Rinnakkais-3D</string> + <string name="cardboard_viewer">Lasit</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Ei linssiä</string> + <string name="cardboard_ipd">Silmien etäisyys</string> + <string name="cardboard_screen_width">Näytön leveys</string> + <string name="cardboard_screen_distance">Näytön etäisyys</string> + <string name="cardboard_head_tracking">Pään seuranta</string> + <string name="cardboard_no_sensor">Tässä laitteessa ei ole kiertoanturia.</string> + <string name="cardboard_recenter">Keskitä näkymä</string> + <string name="cardboard_recenter_button">Keskitä</string> + <string name="cardboard_steamvr">SteamVR (kokeellinen)</string> + <string name="cardboard_steamvr_install">Asenna</string> + <string name="cardboard_steamvr_install_hint">Lataa Valven ARM64-version SteamVR:stä (571 Mt).</string> + <string name="cardboard_steamvr_installing">Ladataan ja puretaan SteamVR:ää…</string> + <string name="cardboard_steamvr_start">Käynnistä</string> + <string name="cardboard_steamvr_ready_hint">Ajaa SteamVR:ää niin, että pääsi toimii lasina; kuva vaihtuu rinnakkais-3D:ksi.</string> + <string name="cardboard_steamvr_stop">Pysäytä</string> + <string name="cardboard_steamvr_running_hint">SteamVR on käynnissä.</string> + <string name="cardboard_steamvr_unavailable">Ei käytettävissä tässä istunnossa.</string> + <string name="cardboard_steamvr_failed">SteamVR ei vastannut; tarkista istunnon loki.</string> + <string name="drawer_component_downloads">Lataa komponentteja</string> + <string name="drawer_component_downloads_hint">Lataukset jaetaan Komponentit-sivun kanssa. Valitse yltä ladattu versio, niin sitä käytetään seuraavalla pelin käynnistyksellä.</string> + <string name="drawer_component_choose_download">Valitse ladattava versio</string> + <string name="stores_fact_developer">Kehittäjä</string> + <string name="stores_fact_publisher">Julkaisija</string> + <string name="stores_fact_released">Julkaistu</string> + <string name="stores_hltb_main">Pääosa</string> + <string name="stores_hltb_extras">+ Lisät</string> + <string name="stores_hltb_completionist">Kaikki</string> + <string name="stores_hltb_hours">%1$s t</string> + <string name="stores_hltb_minutes">%1$d min</string> +</resources> diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 4c48f1342..33326b382 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -15,8 +15,10 @@ <string name="session_channel">Session</string> <string name="session_channel_description">Affiché lorsqu\'une session Steam ou Bureau est ouverte</string> <string name="session_notification">Steam est en cours d\'exécution</string> + <string name="session_notification_downloads">Téléchargement Steam en cours</string> <string name="session_notification_paused">Session en pause - Appuyez pour reprendre quand vous êtes prêt</string> <string name="resume_session">Reprendre</string> + <string name="suspend_now">Mettre en pause maintenant</string> <string name="stop_session">Terminer cette session</string> <string name="frame_gen_title">Génération d\'images</string> <string name="frame_gen_hint">Sélectionnez le mode de génération d\'images à utiliser</string> @@ -72,11 +74,21 @@ <string name="game_env_vkd3d_config">Options VKD3D pour D3D12. nodxr désactive le ray tracing ; à utiliser uniquement si le jeu le nécessite.</string> <string name="game_env_proton_log">Définir sur 1 pour générer un journal de diagnostic Proton. La journalisation peut réduire les performances et consommer de l\'espace de stockage.</string> <string name="game_env_wined3d">Définir sur 1 pour utiliser WineD3D (basé sur OpenGL) au lieu de DXVK, pour tester la compatibilité D3D9–11.</string> + <string name="game_env_no_ntsync">Définir sur 1 pour que ce jeu n\'utilise pas ntsync (droiddeck-ntsync ou un pilote du noyau) ; il utilise droiddeck-esync si un pack existe, sinon droiddeck-fsync.</string> + <string name="game_env_no_esync">Définir sur 1 pour désactiver droiddeck-esync pour ce jeu ; il utilise alors droiddeck-fsync, ou droiddeck-ntsync si fsync est aussi désactivé.</string> + <string name="game_env_no_fsync">Définir sur 1 pour que ce jeu n\'utilise pas droiddeck-fsync ; il utilise droiddeck-esync si un pack existe, sinon droiddeck-ntsync.</string> + <string name="game_env_fsync_first">Définir sur 1 pour exécuter ce jeu avec droiddeck-fsync, ou sur 0 pour donner la priorité à droiddeck-esync si un pack correspond à son Proton.</string> <string name="game_env_xalia">Définir sur 0 pour désactiver l\'assistant de navigation à la manette de Proton pour ce jeu.</string> <string name="game_env_hud">Superposition DXVK pour D3D9–11. fps affiche le taux de rafraîchissement ; annuler la définition masque la superposition.</string> <string name="game_env_dxvk_config">Paramètres DXVK séparés par des points-virgules. Les versions récentes utilisent dxvk.maxFrameRate pour limiter le taux de rafraîchissement ; pour un jeu quotidien, il est recommandé d\'utiliser le limiteur de taux de rafraîchissement de session.</string> <string name="game_env_vkd3d_limit">Limite de taux de rafraîchissement D3D12. 0 désactive la limite.</string> <string name="game_env_glthread">Mesa OpenGL multithread. Pour les jeux OpenGL limités par le CPU, true peut aider ; à tester jeu par jeu.</string> + <string name="sync_backend_title">Synchronisation Wine</string> + <string name="sync_backend_hint">Comment les jeux synchronisent leurs threads. Prendra effet à la prochaine session.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> <string name="fex_preset_title">Préréglages FEX</string> <string name="fex_next_launch">Prendra effet au prochain lancement du jeu.</string> <string name="game_settings_title">Jeu</string> @@ -149,6 +161,15 @@ <string name="add_app_script_hint">Un fichier .sh. Il s\'exécutera depuis son propre dossier et s\'affichera en plein écran via gamescope.</string> <string name="add_app_appimage_hint">Un AppImage ARM64 (aarch64). Il sera décompressé dans l\'environnement d\'exécution Linux lors de la première utilisation.</string> <string name="add_app_github_hint">Installer l\'AppImage ARM64 depuis la dernière version du dépôt.</string> + <string name="app_fex_title">Utiliser FEX</string> + <string name="app_fex_auto">Auto</string> + <string name="app_fex_on">Activé</string> + <string name="app_fex_off">Désactivé</string> + <string name="app_fex_hint">FEX exécute des programmes Linux x86 sur cet appareil ARM64. En mode Auto, il est utilisé lorsque le programme est conçu pour x86.</string> + <string name="app_fex_x86">%1$s via FEX</string> + <string name="app_fex_not_ready">Ce programme est conçu pour les PC %1$s et s\'exécute via FEX, qui n\'est pas encore configuré. Ouvrez Steam une fois en étant connecté : DroidDeck lui fait télécharger FEX et le Steam Linux Runtime nécessaires, puis ce programme s\'ouvre.</string> + <string name="session_end_program">Ce programme</string> + <string name="session_end_fex_missing">%1$s est conçu pour les PC x86 et s\'exécute via FEX, qui n\'est pas encore installé. Lancez Steam une fois en étant connecté et laissez-le télécharger FEX et le Steam Linux Runtime, puis relancez ce programme.</string> <string name="add_app_flatpak_hint">ID d\'application Flathub ou sa page Flathub. Si Flatpak n\'est pas encore configuré, il le sera d\'abord.</string> <string name="add_app_file">Fichier</string> <string name="add_app_no_file">Aucun fichier sélectionné</string> @@ -163,6 +184,8 @@ <string name="add_app_flatpak_id">ID d\'application</string> <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> <string name="add_app_flatpak_invalid">N\'est pas un ID d\'application Flathub valide</string> + <string name="add_app_flatpak_bundle_hint">Ou un fichier .flatpak, pour une application absente de Flathub.</string> + <string name="add_app_pick_flatpak">Choisir un fichier .flatpak</string> <string name="add_app_source">Source</string> <string name="add_app_appearance">Afficher sur le bureau</string> <string name="add_app_preview_name">Nouvelle application</string> @@ -184,6 +207,9 @@ <string name="add_app_cancel">Annuler</string> <string name="add_app_runtime_required">Veuillez d\'abord installer l\'environnement d\'exécution Linux depuis les « Paramètres ».</string> <string name="add_app_busy">Veuillez attendre que %1$s soit terminé.</string> + <string name="steam_compat_label">Compatible</string> + <string name="proton_compatible_note">Les versions marquées « %1$s » dans le menu Compatibilité de Steam incluent les correctifs de préfixe de DroidDeck et lancent les jeux ici. Par défaut, c\'est Proton Experimental ARM64 (%1$s), ou Proton 11.0 ARM64 (%1$s) une fois cette version plus récente installée. Les entrées Proton de Valve sans cette mention ne peuvent pas lancer de jeu sur cet appareil.</string> + <string name="proton_installed_compatible">%1$s installé · %2$s</string> <string name="common_cancel">Annuler</string> <string name="common_auto">Auto</string> <string name="common_never">Jamais</string> @@ -229,6 +255,10 @@ <string name="mode_osc">Commandes à l\'écran</string> <string name="mode_osc_qam">Steam + QAM</string> <string name="mode_osc_note">Le mode automatique affiche les commandes à l\'écran lorsqu\'aucune manette n\'est connectée.</string> + <string name="osc_steam_touch">Tactile Steam (bêta)</string> + <string name="osc_style_next_session">Les commandes tactiles de Steam s\'activeront à la prochaine session Steam</string> + <string name="osc_steam_layout">Disposition tactile Steam</string> + <string name="osc_steam_layout_hint">Déplacez, redimensionnez ou masquez les commandes tactiles de ce jeu ; enregistré dans Steam</string> <string name="mode_controller">Manette</string> <string name="mode_controller_hint">Comment Steam reconnaît votre manette. Prendra effet à la prochaine session.</string> <string name="mode_controller_deck">Manette Steam Deck</string> @@ -238,10 +268,16 @@ <string name="mode_controller_x360">Manette Xbox 360</string> <string name="mode_controller_note">Deck : fournit un bouton d\'accès rapide et un gyroscope. Xbox 360 : utilisez Guide+A pour ouvrir l\'accès rapide.</string> <string name="mode_back">Retour</string> + <string name="back_menu_then_qam">Menu → QAM</string> + <string name="back_qam_then_menu">QAM → Menu</string> <string name="mode_session">Session</string> <string name="mode_suspend">Comportement en arrière-plan</string> <string name="mode_suspend_hint">Comportement lorsque l\'application est masquée ou que l\'écran est éteint.</string> <string name="mode_suspend_manual">Manuel</string> + <string name="mode_suspend_native">Natif</string> + <string name="mode_suspend_steam_note">Auto : met en pause, puis reprend à l\'affichage. Natif : prépare d\'abord Steam à la mise en veille. Manuel : attend « Reprendre ». Jamais : continue de fonctionner.</string> + <string name="mode_background_downloads">Poursuivre les téléchargements</string> + <string name="mode_background_downloads_hint">Retarde la mise en pause en arrière-plan tant que Steam télécharge.</string> <string name="mode_suspend_note">Le mode automatique met en pause en arrière-plan et reprend à l\'affichage. Le mode manuel nécessite d\'appuyer sur « Reprendre ». Ne reste jamais en cours d\'exécution.</string> <string name="mode_startup">Démarrage</string> <string name="mode_steam_startup">Lancer Steam à l\'ouverture de DroidDeck</string> @@ -262,6 +298,10 @@ <string name="mode_client">Client</string> <string name="mode_mangoapp">Superposition de performance</string> <string name="mode_mangoapp_hint">Désactivez cette option si les jeux plantent ou affichent un écran noir en mode Deck. Prendra effet à la prochaine session.</string> + <string name="mode_steam_repair">Réparer le client Steam</string> + <string name="mode_steam_repair_hint">Retélécharge le client Steam au prochain démarrage de Steam. Utile si Steam reste bloqué sur son écran de chargement. Les jeux, la connexion et les paramètres sont conservés.</string> + <string name="mode_steam_repair_queued">Le client Steam sera retéléchargé au prochain démarrage de Steam.</string> + <string name="mode_steam_repair_button">Réparer</string> <string name="mode_added_art">Illustrations</string> <string name="mode_sgdb_key">Clé API SteamGridDB</string> <string name="mode_sgdb_key_yours">Votre clé</string> @@ -274,6 +314,51 @@ <string name="games_pick_exe_title">Choisir l’exe :</string> <string name="games_add_unreachable">Cet emplacement n’est pas disponible dans la session</string> <string name="mode_added_exe_check">Vérifier cet exécutable</string> + <string name="wincomp_title">Composants Windows</string> + <string name="wincomp_pad_hint">LB / RB pour passer d\'une section à l\'autre.</string> + <string name="wincomp_card_none">Aucun</string> + <string name="wincomp_applies">DLL Windows non incluses dans Proton, issues de la liste des composants. Activez celles dont ce jeu a besoin : elles sont placées dans le dossier Windows propre au jeu, jamais dans ses fichiers, et restent quel que soit le Proton utilisé.</string> + <string name="wincomp_offline">Impossible d\'accéder à la liste des composants. Les composants déjà téléchargés peuvent toujours être activés ou désactivés.</string> + <string name="wincomp_recommended">Recommandés pour ce jeu</string> + <string name="wincomp_checking">Analyse des besoins de ce jeu…</string> + <string name="wincomp_when_online">Disponible une fois en ligne</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">Activation de %1$d composant…</item> + <item quantity="many">Activation de %1$d composants…</item> + <item quantity="other">Activation de %1$d composants…</item> + </plurals> + <string name="wincomp_auto_section">Activés pour ce jeu</string> + <string name="wincomp_auto_steam">Steam installe %1$s avec %2$s (application %3$s)</string> + <string name="wincomp_auto_gog">D\'après la liste de GOG pour ce jeu</string> + <string name="wincomp_auto_epic">D\'après les prérequis d\'Epic pour ce jeu</string> + <string name="wincomp_auto_amazon">D\'après les étapes d\'installation d\'Amazon pour ce jeu</string> + <string name="wincomp_steam_by_name">Steam installe %1$s avec l\'application %2$s, identifiée par son nom</string> + <string name="wincomp_suggestions">Également listés</string> + <string name="wincomp_suggestion_reason">Listé par %1$s ; non disponible comme composant ici</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">Le jeu fournit %1$s</string> + <string name="wincomp_found_shipped">Le jeu contient %1$s (fonctionne souvent tel quel)</string> + <string name="wincomp_already_here">Déjà présent dans ce jeu</string> + <string name="wincomp_no_recommendation">Rien dans le dossier du jeu n\'indique un composant.</string> + <string name="wincomp_active">Installés pour ce jeu (%1$d)</string> + <string name="wincomp_all">Tous les composants (%1$d)</string> + <string name="wincomp_installers">Installateur Windows requis (%1$d)</string> + <string name="wincomp_installers_note">Ce sont des programmes d\'installation autonomes, sans paquet à extraire, et ce qu\'ils installent n\'a pas encore été enregistré. La plupart sont d\'anciens environnements d\'exécution que le Mono de Proton remplace déjà ; les enregistrements sont ajoutés au fur et à mesure.</string> + <string name="wincomp_show">Afficher</string> + <string name="wincomp_hide">Masquer</string> + <string name="wincomp_needs_installer">Lance son propre programme d\'installation (pas encore d\'enregistrement)</string> + <string name="wincomp_unsupported">Pas encore pris en charge ici</string> + <string name="wincomp_loading">Chargement des composants…</string> + <string name="wincomp_failed">Installation impossible : %1$s</string> + <string name="wincomp_downloaded">Téléchargé</string> + <string name="wincomp_downloading">Téléchargement de %1$s</string> + <string name="wincomp_installing">Installation : %1$s</string> + <string name="wincomp_reverting">Désactivation…</string> + <string name="wincomp_download_size">%1$s à télécharger</string> + <string name="wincomp_next_launch">Prendra effet au prochain lancement du jeu.</string> <string name="mode_stretch169">Étirer les jeux 16:9</string> <string name="mode_stretch169_hint">Sur les écrans dont le rapport d\'aspect est inférieur à 16:9, étire le jeu pour remplir l\'écran au lieu d\'afficher des bandes noires. Prendra effet à la prochaine session.</string> <string name="mode_audio">Audio</string> @@ -297,6 +382,8 @@ <string name="mode_storage_folder">Dossier : %1$s</string> <string name="mode_second_library">Bibliothèque secondaire</string> <string name="mode_storage_note">Les jeux lisant des ressources depuis une carte SD ou un stockage partagé peuvent saccader.</string> + <string name="mode_storage_diagnostics">Diagnostic du stockage</string> + <string name="mode_storage_diagnostics_hint">Enregistre les résultats d\'allocation et des mesures de temps du stockage dans « Partager les journaux ». Peut ralentir les installations lorsqu\'il est activé. Prendra effet à la prochaine session.</string> <string name="mode_choose_folder">Choisir un dossier…</string> <string name="mode_custom_res_title">Résolution personnalisée</string> <string name="mode_custom_res_text">Remplace la limite de résolution et le format d\'écran. Les dimensions ne correspondant pas à l\'écran afficheront des bandes noires.</string> @@ -305,6 +392,7 @@ <string name="mode_custom_res_range">Plage : 320×240 à 3840×2160.</string> <plurals name="mode_added_count"> <item quantity="one">%1$d jeu</item> + <item quantity="many">%1$d jeux</item> <item quantity="other">%1$d jeux</item> </plurals> <string name="common_ok">OK</string> @@ -327,9 +415,54 @@ <string name="comp_check_nightlies">Vérifier les nouveaux paquets dans Nightlies</string> <string name="comp_proton">Proton</string> <string name="comp_proton_note">Les mises à jour de Steam remplacent le Proton de Valve ; les fichiers originaux de chaque version sont conservés.</string> + <string name="comp_scope">Configuration</string> + <string name="comp_profile_configure">Configurer le profil</string> + <string name="comp_profile">Profil</string> + <string name="comp_scope_general">Profil par défaut</string> + <string name="comp_scope_general_detail">Utilisé automatiquement par les jeux sans réglages personnalisés.</string> + <string name="comp_profile_game_detail">Paramètres enregistrés utilisés automatiquement au lancement de %1$s.</string> + <string name="comp_game_hint">Choisissez uniquement les paramètres dont ce jeu a besoin. Tout le reste continue d\'utiliser le profil par défaut.</string> + <string name="comp_game_fex">Préréglage FEX</string> + <string name="comp_game_fex_inherited">Utilise le préréglage du profil par défaut.</string> + <string name="comp_game_fex_override">Préréglage personnalisé enregistré pour ce jeu.</string> + <string name="comp_game_fex_custom">Variables FEX personnalisées</string> + <string name="comp_game_use_general">Utiliser par défaut</string> + <string name="comp_game_compatibility">Compatibilité du jeu</string> + <string name="comp_game_environment_hint">Options d\'environnement Proton, DXVK, VKD3D et de synchronisation pour ce jeu.</string> + <string name="comp_game_proton_inherited">Utilise le Proton du profil par défaut.</string> + <string name="comp_game_proton_override">Proton personnalisé enregistré pour ce jeu via le mappage de compatibilité de Steam.</string> + <string name="comp_no_proton_short">Aucun Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Utilise le paquet du profil par défaut issu de %1$s, sauf si ce jeu choisit un autre paquet.</string> + <string name="comp_game_component_general">Utilise le paquet de composants du profil par défaut.</string> + <string name="comp_game_component_override">Utilise %1$s uniquement pour ce jeu.</string> + <string name="comp_game_runtime_options">Options d\'exécution</string> + <string name="comp_game_environment_other_hint">Autres options d\'environnement Proton, DXVK, VKD3D-Proton et de synchronisation propres à ce jeu.</string> + <string name="comp_game_saved_next">Le profil de %1$s est enregistré pour le prochain lancement du jeu.</string> + <string name="game_no_steam_title">Démarrer sans Steam</string> + <string name="game_no_steam_detail">Pour un jeu qui se ferme aussitôt : le démarre sans la configuration Steam de Proton. Les fonctionnalités Steam, comme la surimpression, sont désactivées pour lui.</string> + <string name="game_no_steam_on">Activé</string> + <string name="comp_game_proton_live">Le choix de Proton pour %1$s est enregistré et disponible dès maintenant dans Steam.</string> + <string name="comp_game_proton_next">Le choix de Proton pour %1$s est enregistré. Redémarrez Steam avant de lancer le jeu.</string> + <string name="comp_game_proton_not_runnable">%1$s ne peut pas utiliser le Proton sélectionné sur cet appareil.</string> + <string name="comp_game_load_failed">Impossible de charger le profil de %1$s : %2$s</string> + <string name="comp_game_save_failed">Impossible d\'enregistrer le profil du jeu : %1$s</string> + <string name="comp_game_reset">Réinitialiser le profil du jeu</string> + <string name="comp_game_reset_title">Réinitialiser le profil de %1$s ?</string> + <string name="comp_game_reset_body">Supprimer les réglages propres à ce jeu (Proton, FEX, paquet de composants, environnement, filtrage des textures et composants Windows) ? Il utilisera le profil par défaut.</string> + <string name="comp_game_reset_done">%1$s utilise désormais le profil par défaut.</string> + <string name="comp_game_reset_failed">Le profil du jeu n\'a pas pu être entièrement réinitialisé : %1$s</string> + <string name="comp_default_live">%1$s exécute désormais tous les jeux qui n\'ont pas choisi leur propre Proton dans Steam.</string> + <string name="comp_default_next">%1$s exécutera tous les jeux qui n\'ont pas choisi leur propre Proton, à partir du prochain démarrage de Steam.</string> + <string name="comp_default_not_runnable">%1$s ne peut pas lancer de jeux ici, il ne peut donc pas être le Proton par défaut.</string> + <string name="comp_default_failed">Impossible d\'enregistrer le Proton par défaut : %1$s</string> <string name="comp_swapped">%1$s · %2$d remplacé(s)</string> <string name="comp_all_original">%1$s · Tout est original</string> <string name="comp_game_running_suffix">%1$s · Jeu en cours d\'exécution</string> + <string name="comp_esync_pack">%1$s · pack droiddeck-esync</string> + <string name="comp_esync_wanted">%1$s · pas encore de pack droiddeck-esync</string> + <string name="comp_esync_builtin">%1$s · esync intégré, aucun pack requis</string> <string name="comp_gpu_drivers">Pilotes GPU</string> <string name="comp_reapplied">%1$s · Réappliqué au lancement de %2$s</string> <string name="comp_next_queued">Suivant : %1$s · Après la fermeture du jeu</string> @@ -392,6 +525,12 @@ <string name="perf_sysmem">Turnip : rendu sysmem</string> <string name="perf_sysmem_hint">Steam et la plupart des jeux seront plus rapides. Requis pour Adreno 710/720/722.</string> <string name="perf_xalia">Ignorer l\'assistant xalia de Steam</string> + <string name="perf_fastsync">droiddeck-ntsync (expérimental)</string> + <string name="perf_fastsync_hint">Un substitut en espace utilisateur du pilote Linux ntsync, pour les tests. Lorsqu\'il est activé, Proton l\'utilise à la place de droiddeck-esync et de droiddeck-fsync. Fonctionne avec Proton Experimental ARM64, GE-Proton et proton-cachyos. Prendra effet à la prochaine session.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync en priorité</string> + <string name="perf_fsync_hint">Exécute le fsync propre à Proton sur les fichiers Proton d\'origine, avec futex_waitv fourni en espace utilisateur : aucun pack n\'est nécessaire et il continue de fonctionner dès que Steam met à jour Proton. Lorsqu\'il est activé, les jeux l\'utilisent avant droiddeck-esync, sauf les quelques-uns que Proton exclut de fsync. Prendra effet à la prochaine session.</string> + <string name="perf_esync_hint">Les threads Windows des jeux s\'attendent mutuellement via eventfd et la mémoire partagée au lieu de faire des allers-retours par wineserver. Utilise un pack droiddeck-esync conçu pour cette version exacte de Proton ; les packs sont récupérés automatiquement. Tant qu\'il n\'en existe pas pour une nouvelle version de Proton, droiddeck-fsync prend le relais. Prendra effet à la prochaine session.</string> <string name="perf_xalia_hint">Désactive l\'assistant de navigation à la manette de Proton, qui consomme du CPU dans chaque jeu.</string> <string name="perf_seccomp">Exécuter proot sans seccomp</string> <string name="perf_seccomp_hint">Peut corriger certaines erreurs d\'appels système manquants sur certains noyaux, mais peut réduire les performances.</string> @@ -412,8 +551,10 @@ <string name="driver_delete_plain">Ses fichiers seront supprimés de l\'application.</string> <string name="driver_delete_in_use">Ses fichiers seront supprimés. Comme il est en cours d\'utilisation, le pilote par défaut sera utilisé à la place.</string> <string name="drawer_page_display">Affichage</string> + <string name="drawer_page_effects">Effets</string> <string name="drawer_page_controls">Contrôles</string> <string name="drawer_page_components">Composants</string> + <string name="drawer_page_controller">Manette</string> <string name="drawer_page_settings">Paramètres</string> <string name="drawer_page_desc">Page %1$s</string> <string name="drawer_steam">Steam</string> @@ -424,11 +565,39 @@ <string name="drawer_prev_page">Page précédente</string> <string name="drawer_next_page">Page suivante</string> <string name="drawer_hud">HUD de performance</string> + <string name="drawer_scaling">Mode de mise à l\'échelle</string> + <string name="drawer_scaling_sharpness">Netteté de la mise à l\'échelle</string> + <string name="drawer_effects">Effets d\'écran</string> + <string name="drawer_effects_hint">En direct. Consomme un peu de temps GPU tant qu\'un effet est actif.</string> + <string name="drawer_look">Préréglages</string> + <string name="drawer_look_custom">Personnalisé</string> + <string name="drawer_effects_advanced">Avancé</string> + <string name="widgets_expanded">Développé</string> + <string name="widgets_collapsed">Réduit</string> + <string name="drawer_cas">Netteté (CAS)</string> + <string name="drawer_cas_level">Niveau de netteté</string> + <string name="drawer_fake_hdr">Faux HDR</string> + <string name="drawer_deband">Anti-banding</string> + <string name="drawer_deband_strength">Intensité de l\'anti-banding</string> + <string name="drawer_brightness">Luminosité</string> + <string name="drawer_contrast">Contraste</string> + <string name="drawer_gamma">Gamma</string> + <string name="drawer_saturation">Saturation</string> + <string name="drawer_fxaa">FXAA (lissage des contours)</string> + <string name="drawer_toon">Contours cartoon</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Filtrage des textures</string> + <string name="drawer_texture_hint">Jeux DirectX 9–11, à partir de leur prochain lancement.</string> + <string name="drawer_anisotropy">Filtrage anisotrope</string> + <string name="drawer_texture_sharpness">Netteté des textures</string> + <string name="drawer_texture_sharpness_note">Un biais de mip négatif : textures plus nettes, un peu de scintillement. Auto s\'adapte au facteur d\'agrandissement de la session à l\'écran.</string> <string name="drawer_touch_auto">Auto (%1$s)</string> <string name="drawer_keyboard">Clavier</string> <string name="drawer_pc_keyboard">Clavier PC</string> <string name="drawer_android_keyboard">Android</string> <string name="drawer_second_screen">Deuxième écran</string> + <string name="drawer_second_screen_mode">Mode</string> <string name="drawer_second_screen_shows">Afficher</string> <string name="drawer_android_apps">Applications Android</string> <string name="drawer_launch_app">Lancer l\'application</string> @@ -447,6 +616,20 @@ <string name="drawer_stop_title">Arrêter la session ?</string> <string name="drawer_running_now">En cours d\'exécution</string> <string name="drawer_no_game">Aucun jeu n\'utilise actuellement Proton</string> + <string name="drawer_selected_profile">Profil du jeu</string> + <string name="drawer_selected_profile_none">Les détails du profil ne sont pas encore disponibles.</string> + <string name="drawer_profile_refresh_failed_initial">Impossible de charger les profils de jeu. Nouvelle tentative automatique.</string> + <string name="drawer_profile_refresh_failed_stale">Impossible d\'actualiser les profils de jeu. Affichage des dernières données disponibles ; nouvelle tentative automatique.</string> + <string name="drawer_profile_inspect_hint">Afficher et modifier les paramètres enregistrés utilisés au prochain lancement.</string> + <string name="drawer_profile_default">Utilise le profil par défaut</string> + <string name="drawer_profile_custom">Profil personnalisé</string> + <string name="drawer_component_inherited">Utilise %1$s du profil par défaut.</string> + <string name="drawer_game_components_note">Les modifications sont enregistrées dans le profil de %1$s et prennent effet au prochain lancement du jeu.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d réglage propre au jeu</item> + <item quantity="many">%1$d réglages propres au jeu</item> + <item quantity="other">%1$d réglages propres au jeu</item> + </plurals> <string name="drawer_no_proton">Proton n\'est pas encore installé.</string> <string name="drawer_game_running">Jeu en cours d\'exécution</string> <string name="drawer_reapplied">Réappliqué au lancement de %1$s</string> @@ -503,14 +686,33 @@ <string name="setup_saved_logs">Journaux de session enregistrés</string> <string name="setup_saved_logs_hint">Conserver les %1$d plus récents</string> <string name="setup_clear_logs">Effacer les journaux</string> + <string name="setup_reset_settings">Réinitialiser tous les paramètres</string> + <string name="setup_reset_settings_hint">Remet chaque paramètre à sa valeur par défaut</string> + <string name="setup_reset_settings_button">Réinitialiser</string> + <string name="setup_reset_settings_title">Réinitialiser tous les paramètres ?</string> + <string name="setup_reset_settings_body">Les paramètres d\'affichage, de performances, de manette, de thème, de langue et du lanceur reprennent leurs valeurs par défaut. Vos jeux, votre connexion Steam, vos comptes de boutiques, les pilotes téléchargés et les dossiers de stockage sont conservés.</string> + <string name="setup_reset_settings_profiles">Réinitialiser aussi les profils de jeu (Proton, FEX, composants et environnement de chaque jeu, ainsi que l\'environnement du profil par défaut)</string> + <string name="setup_reset_settings_session_running">Arrêtez la session avant de réinitialiser les paramètres.</string> + <string name="setup_reset_settings_partial">Les paramètres ont été réinitialisés, mais certaines parties n\'ont pas pu l\'être. Les journaux contiennent les détails.</string> <string name="setup_offline">Mode hors ligne</string> <string name="setup_sign_in_first">Veuillez d\'abord vous connecter à Steam</string> <string name="setup_launcher">Lanceur</string> + <string name="setup_language">Langue</string> + <string name="setup_language_hint">Steam l\'utilise aussi, à partir de son prochain démarrage.</string> + <string name="setup_language_system">Langue du système (%1$s)</string> + <string name="setup_language_system_short">Langue du système</string> + <string name="setup_app_scale">Échelle de l\'application</string> + <string name="setup_app_scale_hint">Texte et commandes de DroidDeck</string> + <string name="setup_app_scale_default">%1$d%% (par défaut)</string> <string name="setup_theme">Thème</string> <string name="setup_theme_hint">Apparence du lanceur</string> <string name="setup_home">Utiliser comme écran d\'accueil</string> <string name="setup_home_on">DroidDeck peut être utilisé comme application d\'écran d\'accueil</string> <string name="setup_home_off">DroidDeck ne sera pas proposé comme application d\'écran d\'accueil</string> + <string name="setup_orientation">Orientation</string> + <string name="setup_orientation_auto">Paysage automatique</string> + <string name="setup_orientation_landscape">Paysage</string> + <string name="setup_orientation_reverse">Paysage inversé</string> <string name="setup_fullscreen">Plein écran</string> <string name="setup_fullscreen_on">Masquer la barre d\'état et la barre de navigation Android</string> <string name="setup_fullscreen_off">Afficher la barre d\'état et la barre de navigation Android</string> @@ -638,14 +840,17 @@ <string name="upd_just_now">À l\'instant</string> <plurals name="upd_tests_count"> <item quantity="one">%1$d disponible</item> + <item quantity="many">%1$d disponibles</item> <item quantity="other">%1$d disponibles</item> </plurals> <plurals name="upd_preview_behind"> <item quantity="one">Modifications depuis votre version · %1$d version de retard</item> + <item quantity="many">Modifications depuis votre version · %1$d versions de retard</item> <item quantity="other">Modifications depuis votre version · %1$d versions de retard</item> </plurals> <plurals name="upd_preview_recent"> <item quantity="one">Modifications récentes · %1$d version</item> + <item quantity="many">Modifications récentes · %1$d versions</item> <item quantity="other">Modifications récentes · %1$d versions</item> </plurals> <string name="gate_title">Un paramètre Android à modifier</string> @@ -771,6 +976,12 @@ <string name="content_page_gone">Cette page n\'existe plus. Veuillez sélectionner une section à gauche.</string> <string name="content_open_desktop">Ouvrir le bureau</string> <string name="content_install_open_desktop">Installer et ouvrir le bureau</string> + <string name="desktop_remove_title">Désinstaller le bureau</string> + <string name="desktop_remove_text">Supprime le bureau et ses applications intégrées. Conserve les jeux, les applications ajoutées, les paramètres et les bibliothèques partagées.</string> + <string name="desktop_removing">Suppression du bureau</string> + <string name="desktop_removed">Bureau supprimé</string> + <string name="desktop_cleaning_legacy">Suppression des anciens fichiers LXQt</string> + <string name="desktop_removal_unknown">Mettez à jour DroidDeck avant de supprimer cette version du bureau.</string> <string name="content_linux_desktop">Bureau Linux</string> <string name="content_linux_desktop_hint">KDE Plasma, Firefox et vos émulateurs</string> <string name="games_empty">Les jeux installés sur Steam apparaîtront ici après la fin de la session.</string> @@ -800,6 +1011,8 @@ <string name="games_last_played">Dernier lancement %1$s</string> <string name="games_internal">Stockage interne</string> <string name="games_added">Jeux ajoutés</string> + <string name="games_custom">Jeu personnalisé</string> + <string name="games_store_storage">Stockage des jeux</string> <string name="gpu_auto_hint">Sélectionne la meilleure combinaison de pilotes pour ce GPU et les maintient à jour</string> <string name="gpu_manual_hint">Choisir manuellement la combinaison de pilotes</string> <string name="gpu_checking">Vérification des pilotes…</string> @@ -841,6 +1054,12 @@ <string name="deck_tab_left">Pavé tactile gauche</string> <string name="deck_tab_right">Pavé tactile droit</string> <string name="deck_tab_both">Les deux</string> + <string name="deck_tab_gamepad">Manette</string> + <string name="deck_surface_stick">Joystick</string> + <string name="deck_surface_trackpad">Pavé tactile</string> + <string name="deck_surface_switch">Basculer entre joystick et pavé tactile</string> + <string name="deck_more_controls">Plus de commandes Deck</string> + <string name="deck_panels">PANNEAUX</string> <string name="deck_close">Fermer les commandes du deuxième écran</string> <string name="deck_open_steam_menu">Ouvrir le menu Steam</string> <string name="deck_left_upper">Gauche · Haut</string> @@ -876,6 +1095,10 @@ <string name="proton_installed">%1$s installé</string> <string name="proton_queued">Élément en attente d\'une demande précédente</string> <string name="proton_not_installed">Non installé</string> + <string name="proton_update_available">Mise à jour disponible → %1$s</string> + <string name="proton_remove_older">Supprimer les anciennes versions de %1$s</string> + <string name="proton_remove_older_body">Supprime %1$s. Les jeux configurés sur ces versions devront passer à un autre Proton.</string> + <string name="proton_remove_body">Les jeux configurés sur cette version devront passer à un autre Proton.</string> <string name="proton_install_now">Installer maintenant</string> <string name="proton_cancel_queued">Annuler la file d\'attente</string> <string name="pckb_android">Clavier Android</string> @@ -904,6 +1127,8 @@ <string name="game_env_eyebrow">Jeux Proton</string> <string name="pip_title">Picture-in-Picture (PiP)</string> <string name="pip_auto">Passer automatiquement en PiP</string> + <string name="unfolded_controls">Commandes sur l\'écran déplié</string> + <string name="unfolded_controls_hint">Déplié, le jeu s\'affiche au-dessus des pavés tactiles et des poignées arrière du Steam Deck. Plié, le jeu est en plein écran.</string> <string name="pip_suspended">En pause</string> <string name="pip_not_ready">Le mode PiP sera disponible une fois la session prête.</string> <string name="pip_permission">Veuillez autoriser DroidDeck à utiliser le mode PiP dans les paramètres Android.</string> @@ -928,8 +1153,926 @@ <string name="game_file_folder">Choisir un dossier pour les fichiers du jeu frontal</string> <string name="game_file_exported">Fichiers du jeu enregistrés dans %1$s</string> <string name="game_frontend_files">Fichiers frontaux</string> - <string name="fm_up_one_level">Remonter d\'un niveau</string> + + <string name="main_wait_runtime">Attendez la fin de l\'opération sur l\'environnement d\'exécution</string> + <string name="main_wait_proton_install">Attendez la fin de l\'installation de l\'outil de compatibilité</string> + <string name="main_wait_desktop_install">Attendez la fin de l\'installation de l\'application de bureau</string> + <string name="main_wait_install_game">Attendez la fin de l\'installation avant de lancer un jeu</string> + <string name="main_saves_close_session">Fermez d\'abord la session Steam, pour que le jeu ne puisse pas écraser l\'importation</string> + <string name="main_saves_importing">Importation dans %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="one">%1$d fichier importé (%2$s) dans %3$s</item> + <item quantity="many">%1$d fichiers importés (%2$s) dans %3$s</item> + <item quantity="other">%1$d fichiers importés (%2$s) dans %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">%1$d fichier importé (%2$s) dans %3$s. Anciennes sauvegardes copiées dans Download/DroidDeck/Saves/backups</item> + <item quantity="many">%1$d fichiers importés (%2$s) dans %3$s. Anciennes sauvegardes copiées dans Download/DroidDeck/Saves/backups</item> + <item quantity="other">%1$d fichiers importés (%2$s) dans %3$s. Anciennes sauvegardes copiées dans Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Choisir un zip de sauvegarde pour %1$s</string> + <string name="main_saves_exporting">Exportation de %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="one">%1$d fichier exporté (%2$s) : %3$s</item> + <item quantity="many">%1$d fichiers exportés (%2$s) : %3$s</item> + <item quantity="other">%1$d fichiers exportés (%2$s) : %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Choisir où enregistrer %1$s (%2$s)</string> + <string name="main_action_failed">%1$s : échec (%2$s)</string> + <string name="main_no_logs_yet">Aucun journal de session pour l\'instant : lancez d\'abord une session.</string> + <string name="main_no_logs_to_clear">Aucun journal de session à effacer.</string> + <plurals name="main_logs_cleared"> + <item quantity="one">%1$d dossier de journaux de session effacé.</item> + <item quantity="many">%1$d dossiers de journaux de session effacés.</item> + <item quantity="other">%1$d dossiers de journaux de session effacés.</item> + </plurals> + <string name="main_wadb_pair_failed">Échec de l\'appairage du débogage sans fil</string> + <string name="main_wadb_command_failed">Échec de la commande de débogage sans fil</string> + <string name="main_wadb_unavailable">Le débogage sans fil n\'est pas disponible</string> + <string name="main_adb_copied">Commande ADB copiée</string> + <string name="main_pick_roms">Choisir le dossier ROM</string> + <string name="main_non_adreno_title">GPU non Adreno</string> + <string name="main_non_adreno_text">Turnip prend en charge les GPU Adreno. Sur %1$s, Steam peut afficher un écran noir. Téléchargement : %2$.0f Mo.</string> + <string name="main_install_anyway">Installer quand même</string> + <string name="main_remove_runtime_title">Supprimer l\'environnement d\'exécution Linux</string> + <string name="main_remove_runtime_text">Cela supprime l\'environnement d\'exécution, le client Steam qu\'il contient et tous les jeux qui y sont installés.</string> + <string name="main_applied">Appliqué : %1$s</string> + <string name="android_app_open_failed_primary">Impossible d\'ouvrir %1$s sur l\'écran principal</string> + <string name="android_app_open_failed_on">Impossible d\'ouvrir %1$s sur %2$s</string> + <string name="android_app_open_failed_display">Impossible d\'ouvrir %1$s sur l\'écran %2$d</string> + <string name="comp_busy_importing">Importation</string> + <string name="comp_busy_swapping">Remplacement</string> + <string name="comp_busy_restoring">Restauration</string> + <string name="comp_busy_cancelling">Annulation</string> + <string name="comp_busy_deleting">Suppression</string> + <string name="comp_cannot_read">impossible de lire le fichier</string> + <string name="comp_imported">%1$s importé</string> + <string name="comp_swap_cancelled">Le remplacement en attente a été annulé.</string> + <string name="comp_pick_wcp">Choisir un paquet de composants (-linux .wcp)</string> + <string name="comp_nightlies_unreachable">Impossible d\'accéder aux Nightlies</string> + <string name="comp_stored">%1$s enregistré</string> + <string name="comp_download_failed">Échec du téléchargement : %1$s</string> + <string name="drivers_pick_linux">Choisir un pilote d\'environnement d\'exécution Linux (zip -Linux ou bundle Android + Linux)</string> + <string name="drivers_pick_android">Choisir un pilote d\'affichage (zip AdrenoTools ou bundle Android + Linux)</string> + <string name="drivers_pick_any">Choisir un zip de pilote ou un bundle Android + Linux</string> + <string name="main_pkg_problem">%1$s : %2$s</string> + <string name="main_removing_named">Suppression de %1$s…</string> + <string name="main_forgetting_named">Oubli de %1$s…</string> + <string name="main_pick_game_storage">Choisir le dossier de stockage des jeux</string> + <string name="main_pick_decky_zip">Choisir un ZIP de plugin Decky</string> + <string name="main_storage_not_usable">Inutilisable : %1$s</string> + <string name="main_running_program">Programme</string> + <string name="main_removing_runtime">Suppression de l\'environnement d\'exécution Linux</string> + <string name="main_settings_main_screen">Impossible d\'ouvrir les paramètres sur l\'écran inférieur ; ils ont été ouverts sur l\'écran principal.</string> + <string name="main_open_dev_options">Ouvrez les options pour les développeurs dans les paramètres</string> + + <string name="upd_unknown_apps_failed">Impossible d\'ouvrir le paramètre Android « Installer des applis inconnues »</string> + <string name="upd_couldnt_check">Vérification impossible : %1$s</string> + <string name="upd_no_download">Cette version ne propose aucun téléchargement pour cette copie de DroidDeck</string> + <string name="upd_stop_first">Arrêtez d\'abord la session en cours, puis mettez à jour</string> + <string name="upd_stage_downloading">Téléchargement</string> + <string name="upd_stage_installing">Installation</string> + <string name="upd_stage_confirm">En attente de votre confirmation</string> + <string name="upd_failed">La mise à jour a échoué</string> + <string name="upd_not_offered">Android n\'a pas proposé d\'installer la mise à jour</string> + + <string name="decky_install_failed">Échec de l\'installation de Decky : %1$s</string> + <string name="decky_plugin_starting">Démarrage de l\'importation du plugin…</string> + <string name="decky_plugin_failed">Échec de l\'importation du plugin : %1$s</string> + <string name="decky_plugin_installed">Plugin installé. Redémarrez Steam pour le charger.</string> + + <string name="drivers_not_checked">Pas encore vérifié - appuyez sur Actualiser pour rechercher de nouveaux pilotes</string> + <string name="drivers_bundle_sets_display">Android + Linux : définit aussi le pilote d\'affichage</string> + <string name="drivers_bundle_sets_runtime">Android + Linux : définit aussi le pilote d\'exécution</string> + <string name="drivers_runtime_builtin">le Turnip intégré à l\'environnement d\'exécution</string> + <string name="drivers_system_vulkan">Vulkan du système : aucune version fournie pour ce GPU</string> + <string name="drivers_bundled">%1$s (intégré)</string> + <string name="drivers_manual">Manuel</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">Une seule moitié est publiée pour le moment</string> + <string name="drivers_pair_size">%1$.0f Mo pour les deux</string> + <string name="drivers_none_to_set">Aucun pilote à définir : %1$s</string> + <string name="drivers_checking_latest">Recherche des derniers pilotes…</string> + <string name="drivers_up_to_date_check_failed">À jour (vérification impossible : %1$s)</string> + <string name="drivers_recommended_unpublished">Les pilotes recommandés ne sont pas publiés pour le moment</string> + <string name="drivers_downloading_pair">Téléchargement de %1$s %2$s…</string> + <string name="drivers_updated_to">Mis à jour vers %1$s %2$s.</string> + <string name="drivers_updated_to_restart">Mis à jour vers %1$s %2$s. Le pilote d\'affichage prendra effet après le redémarrage de DroidDeck.</string> + <string name="drivers_switched_to">Basculé vers %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Basculé vers %1$s %2$s. Le pilote d\'affichage prendra effet après le redémarrage de DroidDeck.</string> + <string name="drivers_using">Utilise %1$s %2$s.</string> + <string name="drivers_using_restart">Utilise %1$s %2$s. Le pilote d\'affichage prendra effet après le redémarrage de DroidDeck.</string> + <string name="drivers_not_a_driver">Ce n\'est pas un zip de pilote : ni un pilote AdrenoTools, ni un Turnip -Linux, ni un bundle Android + Linux</string> + <string name="drivers_import_failed">Échec de l\'importation : %1$s</string> + <string name="drivers_imported_unnamed">Pilote importé</string> + <string name="drivers_using_bundle">%1$s est utilisé comme pilote d\'exécution et comme pilote d\'affichage.</string> + <string name="drivers_using_bundle_restart">%1$s est utilisé comme pilote d\'exécution et comme pilote d\'affichage. Le pilote d\'affichage prendra effet après le redémarrage de DroidDeck.</string> + <string name="drivers_deleted_bundle">%1$s supprimé (les deux pilotes)</string> + <string name="drivers_deleted">%1$s supprimé</string> + <string name="drivers_release_status">Dernières versions : %1$s · vérifié %2$s</string> + <string name="drivers_release_status_failed">Dernières versions : %1$s · %2$s inaccessible · vérifié %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">il y a %1$d min</item> + <item quantity="many">il y a %1$d min</item> + <item quantity="other">il y a %1$d min</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">il y a %1$d h</item> + <item quantity="many">il y a %1$d h</item> + <item quantity="other">il y a %1$d h</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">il y a %1$d jour</item> + <item quantity="many">il y a %1$d jours</item> + <item quantity="other">il y a %1$d jours</item> + </plurals> + <string name="drivers_checking_sources">Vérification de Banners-Turnip, WinNative et DroidDeck…</string> + <string name="drivers_check_failed">échec de la vérification</string> + <string name="drivers_installed_pick_either">%1$s installé - choisissez-le dans l\'une ou l\'autre liste pour définir les deux pilotes</string> + <string name="drivers_installed_pick">%1$s installé - choisissez-le dans le menu</string> + + <string name="session_wait_removal">Attendez la fin de la suppression de l\'environnement d\'exécution</string> + <string name="session_title_game">Jeu</string> + <string name="session_touch_auto_touchpad">pavé tactile</string> + <string name="session_touch_auto_direct">direct</string> + <string name="session_swap_failed">Échec du remplacement : %1$s</string> + <string name="session_profile_save_failed">Échec de l\'enregistrement du profil : %1$s</string> + <string name="session_desktop_paused">Le bureau est en pause</string> + <string name="session_named_paused">%1$s est en pause</string> + <string name="session_program_paused">Le programme est en pause</string> + <string name="session_starting_desktop">Démarrage du bureau</string> + <string name="session_starting_named">Démarrage de %1$s</string> + <string name="session_starting_program">Démarrage du programme</string> + <string name="session_no_logs">Aucun journal pour cette session.</string> + <string name="session_logs_archive_failed">Impossible de créer l\'archive des journaux de session.</string> + <string name="session_logs_share_failed">Impossible de partager les journaux de session.</string> + <string name="session_starting_session">Démarrage de la session…</string> + <string name="session_runtime_downloading">téléchargement de l\'environnement d\'exécution Linux</string> + <string name="session_runtime_downloading_mb">téléchargement de l\'environnement d\'exécution Linux · %1$d sur %2$d Mo</string> + <string name="session_runtime_checking">vérification de l\'environnement d\'exécution Linux</string> + <string name="session_runtime_unpacking">décompression de l\'environnement d\'exécution Linux</string> + <string name="session_desktop_downloading">téléchargement du bureau</string> + <string name="session_desktop_downloading_mb">téléchargement du bureau · %1$d sur %2$d Mo</string> + <string name="session_desktop_checking">vérification du bureau</string> + <string name="session_desktop_unpacking">décompression du bureau</string> + <string name="session_proton_downloading">téléchargement de Proton Experimental (ARM64)</string> + <string name="session_proton_downloading_mb">téléchargement de Proton Experimental (ARM64) · %1$d sur %2$d Mo</string> + <string name="session_proton_checking">vérification de Proton Experimental (ARM64)</string> + <string name="session_proton_unpacking">décompression de Proton Experimental (ARM64)</string> + <string name="session_runtime_catalog_unreachable">Impossible d\'accéder au catalogue des environnements d\'exécution. Vérifiez la connexion et appuyez à nouveau sur Play.</string> + <string name="session_runtime_install_failed">L\'environnement d\'exécution Linux ne s\'est pas installé. Vérifiez-le dans « Paramètres » et appuyez à nouveau sur Play.</string> + <string name="session_desktop_install_failed">Le bureau ne s\'est pas installé (%1$s). Vérifiez la connexion et appuyez à nouveau sur Bureau.</string> + <string name="session_ended_named">%1$s s\'est arrêté de manière inattendue. Partagez les journaux dans un rapport de bug, ou réessayez.</string> + <string name="session_ended_program">Le programme s\'est arrêté de manière inattendue. Partagez les journaux dans un rapport de bug, ou réessayez.</string> + <string name="session_ended_desktop">Le bureau s\'est arrêté de manière inattendue. Partagez les journaux dans un rapport de bug, ou réessayez.</string> + <string name="session_ended_detail">Code de sortie %1$d · %2$s</string> + <string name="session_ended_no_log">aucun journal</string> + <plurals name="session_hint_enosys"> + <item quantity="one">Le journal indique %1$d erreur « Function not implemented » : l\'accélération seccomp de proot fait échouer un utilitaire sur cet appareil. Essayez %2$s → « %3$s » (ou « %4$s ») et relancez.</item> + <item quantity="many">Le journal indique %1$d erreurs « Function not implemented » : l\'accélération seccomp de proot fait échouer un utilitaire sur cet appareil. Essayez %2$s → « %3$s » (ou « %4$s ») et relancez.</item> + <item quantity="other">Le journal indique %1$d erreurs « Function not implemented » : l\'accélération seccomp de proot fait échouer un utilitaire sur cet appareil. Essayez %2$s → « %3$s » (ou « %4$s ») et relancez.</item> + </plurals> + + <string name="mode_decky_plugins">Plugins</string> + <string name="mode_decky_plugins_hint">Installez un ZIP de plugin pour la prochaine session Steam.</string> + <string name="mode_decky_plugin_downloading">Téléchargement…</string> + <string name="mode_decky_plugin_install_zip">Installer depuis un ZIP</string> + <string name="store_cat_games">Jeux</string> + <string name="store_cat_audio_video">Audio et vidéo</string> + <string name="store_cat_graphics">Graphisme</string> + <string name="store_cat_internet">Internet</string> + <string name="store_cat_office">Bureautique</string> + <string name="store_cat_developer">Développement</string> + <string name="store_cat_education">Éducation</string> + <string name="store_cat_science">Sciences</string> + <string name="store_cat_system">Système</string> + <string name="store_cat_utilities">Utilitaires</string> + <string name="store_section_popular">Populaires</string> + <string name="store_section_trending">Tendances</string> + <string name="store_section_new">Nouveautés</string> + <string name="store_section_updated">Mis à jour</string> + <string name="store_section_loading">chargement…</string> + <string name="store_flatpak_setup">Configuration de Flatpak</string> + <string name="store_problem">%1$s : %2$s</string> + <string name="store_updates_check_failed">Impossible de rechercher des mises à jour sur Flathub</string> + <string name="content_rom_installed">installé</string> + <string name="content_rom_folder">dossier</string> + <string name="theme_graphite">Graphite</string> + <string name="theme_graphite_detail">Surfaces graphite, avec le bleu de l\'orbe du logo pour tout élément sélectionné, ciblé ou pressé.</string> + <string name="theme_paper">Papier sur noir</string> + <string name="theme_paper_detail">Le blanc est la couleur principale ; le bleu de l\'icône signale les filets, les anneaux et les points.</string> + <string name="theme_icon_blue">Bleu icône</string> + <string name="theme_icon_blue_detail">Fond noir, texte blanc, et le bleu pour tout élément sélectionné, ciblé ou pressé.</string> + <string name="theme_electric">Marine électrique</string> + <string name="theme_electric_detail">Un bleu plus vif, sur des surfaces qui tirent vers le marine plutôt que vers le gris.</string> + <string name="pckb_key_esc">Échap</string> + <string name="pckb_key_ins">Inser</string> + <string name="pckb_key_del">Suppr</string> + <string name="pckb_key_home">Début</string> + <string name="pckb_key_end">Fin</string> + <string name="pckb_key_pgup">PgPréc</string> + <string name="pckb_key_pgdn">PgSuiv</string> + <string name="pckb_key_backspace">⌫ Retour</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Verr Maj</string> + <string name="pckb_key_enter">Entrée ⏎</string> + <string name="pckb_key_shift">⇧ Maj</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Espace</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <string name="game_sync_err_runtime">Installez d\'abord l\'environnement d\'exécution</string> + <string name="game_sync_err_steam_library">La bibliothèque Steam n\'est pas disponible</string> + <string name="game_sync_err_previous_sd">L\'ancienne bibliothèque SD n\'est pas disponible</string> + <string name="game_sync_err_game_storage">Le stockage des jeux n\'est pas disponible</string> + <string name="game_sync_err_added_games">Le dossier des jeux ajoutés n\'est pas disponible</string> + <string name="game_sync_err_manifests">Les manifestes Steam ne sont pas disponibles</string> + <string name="content_system_many">Nombreux systèmes</string> + <string name="fm_drive_internal">Interne</string> <string name="fm_drive_linux">Système Linux</string> + <string name="fm_drive_sd">Carte SD</string> + <string name="fm_drive_storage">Stockage</string> + <string name="fm_select">Sélectionner</string> + <string name="fm_properties">Propriétés</string> + <string name="fm_favorite_add">Ajouter aux favoris</string> + <string name="fm_favorite_remove">Retirer des favoris</string> + <string name="fm_rename">Renommer</string> + <string name="fm_copy">Copier</string> + <string name="fm_cut">Couper</string> + <string name="fm_actions">Actions</string> + <string name="fm_favorites">Favoris</string> + <string name="fm_pin_current">Épingler le dossier actuel</string> + <string name="fm_no_favorites">Aucun favori pour l\'instant - épinglez un dossier via son menu ⋮ pour y revenir rapidement.</string> + <string name="fm_unpin_favorite">Retirer des favoris</string> + <string name="fm_size_zero">0 octet</string> + <string name="fm_unit_bytes">octets</string> + <string name="fm_prop_location">Emplacement</string> + <string name="fm_prop_type">Type</string> + <string name="fm_prop_size">Taille</string> + <string name="fm_prop_modified">Modifié</string> + <string name="fm_type_folder">Dossier</string> + <string name="fm_type_file">Fichier</string> + <string name="fm_type_ext_file">Fichier %1$s</string> + <string name="fm_read_only">Lecture seule</string> + <string name="fm_read_only_hint">Empêche les jeux (et Wine) d\'écraser ou de supprimer ce fichier.</string> + <string name="fm_read_only_failed">Impossible de modifier « Lecture seule »</string> + <string name="fm_hidden">Masqué</string> + <string name="fm_hidden_unsupported">Ce stockage ne peut pas enregistrer l\'attribut masqué.</string> + <string name="fm_hidden_hint">Marque le fichier comme masqué sous Windows (attribut masqué de Wine).</string> + <string name="fm_hidden_failed">Impossible de modifier « Masqué » sur ce stockage</string> + <string name="fm_done">Terminé</string> + <string name="fm_op_deleting">Suppression...</string> + <string name="fm_delete_failed">Échec de la suppression</string> + <string name="fm_op_moving">Déplacement...</string> + <string name="fm_op_copying">Copie...</string> + <string name="fm_op_moving_item">Déplacement - %1$s</string> + <string name="fm_op_copying_item">Copie - %1$s</string> + <string name="fm_op_moving_batch">Déplacement %1$d/%2$d - %3$s</string> + <string name="fm_op_copying_batch">Copie %1$d/%2$d - %3$s</string> + <string name="fm_paste_failed">%1$d terminé(s), %2$d en échec</string> + <string name="fm_paste_skipped">%1$d terminé(s), %2$d ignoré(s)</string> + <plurals name="fm_paste_moved"> + <item quantity="one">%1$d élément déplacé</item> + <item quantity="many">%1$d éléments déplacés</item> + <item quantity="other">%1$d éléments déplacés</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">%1$d élément copié</item> + <item quantity="many">%1$d éléments copiés</item> + <item quantity="other">%1$d éléments copiés</item> + </plurals> + <string name="fm_already_exists">« %1$s » existe déjà</string> + <string name="fm_op_renaming">Renommage...</string> + <string name="fm_rename_failed">Échec du renommage</string> + <string name="fm_op_creating_folder">Création du dossier...</string> + <string name="fm_create_folder_failed">Impossible de créer le dossier</string> + <string name="fm_new_folder">Nouveau dossier</string> + <string name="fm_folder_name">Nom du dossier</string> + <string name="fm_create">Créer</string> + <string name="fm_new_name">Nouveau nom</string> + <string name="fm_delete_title">Supprimer ?</string> + <string name="fm_delete_text">Supprimer définitivement « %1$s » ?</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">Supprimer %1$d élément ?</item> + <item quantity="many">Supprimer %1$d éléments ?</item> + <item quantity="other">Supprimer %1$d éléments ?</item> + </plurals> + <string name="fm_cannot_undo">Cette action est irréversible.</string> + <plurals name="fm_and_more"> + <item quantity="one">…et %1$d autre</item> + <item quantity="many">…et %1$d autres</item> + <item quantity="other">…et %1$d autres</item> + </plurals> + <string name="fm_op_deleting_batch">Suppression %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">%1$d élément n\'a pas pu être supprimé</item> + <item quantity="many">%1$d éléments n\'ont pas pu être supprimés</item> + <item quantity="other">%1$d éléments n\'ont pas pu être supprimés</item> + </plurals> + <string name="fm_conflict_merge_hint">Fusionner ajoute et remplace des fichiers dans le dossier existant.</string> + <string name="fm_conflict_overwrite_hint">Écraser remplace le fichier existant.</string> + <string name="fm_conflict_apply_all">Appliquer à tous les conflits</string> + <string name="fm_conflict_merge">Fusionner</string> + <string name="fm_conflict_overwrite">Écraser</string> + <string name="fm_conflict_keep_both">Conserver les deux</string> + <string name="fm_conflict_skip">Ignorer</string> + <string name="fm_select_this_folder">Sélectionner ce dossier</string> + <string name="fm_back">Retour</string> + <string name="fm_drive_not_readable">%1$s est monté, mais illisible pour le moment</string> + <string name="fm_list_view">Vue en liste</string> + <string name="fm_grid_view">Vue en grille</string> + <string name="fm_search">Rechercher</string> + <string name="fm_sort">Trier</string> + <string name="fm_sort_name">Nom</string> + <string name="fm_sort_date">Date de modification</string> + <string name="fm_rows_comfortable">Lignes aérées</string> + <string name="fm_rows_compact">Lignes compactes</string> + <string name="fm_hide_hidden">Masquer les fichiers cachés</string> + <string name="fm_show_hidden">Afficher les fichiers cachés</string> + <string name="fm_hide_favorites">Masquer les favoris</string> + <string name="fm_show_favorites">Afficher les favoris</string> + <string name="fm_filter_folder">Filtrer ce dossier</string> + <string name="fm_free_space">%1$s libres</string> + <string name="fm_selected_count">%1$d sélectionné(s)</string> + <string name="fm_select_none">Aucun</string> + <string name="fm_select_all">Tout</string> + <string name="fm_paste">Coller</string> + <plurals name="fm_items"> + <item quantity="one">%1$d élément</item> + <item quantity="many">%1$d éléments</item> + <item quantity="other">%1$d éléments</item> + </plurals> + <string name="fm_paste_here">Coller %1$s ici</string> + <string name="fm_paste_move_here">Coller %1$s ici (déplacer)</string> + <string name="fm_rail_title">Fichiers</string> + <string name="fm_rail_storage">STOCKAGE</string> + <string name="fm_rail_quick">ACCÈS RAPIDE</string> + <string name="fm_rail_favorites">FAVORIS</string> + <string name="fm_rail_downloads">Téléchargements</string> + <string name="fm_rail_downloads_short">Téléch.</string> + <string name="fm_rail_roms">ROMs</string> + <string name="fm_rail_session_logs">Journaux de session</string> + <string name="fm_rail_pictures">Images</string> + <string name="fm_favorite_added">« %1$s » ajouté aux favoris</string> + <string name="fm_favorite_removed">« %1$s » retiré des favoris</string> + <string name="fm_empty_directory">Dossier vide</string> + <string name="fm_up_one_level">Remonter d\'un niveau</string> + <string name="fm_choose_folder">Choisir un dossier</string> + <string name="fm_choose_file">Choisir un fichier</string> + <string name="ctrl_tint_steam_blue">Bleu Steam</string> + <string name="ctrl_tint_sky">Ciel</string> + <string name="ctrl_tint_white">Blanc</string> + <string name="ctrl_tint_violet">Violet</string> + <string name="ctrl_tint_green">Vert</string> + <string name="ctrl_tint_amber">Ambre</string> + <string name="ctrl_tint_red">Rouge</string> + <string name="ctrl_tint_pink">Rose</string> + <string name="ctrl_button_a">Bouton A</string> + <string name="ctrl_button_b">Bouton B</string> + <string name="ctrl_button_x">Bouton X</string> + <string name="ctrl_button_y">Bouton Y</string> + <string name="ctrl_button_lb">Bouton bombé gauche</string> + <string name="ctrl_button_rb">Bouton bombé droit</string> + <string name="ctrl_button_lt">Gâchette gauche</string> + <string name="ctrl_button_rt">Gâchette droite</string> + <string name="ctrl_button_view">Bouton Affichage</string> + <string name="ctrl_button_menu">Bouton Menu</string> + <string name="ctrl_target_view">Affichage</string> + <string name="ctrl_target_menu">Menu</string> + <string name="ctrl_target_up">Croix haut</string> + <string name="ctrl_target_down">Croix bas</string> + <string name="ctrl_target_left">Croix gauche</string> + <string name="ctrl_target_right">Croix droite</string> + <string name="ctrl_target_hidden">Masqué</string> + <string name="ctrl_editor_drag">Faites glisser pour déplacer</string> + <string name="ctrl_editor_snap">Aimanter</string> + <string name="ctrl_editor_save">Enregistrer</string> + <string name="second_mode_none">Aucun</string> + <string name="second_mode_keyboard_trackpad">Clavier + pavé tactile</string> + <string name="second_mode_terminal">Terminal</string> + <string name="second_mode_deck_controls">Poignées Deck + pavés tactiles</string> + + <string name="rtinst_removing">Suppression de l\'environnement d\'exécution Linux</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">Suppression de l\'environnement d\'exécution Linux · %1$d élément</item> + <item quantity="many">Suppression de l\'environnement d\'exécution Linux · %1$d éléments</item> + <item quantity="other">Suppression de l\'environnement d\'exécution Linux · %1$d éléments</item> + </plurals> + <string name="rtinst_removed">Environnement d\'exécution Linux supprimé</string> + <string name="rtinst_removal_failed">Impossible de terminer la suppression de l\'environnement d\'exécution Linux. Réessayez la suppression dans « Paramètres ».</string> + <string name="rtinst_removing_leftovers">Suppression des restes de l\'environnement d\'exécution</string> + <string name="rtinst_downloading">Téléchargement</string> + <string name="rtinst_verifying">Vérification</string> + <string name="rtinst_extracting">Extraction</string> + <string name="rtinst_channel_description">Affiché pendant l\'installation ou la suppression de l\'environnement d\'exécution Linux</string> + <string name="rtinst_title_installing">Installation de l\'environnement d\'exécution Linux</string> + <string name="rtinst_title_removing">Suppression de l\'environnement d\'exécution Linux</string> + + <string name="deskpkg_runtime_missing">L\'environnement d\'exécution Linux n\'est pas installé</string> + <string name="deskpkg_no_checksum">Le catalogue n\'a pas de somme de contrôle pour %1$s</string> + <string name="deskpkg_checksum_mismatch">Somme de contrôle incorrecte - rien n\'a été modifié</string> + <string name="deskpkg_installing">Installation de %1$s</string> + <string name="deskpkg_place_failed">Impossible de placer l\'AppImage</string> + <string name="deskpkg_extract_failed">Échec de l\'extraction</string> + <string name="deskpkg_install_failed">Échec de l\'installation</string> + + <string name="flatpakmgr_setup_failed">Échec de la configuration de Flatpak (code %1$d)</string> + <string name="flatpakmgr_setup_unfinished">La configuration de Flatpak ne s\'est pas terminée</string> + <string name="flatpakmgr_busy">Une autre tâche de la boutique est en cours</string> + <string name="flatpakmgr_not_ready">Configurez d\'abord Flatpak</string> + <string name="flatpakmgr_starting">Démarrage</string> + <string name="flatpakmgr_exit_status">Flatpak s\'est arrêté avec le code %1$d</string> + <string name="flatpakmgr_installing">Installation : %1$s</string> + <string name="flatpakmgr_updating">Mise à jour : %1$s</string> + <string name="flatpakmgr_removing">Suppression : %1$s</string> + <string name="flatpakmgr_installing_step">Installation : %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_updating_step">Mise à jour : %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_removing_step">Suppression : %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_ref_translations">traductions</string> + <string name="flatpakmgr_ref_graphics_drivers">pilotes graphiques</string> + <string name="flatpakmgr_ref_media_codecs">codecs multimédias</string> + <string name="flatpakmgr_ref_runtime">environnement d\'exécution %1$s</string> + <string name="flatpakmgr_ref_runtime_branch">environnement d\'exécution %1$s %2$s</string> + + <string name="appimg_too_small">Le fichier est trop petit pour être un AppImage</string> + <string name="appimg_unreadable">Impossible de lire le fichier : %1$s</string> + <string name="appimg_not_linux">Ce n\'est pas un AppImage (ce n\'est pas un programme Linux)</string> + <string name="appimg_wrong_arch">Cet AppImage n\'est conçu ni pour ARM64 (aarch64) ni pour les PC x86</string> + <string name="appimg_not_appimage">C\'est un programme Linux, mais pas un AppImage</string> + <string name="appimg_wrong_type">Seuls les AppImage de type 2 peuvent être importés (celui-ci est de type %1$d)</string> + <string name="appimg_no_space_import">Espace libre insuffisant : l\'importation nécessite environ %1$s</string> + <string name="appimg_no_space_update">Espace libre insuffisant : la mise à jour nécessite environ %1$s</string> + <string name="appimg_import_failed">Échec de l\'importation</string> + <string name="appimg_gone">Cet AppImage n\'existe plus</string> + <string name="appimg_update_failed">Échec de la mise à jour</string> + <string name="appimg_extracting">Extraction de %1$s</string> + <string name="appimg_extract_failed">Impossible d\'extraire l\'AppImage</string> + <string name="appimg_extract_failed_detail">Impossible d\'extraire l\'AppImage : %1$s</string> + + <string name="deckymgr_stop_session_loader">Arrêtez la session en cours avant d\'installer Decky Loader</string> + <string name="deckymgr_download_failed">Échec du téléchargement de Decky Loader</string> + <string name="deckymgr_size_mismatch">La taille de Decky Loader ne correspond pas aux métadonnées de la version</string> + <string name="deckymgr_checksum_download_failed">Impossible de télécharger la somme de contrôle de la version</string> + <string name="deckymgr_checksum_missing">La somme de contrôle de la version est manquante</string> + <string name="deckymgr_checksum_invalid">La somme de contrôle de la version n\'est pas valide</string> + <string name="deckymgr_checksum_mismatch">Somme de contrôle de Decky Loader incorrecte</string> + <string name="deckymgr_not_elf">Le fichier de la version n\'est pas un exécutable Linux 64 bits compatible</string> + <string name="deckymgr_session_started_loader">Une session a démarré pendant le téléchargement ; arrêtez-la avant d\'installer Decky Loader</string> + <string name="deckymgr_loader_not_executable">Impossible de rendre PluginLoader exécutable</string> + <string name="deckymgr_cef_failed">Impossible d\'activer le débogage distant CEF de Steam</string> + <string name="deckymgr_stage_failed">Impossible de préparer PluginLoader</string> + <string name="deckymgr_loader_preserve_failed">Impossible de conserver le PluginLoader existant</string> + <string name="deckymgr_loader_activate_failed">Impossible d\'activer le nouveau PluginLoader</string> + <string name="deckymgr_stop_session_plugin">Arrêtez la session en cours avant d\'installer un plugin</string> + <string name="deckymgr_loader_required">Installez Decky Loader avant d\'importer un plugin</string> + <string name="deckymgr_zip_unreadable">Impossible de lire le fichier ZIP sélectionné</string> + <string name="deckymgr_plugins_dir_failed">Impossible de créer le dossier des plugins Decky</string> + <string name="deckymgr_extracting_zip">Extraction du ZIP du plugin</string> + <string name="deckymgr_zip_one_folder">Le ZIP du plugin doit contenir un seul dossier de plugin à la racine</string> + <string name="deckymgr_zip_missing_files">Il manque plugin.json, package.json ou dist/index.js dans le ZIP du plugin</string> + <string name="deckymgr_plugin_name_missing">Le nom du plugin est manquant</string> + <string name="deckymgr_session_started_plugin">Une session a démarré pendant l\'installation ; arrêtez-la avant d\'installer un plugin</string> + <string name="deckymgr_plugin_installed">Plugin installé · redémarrez Steam pour le charger</string> + <string name="deckymgr_plugin_install_failed">Échec de l\'installation du ZIP du plugin</string> + <string name="deckymgr_zip_too_many">Le ZIP du plugin contient trop d\'éléments</string> + <string name="deckymgr_zip_invalid_path">Le ZIP du plugin contient un chemin non valide</string> + <string name="deckymgr_zip_duplicate_paths">Le ZIP du plugin contient des chemins en double</string> + <string name="deckymgr_zip_unsafe_path">Le ZIP du plugin contient un chemin dangereux</string> + <string name="deckymgr_folder_failed">Impossible de créer le dossier du plugin</string> + <string name="deckymgr_zip_too_large">Une fois décompressé, le ZIP du plugin dépasse la limite de 1 Gio</string> + <string name="deckymgr_bundled_not_executable">Impossible de rendre exécutable le binaire fourni avec le plugin</string> + <string name="deckymgr_remote_entry_invalid">Entrée remote_binary non valide</string> + <string name="deckymgr_remote_name_invalid">Nom de binaire distant non valide</string> + <string name="deckymgr_remote_url_https">L\'URL du binaire distant doit utiliser HTTPS</string> + <string name="deckymgr_remote_hash_invalid">Le binaire distant %1$s a un hachage SHA-256 non valide</string> + <string name="deckymgr_bin_dir_failed">Impossible de créer le dossier bin du plugin</string> + <string name="deckymgr_downloading_binary">Téléchargement du binaire du plugin · %1$s</string> + <string name="deckymgr_binary_download_failed">Impossible de télécharger le binaire du plugin %1$s</string> + <string name="deckymgr_binary_checksum_mismatch">Somme de contrôle du binaire du plugin incorrecte · %1$s</string> + <string name="deckymgr_binary_not_executable">Impossible de rendre exécutable le binaire du plugin · %1$s</string> + <string name="deckymgr_binary_replace_failed">Impossible de remplacer le binaire du plugin · %1$s</string> + <string name="deckymgr_binary_install_failed">Impossible d\'installer le binaire du plugin · %1$s</string> + <string name="deckymgr_folder_taken">Le dossier du plugin appartient à un autre plugin</string> + <string name="deckymgr_preserve_failed">Impossible de conserver le plugin existant</string> + <string name="deckymgr_activate_failed">Impossible d\'activer le plugin importé</string> + <string name="deckymgr_settings_failed">Impossible de mettre à jour les paramètres des plugins Decky</string> + <string name="deckymgr_restore_failed">Impossible de restaurer le plugin existant</string> + + <string name="adbfix_save_failed">Impossible d\'enregistrer l\'identité du débogage sans fil</string> + <string name="adbfix_command_failed">Échec de la commande ADB (%1$d)</string> + <string name="adbfix_not_confirmed">Android n\'a pas confirmé la modification de la limite des processus enfants : %1$s</string> + <string name="adbfix_no_pairing">Aucun appairage de débogage sans fil n\'est enregistré</string> + <string name="adbfix_enter_address">Saisissez l\'adresse IP et le port actuels du débogage sans fil</string> + + <string name="adbpair_timed_out">Délai dépassé en attendant la fenêtre d\'appairage. Réessayez depuis DroidDeck.</string> + <string name="adbpair_discovery_failed_code">Impossible de rechercher la fenêtre d\'appairage (%1$d). Vérifiez que le Wi-Fi est activé.</string> + <string name="adbpair_discovery_failed">Impossible de rechercher la fenêtre d\'appairage : %1$s</string> + <string name="adbpair_six_digits">Saisissez les 6 chiffres du code</string> + <string name="adbpair_pairing">Appairage…</string> + <string name="adbpair_code_rejected">Ce code n\'a pas fonctionné. Rouvrez la fenêtre d\'appairage et saisissez son nouveau code.</string> + <string name="adbpair_connecting">Appairé. Connexion…</string> + <string name="adbpair_port_not_found">Appairé, mais le port de débogage sans fil est introuvable. Laissez le débogage sans fil activé et réessayez.</string> + <string name="adbpair_applying">Application du paramètre…</string> + <string name="adbpair_command_failed">Échec de la commande de débogage sans fil</string> + <string name="adbpair_channel_name">Configuration du débogage sans fil</string> + <string name="adbpair_channel_description">Demande le code d\'appairage du débogage sans fil pendant que les paramètres sont ouverts</string> + <string name="adbpair_title_pair">Appairer le débogage sans fil</string> + <string name="adbpair_waiting_text">Activez le débogage sans fil, puis appuyez sur « Associer l\'appareil avec un code d\'appairage ».</string> + <string name="adbpair_waiting_long">Activez le débogage sans fil, puis appuyez sur « Associer l\'appareil avec un code d\'appairage ». Restez dans les paramètres : le code se saisit dans cette notification.</string> + <string name="adbpair_code_label">Code d\'appairage à 6 chiffres</string> + <string name="adbpair_code_hint">Appuyez sur « Saisir le code » et tapez les 6 chiffres affichés dans les paramètres.</string> + <string name="adbpair_title_code">Saisissez le code d\'appairage Wi-Fi</string> + <string name="adbpair_title_retry">Saisissez à nouveau le code</string> + <string name="adbpair_enter_code">Saisir le code</string> + <string name="adbpair_title_working">Configuration de Steam</string> + <string name="adbpair_title_done">Steam est prêt</string> + <string name="adbpair_done_text">Appuyez pour revenir à DroidDeck. Vous pouvez maintenant désactiver le débogage sans fil.</string> + <string name="adbpair_title_failed">Configuration du débogage sans fil interrompue</string> + + <string name="selfinst_http">Le téléchargement a renvoyé HTTP %1$d</string> + <string name="selfinst_size_mismatch">La taille du téléchargement ne correspond pas à l\'APK publié ; il a été supprimé - réessayez</string> + <string name="selfinst_checksum_mismatch">Le téléchargement ne correspond pas à sa somme de contrôle et a été supprimé - réessayez</string> + <string name="selfinst_wrong_package">La mise à jour concerne le paquet Android %1$s, et non %2$s</string> + <string name="selfinst_metadata_signer">Les métadonnées de la mise à jour ne mentionnent pas la clé de signature de publication de DroidDeck</string> + <string name="selfinst_unreadable">Android n\'a pas pu lire l\'APK téléchargé</string> + <string name="selfinst_apk_wrong_package">L\'APK téléchargé concerne %1$s, et non %2$s</string> + <string name="selfinst_version_mismatch">L\'APK téléchargé indique versionCode %1$d, et non le %2$d publié</string> + <string name="selfinst_downgrade">Android refuse d\'installer versionCode %1$d par-dessus le %2$d installé</string> + <string name="selfinst_no_signing">L\'APK téléchargé ne contient aucune information de signature</string> + <string name="selfinst_wrong_signer">L\'APK téléchargé n\'est pas signé avec la clé de publication de DroidDeck</string> + <string name="appupd_http">Le catalogue des mises à jour a renvoyé HTTP %1$d</string> + + <string name="loadstate_starting">Démarrage de la session…</string> + <string name="loadstate_elapsed">%1$d:%2$02d écoulées · toujours en cours</string> + <string name="loadstate_steam_update">Téléchargement de la mise à jour du client Steam · %1$d%%</string> + <string name="loadstate_steam_client">Téléchargement du client Steam · %1$s</string> + + <string name="pextra_busy">Une installation d\'outil de compatibilité est déjà en cours</string> + <string name="pextra_stop_session">Arrêtez la session en cours avant d\'installer des outils de compatibilité</string> + <string name="pextra_no_release">Aucune version ARM64 trouvée pour %1$s</string> + <string name="pextra_no_space">Au moins 4 Go libres, plus la taille du téléchargement, sont nécessaires pour installer %1$s</string> + <string name="pextra_downloading">Téléchargement de %1$s %2$s</string> + <string name="pextra_retrying">Nouvelle tentative de téléchargement · essai %1$d sur %2$d</string> + <string name="pextra_interrupted">Téléchargement interrompu</string> + <string name="pextra_download_failed">Échec du téléchargement ; le fichier partiel est conservé pour pouvoir reprendre</string> + <string name="pextra_too_big">Le téléchargement dépassait la taille de la version et a été supprimé</string> + <string name="pextra_incomplete">Le téléchargement est incomplet ; le fichier partiel est conservé pour une reprise ultérieure</string> + <string name="pextra_verifying">Vérification du téléchargement</string> + <string name="pextra_checksum_unreadable">Impossible de lire la somme de contrôle de la version ; réessayez</string> + <string name="pextra_no_checksum">%1$s %2$s ne publie aucune somme de contrôle ; rien n\'a été installé</string> + <string name="pextra_checksum_mismatch">Somme de contrôle incorrecte ; le téléchargement a été supprimé</string> + <string name="pextra_session_started">Une session a démarré pendant le téléchargement ; arrêtez-la avant d\'installer des outils de compatibilité</string> + <string name="pextra_installing">Installation de %1$s</string> + <string name="pextra_install_failed">Échec de l\'installation de %1$s (code %2$d)</string> + <string name="pextra_could_not_install">Impossible d\'installer %1$s</string> + <string name="esync_fetching">Récupération du pack droiddeck-esync</string> + + <string name="cmgr_fex_linux_helpers">avec les utilitaires Linux</string> + <string name="cmgr_fex_dlls_only">DLL uniquement</string> + <string name="cmgr_original_proton_updated">Original (Proton mis à jour depuis le dernier remplacement)</string> + <string name="cmgr_changed_outside">Modifié en dehors de « Composants »</string> + <string name="cmgr_not_present">absent</string> + <string name="cmgr_original_version">Original %1$s</string> + <string name="cmgr_original_from">Original issu de %1$s</string> + <string name="cmgr_swap_queued">%1$s %2$s sera placé dans %3$s à la fermeture du jeu qui l\'utilise.</string> + <string name="cmgr_swap_done">%1$s %2$s est maintenant dans %3$s. Prendra effet au prochain lancement d\'un jeu.</string> + <string name="cmgr_original_missing">Cet ensemble original n\'est pas enregistré</string> + <string name="cmgr_restore_queued">%1$s de %2$s reviendra à l\'original à la fermeture du jeu qui l\'utilise.</string> + <string name="cmgr_restore_done">%1$s de %2$s restauré à partir de l\'original %3$s.</string> + <string name="cmgr_package_in_use">%1$s est utilisé dans %2$s. Remplacez-le d\'abord par autre chose dans ce Proton.</string> + <string name="cmgr_package_not_stored">%1$s n\'est pas enregistré</string> + <string name="cmgr_deleted">%1$s supprimé.</string> + <string name="cmgr_original_kept">L\'original de la version installée est conservé : c\'est le moyen d\'y revenir.</string> + <string name="cmgr_not_stored">Non enregistré</string> + <string name="cmgr_deleted_original">Original %1$s de %2$s supprimé.</string> + <string name="cmgr_nightlies_only">Les téléchargements proviennent uniquement des versions Nightlies</string> + <string name="cmgr_list_predates_checksums">Cette liste de paquets est antérieure aux sommes de contrôle - actualisez-la et réessayez</string> + <string name="cmgr_checksum_mismatch">Somme de contrôle incorrecte</string> + + <string name="sprefs_upscaler_linear">Linéaire</string> + <string name="sprefs_upscaler_nearest">Plus proche voisin</string> + <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (qualité)</string> + <string name="sprefs_upscaler_sharpen">Netteté seule</string> + <string name="sprefs_lod_bias_auto">Auto (selon la mise à l\'échelle)</string> + + <string name="look_off_desc">Aucun effet appliqué.</string> + <string name="look_game_clarity">Clarté de jeu</string> + <string name="look_game_clarity_desc">Netteté avec une pointe de contraste - pour tous les jours.</string> + <string name="look_vivid">Éclatant</string> + <string name="look_vivid_desc">Couleurs poussées à fond. Anti-banding activé pour que le ciel ne forme pas de bandes.</string> + <string name="look_cinematic">Cinématique</string> + <string name="look_cinematic_desc">Contraste plus profond, couleurs plus froides, contours lissés.</string> + <string name="look_competitive">Compétitif</string> + <string name="look_competitive_desc">Ombres éclaircies et netteté renforcée - repérez les adversaires en premier.</string> + <string name="look_adaptive_sharpen">Netteté adaptative</string> + <string name="look_adaptive_sharpen_desc">Netteté uniquement, couleurs intactes.</string> + <string name="look_filmic">Filmique</string> + <string name="look_filmic_desc">Courbe façon pellicule, légèrement atténuée.</string> + <string name="look_arcade">Arcade</string> + <string name="look_arcade_desc">Intense et percutant.</string> + <string name="look_retro_crt">CRT rétro</string> + <string name="look_retro_crt_desc">Les passes tube CRT et signal analogique combinées.</string> + <string name="look_upscale_sharp">Agrandissement net</string> + <string name="look_upscale_sharp_desc">Pour une résolution inférieure à celle de l\'écran.</string> + <string name="look_pixel_clean">Pixels nets</string> + <string name="look_pixel_clean_desc">Pixels nets pour la 2D et les vieux jeux.</string> + <string name="look_anime_edge">Contours anime</string> + <string name="look_anime_edge_desc">Contours façon dessin animé et netteté accrue.</string> + + <string name="gpuinfo_not_adreno">GPU non Adreno</string> + <string name="gpuinfo_supported">Pris en charge</string> + <string name="gpuinfo_experimental">Expérimental : ses pilotes sont des versions de test</string> + <string name="gpuinfo_below_tested">En dessous du matériel testé (Adreno 725 et plus récent) : risque de ne pas fonctionner</string> + <string name="gpuinfo_unsupported">Non pris en charge : DroidDeck nécessite un GPU Adreno (Snapdragon)</string> + + <string name="turnip_rate_limited">Limite de requêtes GitHub atteinte - réessayez plus tard</string> + <string name="turnip_github_http">GitHub a répondu HTTP %1$d</string> + <string name="turnip_download_http">le téléchargement a répondu HTTP %1$d</string> + <string name="turnip_list_predates_checksums">Cette liste de pilotes est antérieure aux sommes de contrôle - actualisez-la et réessayez</string> + <string name="turnip_checksum_mismatch">Somme de contrôle incorrecte - le téléchargement a été supprimé</string> + <string name="turnip_not_adrenotools">Ce n\'est pas un pilote AdrenoTools : %1$s</string> + <string name="turnip_reject_no_meta">aucun meta.json</string> + <string name="turnip_reject_meta_unreadable">meta.json est illisible (%1$s)</string> + <string name="turnip_reject_linux_driver">c\'est un pilote d\'exécution Linux (importez-le sous « Pilote d\'exécution Linux »)</string> + <string name="turnip_reject_wayland">c\'est un pilote de jeu Wayland, inutile pour cette application</string> + <string name="turnip_reject_no_library_name">meta.json n\'indique aucun libraryName, rien ne peut donc être transmis à AdrenoTools</string> + <string name="turnip_reject_library_missing">meta.json indique %1$s, qui n\'est pas dans le zip</string> + <string name="turnip_reject_not_aarch64">%1$s n\'est pas une bibliothèque partagée ELF AArch64 64 bits</string> + <string name="turnip_reject_glibc">%1$s utilise glibc - c\'est un Turnip « -Linux », à importer sous « Pilote d\'exécution Linux »</string> + <string name="lvd_no_library">Aucun libvulkan_freedreno*.so dans ce zip. Un zip Turnip Android (AdrenoTools) ou -Wayland n\'est pas un pilote d\'exécution Linux.</string> + <string name="lvd_not_aarch64">%1$s n\'est pas une bibliothèque partagée ELF AArch64 64 bits.</string> + <string name="lvd_not_glibc">%1$s n\'est pas un pilote glibc - il utilise la libc d\'Android. L\'environnement d\'exécution Linux nécessite un zip « -Linux » ; un Turnip standard ou « -Wayland » ne peut pas du tout être chargé par le client Steam.</string> + <string name="bundle_no_manifest">Ce n\'est pas un bundle de pilotes : aucun manifest.json désignant un pilote Android et un pilote Linux</string> + <string name="bundle_entry_outside">Ce n\'est pas un bundle de pilotes : %1$s pointe en dehors de celui-ci</string> + <string name="bundle_too_large">Ce n\'est pas un bundle de pilotes : il dépasse %1$d Mo une fois décompressé</string> + + <string name="gstore_sd_card">Carte SD</string> + <string name="gstore_folder">Dossier</string> + + <string name="logshare_chooser">Partager les journaux de session</string> + + <string name="hdr_needs_android_10">nécessite Android 10 ou plus récent (couches d\'affichage avec espace colorimétrique)</string> + <string name="hdr_none">cet écran n\'indique aucune prise en charge HDR</string> + <string name="hdr_not_hdr10">cet écran prend en charge %1$s, mais pas HDR10</string> + + <string name="gpu_this_gpu">ce GPU</string> + <string name="phantom_title_on">Limite des processus enfants activée</string> + <string name="phantom_title_unset">Limite des processus enfants non définie</string> + <string name="phantom_title_unreadable">Impossible de vérifier la limite des processus enfants</string> + <string name="phantom_title_off">Limite des processus enfants désactivée</string> + <string name="phantom_title_not_required">Limite des processus enfants non requise</string> + <string name="phantom_instructions_fix">%1$s Si ce n\'est pas possible, connectez cet appareil à un ordinateur avec ADB et exécutez la commande ci-dessous. Revenez ensuite ici et vérifiez à nouveau.</string> + <string name="phantom_instructions_off">Les sessions Steam peuvent démarrer.</string> + <string name="phantom_instructions_not_applicable">Cette version d\'Android n\'utilise pas cette limite.</string> + <string name="phantom_fix_toggle">Activez « Désactiver les restrictions des processus enfants » dans les options pour les développeurs, puis laissez les options pour les développeurs activées.</string> + <string name="phantom_fix_no_toggle">Cette version d\'Android ne dispose pas de cet interrupteur. Utilisez le débogage sans fil pour le modifier depuis cet appareil.</string> + <string name="upd_block_no_apk">Cette version ne propose aucun téléchargement pour cette copie de DroidDeck.</string> + <string name="upd_block_version_code">Android ne l\'installera pas par-dessus cette version, car son versionCode %1$d est inférieur à celui installé (%2$d).</string> + <string name="upd_fail_downgrade">Android n\'installe pas une version plus ancienne par-dessus une plus récente. Restez sur cette version jusqu\'à ce que le canal la rattrape.</string> + <string name="upd_fail_signature">Cette copie de DroidDeck est signée différemment (version compilée localement ?), Android ne peut donc pas la mettre à jour sur place.</string> + <string name="upd_fail_storage">Espace libre insuffisant pour la mise à jour.</string> + <string name="upd_fail_aborted">La mise à jour a été annulée.</string> + <string name="upd_fail_with_message">La mise à jour n\'a pas été installée : %1$s</string> + <string name="upd_fail_generic">La mise à jour n\'a pas été installée.</string> + <string name="saves_layout_zip">Zip %1$s</string> + <string name="saves_zip">zip</string> + <string name="saves_not_launched">%1$s n\'a pas encore été lancé</string> + <string name="saves_none_found">Aucune sauvegarde trouvée pour %1$s</string> + <string name="saves_not_save_zip">%1$s ne contient pas de dossier drive_c : ce n\'est donc pas un zip de sauvegarde GameHub ou Winlator</string> + <string name="gstore_cannot_create">impossible de créer des dossiers dans %1$s</string> + <string name="gstore_cannot_write">impossible d\'écrire dans %1$s</string> + <string name="gstore_cannot_write_detail">impossible d\'écrire dans %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">Dossier indisponible</string> + <string name="gfiles_name_taken">Un autre fichier utilise déjà %1$s</string> + <string name="gfiles_art_unavailable">Dossier des illustrations indisponible</string> + <string name="gfiles_unrelated_inventory">Le dossier contient un inventaire de synchronisation sans rapport</string> + <string name="gfiles_remove_failed">Impossible de supprimer %1$s</string> + <string name="gfiles_outside_folder">Le fichier sort du dossier d\'exportation</string> + <string name="cmgr_fex_unnamed">FEX (version sans nom de version)</string> + <string name="cmgr_no_profile">aucun profile.json dans %1$s</string> + <string name="cmgr_not_component_package">%1$s n\'est pas un paquet de composants Linux (type %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s libres</string> + <string name="mode_storage_auto_short">Automatique</string> + <string name="appupd_catalog_rejected">Le catalogue de mises à jour n\'a pas passé les vérifications (%1$s)</string> + <string name="main_adb_clip_label">Paramètre de processus enfants DroidDeck</string> + <string name="setup_signed_in_offline_unnamed">Connecté · Mode hors ligne</string> + <string name="setup_signed_in_unnamed">Connecté</string> + <string name="driver_tag_bundled">INTÉGRÉ</string> + <string name="driver_tag_downloaded">TÉLÉCHARGÉ</string> + <string name="driver_tag_imported">IMPORTÉ</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Affichage de la session</string> + <string name="display_resolution">Résolution</string> + <string name="display_game_fit">Ajustement des jeux 16:9</string> + <string name="display_game_fit_hint">Comment les jeux 16:9 s\'adaptent à cet écran. À la prochaine session.</string> + <string name="display_game_fit_unavailable">Choisissez « Adapter à l\'écran » ou une taille personnalisée plus étroite que 16:9.</string> + <string name="display_preserve">Conserver les proportions</string> + <string name="display_stretch">Étirer sur l\'écran</string> + <string name="display_image_scaling">Mise à l\'échelle de l\'image</string> + <string name="display_filter">Filtre de mise à l\'échelle</string> + <string name="display_filter_hint">Partagé entre Steam et le bureau.</string> + <string name="display_filter_live">S\'applique en direct. Partagé entre Steam et le bureau.</string> + <string name="display_filter_note">FSR, GSR et NIS s\'appliquent lors de l\'agrandissement de l\'image de la session. « Netteté seule » fonctionne aussi sans agrandissement.</string> + <string name="display_sharpness">Netteté du filtre</string> + <string name="display_frame_rate">Fréquence d\'images</string> + <string name="display_look_filter">%1$s Règle la mise à l\'échelle sur %2$s.</string> + <string name="display_match_screen">Adapter à l\'écran (%1$d×%2$d)</string> + <string name="display_follow_screen">Suivre l\'écran (s\'adapte au pliage et au dépliage)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Afficher les détails de l\'appareil</string> + <string name="device_info_hide">Masquer les détails de l\'appareil</string> + <string name="device_info_loading">Lecture des informations de l\'appareil…</string> + <string name="device_info_copy">Copier</string> + <string name="device_info_copy_hint">Copiez ces détails dans un rapport d\'erreur.</string> + <string name="device_info_copied">Copié.</string> + <string name="force_ssbs_title">Forcer SSBS</string> + <string name="force_ssbs_hint">Maintient activé un réglage de vitesse du CPU que Wine désactive sans cesse. Peut accélérer les jeux Windows sur Snapdragon 8 Elite ; peu de différence constatée sur les puces plus anciennes. Prendra effet au prochain lancement du jeu.</string> + + <string name="rail_stores">Boutiques</string> + <string name="source_steam">Steam</string> + <string name="source_added">Personnalisé</string> + <string name="setup_stores">Boutiques</string> + <string name="setup_stores_open_on">Ouvrir une boutique sur</string> + <string name="setup_stores_speed">Niveau de vitesse de téléchargement</string> + <string name="setup_stores_engine">Moteur des boutiques</string> + <string name="setup_stores_engine_chip_ready">● Moteur prêt</string> + <string name="setup_stores_engine_chip_missing">Non compilé</string> + <string name="stores_tier_balanced">Équilibré</string> + <string name="stores_tier_fast">Rapide</string> + <string name="stores_tier_max">Max</string> + <string name="stores_title">Boutiques</string> + <string name="stores_settings">Paramètres des boutiques</string> + <string name="stores_settings_eyebrow">Paramètres</string> + <string name="stores_chip_signed_out">(non connecté)</string> + <string name="stores_tab_store">Boutique</string> + <string name="stores_tab_installed">Installés</string> + <string name="stores_tab_library">Bibliothèque</string> + <string name="stores_tab_all">Tous</string> + <string name="stores_tab_downloads">Téléchargements</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Se connecter</string> + <string name="stores_signout_named">%1$s · se déconnecter</string> + <string name="stores_signout_title">Se déconnecter de %1$s ?</string> + <string name="stores_signout_note">Les jeux installés sont conservés. La bibliothèque sera récupérée à nouveau lors de votre prochaine connexion.</string> + <string name="stores_signout_stay">Rester connecté</string> + <string name="stores_signout_go">Se déconnecter</string> + <string name="stores_search_catalog">Rechercher dans le catalogue %1$s…</string> + <string name="stores_search_installed">Rechercher dans les jeux installés…</string> + <string name="stores_search_library">Rechercher dans votre bibliothèque…</string> + <string name="stores_search_open">Rechercher</string> + <string name="stores_installed_empty">Aucun jeu installé</string> + <string name="stores_library_loading">Chargement</string> + <string name="stores_nothing_matches">Aucun jeu</string> + <string name="stores_shelves_loading">Chargement de la boutique…</string> + <string name="stores_amazon_no_catalog">Aucun catalogue</string> + <string name="stores_shelf_new">Nouveautés</string> + <string name="stores_shelf_deals">Promotions</string> + <string name="stores_shelf_free">Jeux gratuits</string> + <string name="stores_shelf_trending">Tendances</string> + <string name="stores_shelf_library">Votre bibliothèque</string> + <string name="stores_play">Jouer</string> + <string name="stores_install">Installer</string> + <string name="stores_resume_install">Reprendre l\'installation</string> + <string name="stores_dl_interrupted">Interrompu</string> + <string name="stores_install_size">Installer · %1$s</string> + <string name="stores_get_free">Obtenir gratuitement</string> + <string name="stores_view_on">Voir sur %1$s</string> + <string name="stores_buy_price">Acheter · %1$s</string> + <string name="stores_installed_chip">Installé</string> + <string name="stores_in_library">Dans la bibliothèque</string> + <string name="stores_free">Gratuit</string> + <string name="stores_resume">Reprendre</string> + <string name="stores_refresh">Actualiser</string> + <string name="stores_show_mature">Afficher le contenu pour adultes</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Hors ligne</string> + <string name="epic_card_none">Désactivé</string> + <string name="epic_eos">Connexion Epic (EOS)</string> + <string name="epic_offline">Lancer hors ligne</string> + <string name="epic_resolve">Corriger la connexion Epic</string> + <string name="store_cloud_saves">Sauvegardes cloud</string> + <string name="cloud_upload">Envoyer vers le cloud</string> + <string name="cloud_download">Télécharger depuis le cloud</string> + <string name="cloud_none">Aucune sauvegarde cloud</string> + <string name="cloud_conflict">Conflit cloud</string> + <string name="cloud_conflict_files">Conflit cloud · %1$d fichiers</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Garder la version cloud</string> + <string name="cloud_keep_local">Garder la version locale</string> + <string name="cloud_checking">Vérification du cloud…</string> + <string name="cloud_last_sync">Dernière synchro : %1$s</string> + <string name="cloud_never">Pas encore synchronisé</string> + <string name="cloud_done">%1$d fichiers</string> + <string name="stores_eyebrow_installed">installé</string> + <string name="stores_eyebrow_library">dans votre bibliothèque</string> + <string name="stores_eyebrow_store">boutique</string> + <string name="achievements_title">Succès</string> + <string name="achievements_count">%1$d sur %2$d débloqués</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Débloqués · %1$d</string> + <string name="achievements_group_locked">Verrouillés · %1$d</string> + <string name="achievements_group_hidden">Cachés · %1$d</string> + <string name="achievements_hidden">Succès caché</string> + <string name="achievements_hidden_detail">Les détails s\'affichent une fois débloqué.</string> + <string name="achievements_all">Tous les succès débloqués</string> + <string name="achievements_reading">Lecture…</string> + <string name="achievements_signed_out">Connectez-vous à %1$s pour voir vos succès.</string> + <string name="achievements_unreachable">Impossible de joindre %1$s. Réessayez une fois en ligne.</string> + <string name="achievements_none">%1$s n\'indique aucun succès pour ce jeu.</string> + <string name="achievements_show_all">Tout afficher (%1$d)</string> + <string name="achievements_dialog_title">Succès · %1$s</string> + <string name="stores_in_steam">✓ Dans Steam</string> + <string name="stores_uninstall">Désinstaller</string> + <string name="stores_uninstalling">Désinstallation…</string> + <string name="stores_uninstall_title">Désinstaller %1$s ?</string> + <string name="stores_uninstall_body_size">Libère %1$s et le retire de Steam.</string> + <string name="stores_uninstall_body">Supprime ses fichiers et son raccourci Steam.</string> + <string name="stores_launch_not_registered">Steam détectera le jeu à son prochain démarrage ; lancez-le depuis Steam, ou démarrez Steam maintenant.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Choix de Steam pour chaque jeu</string> + <string name="stores_card_audio">Audio</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Composants</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Moteur</string> + <string name="stores_card_engine_value">%1$s · natif</string> + <string name="stores_card_engine_java">%1$s · intégré</string> + <string name="stores_how_it_runs">Fonctionnement</string> + <string name="stores_kv_fetched">Récupéré depuis</string> + <string name="stores_kv_fetched_value">%1$s, avec votre compte</string> + <string name="stores_kv_installed_to">Installé dans</string> + <string name="stores_kv_installed_to_value">Stockage des jeux (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Lancé par</string> + <string name="stores_kv_launched_by_value">Steam et Proton, comme les jeux Steam</string> + <string name="stores_kv_launched_by_epic">Steam et Proton · connexion Epic à chaque lancement, superposition EOS désactivée</string> + <string name="stores_kv_launched_by_amazon">Steam et Proton · variables Amazon Games Services définies</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">ajouté à Steam à l\'installation (interface Deck + Steam Input)</string> + <string name="stores_downloads_empty">Aucun téléchargement</string> + <string name="stores_dl_paused">En pause</string> + <string name="stores_dl_queued">En attente</string> + <string name="stores_dl_queued_at">En attente · n° %1$d</string> + <string name="stores_dl_cancelled">Annulé</string> + <string name="stores_dl_failed">Échec</string> + <string name="stores_dl_failed_reason">Échec · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Pause</string> + <string name="stores_dl_resume">Reprendre</string> + <string name="stores_dl_cancel">Annuler</string> + <string name="stores_dl_retry">Réessayer</string> + <string name="stores_dl_clear">Effacer</string> + <string name="stores_dl_clear_all">Effacer les terminés</string> + <string name="stores_stage_manifest">Manifeste</string> + <string name="stores_stage_download">Téléchargement</string> + <string name="stores_stage_verify">Vérification</string> + <string name="stores_stage_install">Installation</string> + <string name="stores_stage_done">Terminé</string> + <string name="stores_stage_checking">Contrôle en cours</string> + <string name="stores_stage_downloading">Téléchargement en cours</string> + <string name="stores_dl_cancel_confirm">Supprimer le téléchargement ?</string> + <string name="stores_dl_failed_kept">Échec · %1$s conservés</string> + <string name="stores_stage_verifying">Vérification en cours</string> + <string name="stores_stage_installing">Installation en cours</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d fichiers</string> + <string name="stores_download_manager">Gestionnaire de téléchargements</string> + <string name="stores_parallel">Téléchargements simultanés</string> + <string name="stores_install_where">Installer dans</string> + <string name="stores_install_title">Installer %1$s :</string> + <string name="stores_target_internal">Interne</string> + <string name="stores_target_free">%1$s libres</string> + <string name="stores_notification_channel">Téléchargements des boutiques</string> + <string name="stores_notification_channel_hint">Affiche la progression des téléchargements de jeux et les poursuit en arrière-plan</string> + <string name="stores_notification_one">Téléchargement en cours</string> + <string name="stores_notification_many">%1$d téléchargements</string> + <string name="stores_notification_waiting">Préparation…</string> + <string name="stores_notification_done_channel">Téléchargements des boutiques terminés</string> + <string name="stores_notification_installed">%1$s installé</string> + <string name="stores_notification_failed">Échec du téléchargement de %1$s</string> + <string name="stores_notification_delete">Supprimer</string> + <string name="stores_notification_keep">Conserver</string> + <string name="stores_login_title">Se connecter à %1$s</string> + <string name="stores_login_provider_title">Connectez-vous avec votre fournisseur</string> + <string name="stores_login_reload">Recharger</string> + <string name="stores_login_close">Fermer</string> + <string name="stores_login_retry">Réessayer</string> + <string name="stores_login_finishing">Finalisation de la connexion…</string> + <string name="stores_login_error_network">Impossible de charger la page de connexion : %1$s</string> + <string name="stores_login_error_idp_blocked">Ce fournisseur refuse la connexion depuis une page intégrée. Connectez-vous plutôt avec le mot de passe de la boutique.</string> + <string name="stores_login_error_http">La page de connexion a renvoyé une erreur (HTTP %1$d).</string> + <string name="stores_login_error_ssl">La liaison avec la page de connexion n\'est pas sécurisée.</string> + <string name="stores_login_error_verification">Impossible de vérifier la connexion. Réessayez.</string> + <string name="stores_login_error_generic">Échec de la connexion. Réessayez.</string> <string name="common_close">Fermer</string> <string name="common_back">Retour</string> <string name="common_edit">Modifier</string> @@ -957,27 +2100,134 @@ <string name="art_reset">Revenir à automatique</string> <plurals name="games_added_n"> <item quantity="one">%1$d jeu ajouté</item> + <item quantity="many">%1$d jeux ajoutés</item> <item quantity="other">%1$d jeux ajoutés</item> </plurals> <plurals name="games_already_n"> <item quantity="one">%1$d déjà dans Jeux</item> + <item quantity="many">%1$d déjà dans Jeux</item> <item quantity="other">%1$d déjà dans Jeux</item> </plurals> - <string name="achievements_title">Succès</string> - <string name="achievements_count">%1$d sur %2$d débloqués</string> - <string name="achievements_count_short">%1$d / %2$d</string> - <string name="achievements_group_unlocked">Débloqués · %1$d</string> - <string name="achievements_group_locked">Verrouillés · %1$d</string> - <string name="achievements_group_hidden">Cachés · %1$d</string> - <string name="achievements_hidden">Succès caché</string> - <string name="achievements_hidden_detail">Les détails s\'affichent une fois débloqué.</string> - <string name="achievements_all">Tous les succès débloqués</string> - <string name="achievements_reading">Lecture…</string> - <string name="achievements_signed_out">Connectez-vous à %1$s pour voir vos succès.</string> - <string name="achievements_unreachable">Impossible de joindre %1$s. Réessayez une fois en ligne.</string> - <string name="achievements_none">%1$s n\'indique aucun succès pour ce jeu.</string> - <string name="achievements_show_all">Tout afficher (%1$d)</string> - <string name="achievements_dialog_title">Succès · %1$s</string> + <string name="hop_back">B · Retour vers %1$s</string> + <string name="hop_from_games">le jeu</string> + <string name="hop_from_play">les paramètres de jeu</string> + <string name="sheet_graphics_drivers">Pilotes graphiques</string> + <string name="comp_this_proton">Ce Proton</string> + <string name="comp_every_proton">Tous les Proton</string> + <string name="comp_for_every_proton">Pour tous les Proton</string> + <string name="comp_available_count">Disponible · %1$d</string> + <string name="comp_checked">Vérifié : %1$s</string> + <string name="comp_check_all">Rechercher de nouveaux composants et pilotes</string> + <string name="comp_get">Obtenir</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton et FEX ci-dessous sont ceux de ce Proton</string> + <string name="comp_device_tested">%1$s · testé</string> + <string name="comp_device_untested">%1$s · non testé</string> + <string name="comp_device_unsupported">%1$s · non pris en charge</string> + <string name="gpu_auto_keeps">Maintient la paire recommandée à jour</string> + <string name="gpu_bar_auto">Auto · %1$s</string> + <string name="gpu_separately">Pilotes d\'exécution et d\'affichage séparément</string> + <string name="gpu_use_title">Utiliser %1$s ?</string> + <string name="proton_remove_title">Supprimer %1$s ?</string> + <string name="setup_tab_diagnostics">Diagnostic</string> + <string name="setup_ready">Prêt à jouer</string> + <string name="setup_not_ready">Pas encore prêt à jouer</string> + <string name="setup_ready_runtime">environnement d\'exécution %1$s</string> + <string name="setup_ready_limit_off">limite des processus désactivée</string> + <string name="setup_ready_signed_in">connecté en tant que %1$s</string> + <string name="setup_needs_you">Quand une intervention est requise</string> + <string name="setup_group_look">Apparence</string> + <string name="setup_group_device">Cet appareil</string> + <string name="setup_group_art">Illustrations de la bibliothèque</string> + <string name="flathub_eyebrow">APPLICATIONS LINUX</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Applications pour le bureau Linux, à ouvrir depuis l\'onglet Bureau.</string> + <string name="flathub_setup">Configurer Flathub</string> + <string name="flathub_remove_title">Supprimer %1$s ?</string> + <string name="flathub_remove_note">Ses paramètres et fichiers sont conservés.</string> + <string name="flathub_keep">Conserver</string> + <string name="desktop_size">Taille sur le disque</string> + <string name="desktop_uninstall_title">Désinstaller le bureau ?</string> + <string name="desktop_all_systems">Tous</string> + <string name="gog_achievements">Succès GOG</string> + <string name="gog_achievements_hint">Synchronise les succès des jeux GOG pris en charge. S\'applique au prochain lancement.</string> + <string name="gog_achievements_off">Succès désactivés</string> + <string name="gog_achievements_on">Succès activés</string> + <string name="gog_achievements_signin">Reconnectez-vous à GOG</string> + <string name="gog_achievements_uploaded">Succès envoyés</string> + <string name="gog_achievements_pending">Envoi du succès en attente</string> + <string name="gog_achievements_enable_store">Activez les succès GOG dans les paramètres des boutiques</string> + <string name="gog_achievements_unavailable">Succès indisponibles ; consultez les journaux de session</string> + <string name="achievement_unlocked">Succès débloqué</string> + <string name="touch_paste">Coller</string> + <string name="touch_dpad">Pavé directionnel</string> + <string name="touch_left_stick">Stick gauche</string> + <string name="touch_right_stick">Stick droit</string> + <string name="touch_menu">Menu</string> + <string name="touch_keyboard">Clavier</string> + <string name="touch_dpad_up">Pavé directionnel haut</string> + <string name="touch_dpad_down">Pavé directionnel bas</string> + <string name="touch_dpad_left">Pavé directionnel gauche</string> + <string name="touch_dpad_right">Pavé directionnel droite</string> + <string name="touch_title">Commandes tactiles</string> + <string name="touch_controller">Manette tactile</string> + <string name="touch_mouse">Souris</string> + <string name="touch_on">Activé</string> + <string name="touch_off">Désactivé</string> + <string name="touch_speed_slow">Lente</string> + <string name="touch_speed_normal">Normale</string> + <string name="touch_speed_fast">Rapide</string> + <string name="touch_edit_layout">Modifier la disposition</string> + <string name="touch_droiddeck_menu">Menu DroidDeck</string> + <string name="touch_drag_hint">Faites glisser des commandes sur l\'écran</string> + <string name="touch_arrange">Organiser</string> + <string name="touch_discard">Ignorer</string> + <string name="touch_saving">Enregistrement…</string> + <string name="touch_icon">Icône</string> + <string name="touch_narrower">L−</string> + <string name="touch_wider">L+</string> + <string name="touch_shorter">H−</string> + <string name="touch_taller">H+</string> + <string name="touch_hide">Masquer</string> + <string name="touch_color_opacity">Couleur et opacité…</string> + <string name="touch_copy_layout">Copier cette disposition</string> + <string name="touch_layout_copied">Disposition copiée ; collez-la dans la disposition tactile de n\'importe quel jeu</string> + <string name="touch_paste_layout">Coller la disposition copiée</string> + <string name="touch_reset_layout">Rétablir celle de Steam</string> + <string name="touch_no_binding">Cette commande n\'a pas d\'action assignée dans Steam pour lui donner une icône</string> + <string name="touch_icon_for">Icône pour %1$s</string> + <string name="touch_layout_color">Couleur de la disposition tactile</string> + <string name="touch_done">Terminé</string> + <string name="touch_save_failed">Impossible d\'enregistrer la disposition tactile</string> + <string name="touch_saved">Disposition tactile enregistrée sur Steam</string> + <string name="touch_app_id">Appli %1$d</string> + <string name="touch_label_hint">Libellé (facultatif)</string> + <string name="touch_icons_missing">Les icônes de Steam n\'ont pas été trouvées</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d icône de Steam</item> + <item quantity="many">%1$d icônes de Steam</item> + <item quantity="other">%1$d icônes de Steam</item> + </plurals> + <string name="touch_icon_color">Couleur de l\'icône</string> + <string name="touch_button_color">Couleur du bouton</string> + <string name="touch_no_icon">Aucune icône</string> + + <string name="stores_sync_sign_in">Vérification de la connexion…</string> + <string name="stores_sync_library">Récupération de votre bibliothèque…</string> + <string name="stores_sync_game_list">Récupération de la liste des jeux…</string> + <string name="stores_sync_details">Chargement des détails des jeux… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">Synchronisation de %1$d jeu…</item> + <item quantity="many">Synchronisation de %1$d jeux…</item> + <item quantity="other">Synchronisation de %1$d jeux…</item> + </plurals> + <string name="stores_sync_progress">Synchronisation… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Récupération des jaquettes…</string> + <string name="stores_sync_failed">Échec de la synchronisation</string> + <string name="stores_sync_no_games">Aucun jeu trouvé dans votre bibliothèque %1$s</string> + <string name="stores_sync_amazon_no_answer">Amazon Games n\'a pas répondu pour votre bibliothèque : actualisez, ou reconnectez-vous</string> + <string name="stores_sync_gog_unreachable">Impossible de contacter GOG</string> + <string name="stores_sync_gog_unreadable">Impossible de lire votre bibliothèque GOG</string> + <string name="stores_registering">Enregistrement auprès de Steam…</string> <string name="drawer_page_glasses">Lunettes</string> <string name="glasses_open">Lunettes VITURE</string> <string name="glasses_control">Contrôle des lunettes</string> @@ -1026,4 +2276,34 @@ <string name="stores_hltb_completionist">Complétionniste</string> <string name="stores_hltb_hours">%1$s h</string> <string name="stores_hltb_minutes">%1$d min</string> -</resources> \ No newline at end of file + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Mode Cardboard</string> + <string name="cardboard_mode_hint">Vues pour l\'œil gauche et l\'œil droit, pour un casque VR pour téléphone.</string> + <string name="cardboard_source">Image</string> + <string name="cardboard_source_screen">Écran virtuel</string> + <string name="cardboard_source_sbs">3D côte à côte</string> + <string name="cardboard_viewer">Casque</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Sans lentille</string> + <string name="cardboard_ipd">Écart entre les yeux</string> + <string name="cardboard_screen_width">Largeur de l\'écran</string> + <string name="cardboard_screen_distance">Distance de l\'écran</string> + <string name="cardboard_head_tracking">Suivi de la tête</string> + <string name="cardboard_no_sensor">Cet appareil n\'a pas de capteur de rotation.</string> + <string name="cardboard_recenter">Recentrer la vue</string> + <string name="cardboard_recenter_button">Recentrer</string> + <string name="cardboard_steamvr">SteamVR (expérimental)</string> + <string name="cardboard_steamvr_install">Installer</string> + <string name="cardboard_steamvr_install_hint">Télécharge SteamVR ARM64 de Valve (571 Mo).</string> + <string name="cardboard_steamvr_installing">Téléchargement et décompression de SteamVR…</string> + <string name="cardboard_steamvr_start">Démarrer</string> + <string name="cardboard_steamvr_ready_hint">Lance SteamVR avec votre tête comme casque ; l\'image passe en 3D côte à côte.</string> + <string name="cardboard_steamvr_stop">Arrêter</string> + <string name="cardboard_steamvr_running_hint">SteamVR est en cours d\'exécution.</string> + <string name="cardboard_steamvr_unavailable">Indisponible dans cette session.</string> + <string name="cardboard_steamvr_failed">SteamVR ne répond pas ; consultez le journal de session.</string> + <string name="drawer_component_downloads">Télécharger des composants</string> + <string name="drawer_component_downloads_hint">Les téléchargements sont partagés avec la page « Composants ». Choisissez une version téléchargée ci-dessus pour l\'utiliser au prochain lancement du jeu.</string> + <string name="drawer_component_choose_download">Choisissez une version à télécharger</string> +</resources> diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml new file mode 100644 index 000000000..b36f6fd37 --- /dev/null +++ b/app/src/main/res/values-hi/strings.xml @@ -0,0 +1,2235 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>पहली बार चलाने पर Steam डाउनलोड होता है। इसमें कुछ मिनट लग सकते हैं।</item> + <item>डिफ़ॉल्ट रूप से, ऐप से बाहर जाने पर Steam रुक जाता है; वापस आने पर यह वहीं से शुरू होता है जहां छोड़ा था।</item> + <item>आपका कंट्रोलर Xbox 360 पैड के रूप में दिखाई देता है। नहीं तो, ऑन-स्क्रीन कंट्रोल का उपयोग करें।</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>पहली बार चलाने पर डेस्कटॉप सेट हो जाता है। इसमें कुछ मिनट लग सकते हैं।</item> + <item>डिफ़ॉल्ट रूप से, ऐप से बाहर जाने पर सेशन रुक जाता है; वापस आने पर यह वहीं से शुरू होता है जहां छोड़ा था।</item> + <item>टच टचपैड की तरह काम करता है: पॉइंटर घुमाने के लिए ड्रैग करें, क्लिक के लिए टैप करें।</item> + <item>आपका कंट्रोलर Xbox 360 पैड के रूप में दिखाई देता है। नहीं तो, ऑन-स्क्रीन कंट्रोल का उपयोग करें।</item> + </string-array> + <string name="session_channel">सेशन</string> + <string name="session_channel_description">Steam या डेस्कटॉप सेशन खुला होने पर दिखता है</string> + <string name="session_notification">Steam चल रहा है</string> + <string name="session_notification_downloads">Steam डाउनलोड जारी है</string> + <string name="session_notification_paused">सेशन रुका हुआ है - तैयार होने पर फिर से शुरू करें पर टैप करें</string> + <string name="resume_session">फिर से शुरू करें</string> + <string name="suspend_now">अभी सस्पेंड करें</string> + <string name="stop_session">सेशन बंद करें</string> + <string name="frame_gen_title">फ़्रेम जनरेशन</string> + <string name="frame_gen_hint">फ़्रेम जनरेशन मोड चुनें</string> + <string name="frame_gen_off">बंद</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG एडैप्टिव %1$d</string> + <string name="lsfg_needs_steam">Steam पर Lossless Scaling की ज़रूरत है</string> + <string name="lsfg_needs_install">Steam में Lossless Scaling इंस्टॉल करें या Lossless.dll इंपोर्ट करें</string> + <string name="lsfg_import">Lossless.dll इंपोर्ट करें…</string> + <string name="lsfg_using_steam">Steam वाली कॉपी का उपयोग हो रहा है</string> + <string name="lsfg_using_import">इंपोर्ट की गई कॉपी का उपयोग हो रहा है</string> + <string name="lsfg_pick_title">Lossless.dll चुनें</string> + <string name="lsfg_imported">Lossless.dll इंपोर्ट हो गई</string> + <string name="lsfg_import_unchanged">यह Lossless.dll पहले से उपयोग में है</string> + <string name="lsfg_import_not_owned">इस Steam खाते में Lossless Scaling नहीं है</string> + <string name="lsfg_import_invalid">यह फ़ाइल Lossless Scaling की Lossless.dll नहीं है</string> + <string name="lsfg_import_failed">इस Lossless.dll से LSFG तैयार नहीं हो सका</string> + <string name="lsfg_problem_missing">LSFG को Steam से Lossless Scaling या इंपोर्ट की गई Lossless.dll चाहिए</string> + <string name="lsfg_problem_failed">LSFG अपने शेडर Lossless.dll से तैयार नहीं कर सका</string> + <string name="hud_fg_unsupported">%1$s: असमर्थित</string> + <string name="hud_fg_failed">%1$s: इंजन विफल</string> + <string name="hud_fg_needs_lossless">%1$s: को Lossless Scaling चाहिए</string> + <string name="hud_fg_starting">%1$s %2$s · शुरू हो रहा है</string> + <string name="second_library_import_hint">एक Steam लाइब्रेरी जोड़ता है और उसमें Windows गेम खोजता है। जिन गेम के मालिक आप हैं वे अपनी Steam एंट्री का उपयोग करते हैं; बाकी non-Steam शॉर्टकट बन जाते हैं।</string> + <string name="game_env_title">गेम एनवायरनमेंट</string> + <string name="game_env_hint">Proton गेम के लिए एनवायरनमेंट वेरिएबल।</string> + <string name="game_env_edit">संपादित करें</string> + <string name="game_env_shared">सभी Proton गेम</string> + <string name="game_env_scope">प्रोफ़ाइल</string> + <string name="game_env_app_id">Steam ऐप ID</string> + <string name="game_env_open">खोलें</string> + <string name="game_env_profile">गेम %1$s</string> + <string name="game_env_add">वेरिएबल जोड़ें</string> + <string name="game_env_name">वेरिएबल का नाम</string> + <string name="game_env_value">मान</string> + <string name="game_env_save">सेव करें</string> + <string name="game_env_remove">हटाएं</string> + <string name="game_env_restore">फिर से इनहेरिट करें</string> + <string name="game_env_reset">एनवायरनमेंट ओवरराइड हटाएं</string> + <string name="game_env_unset">इस प्रोफ़ाइल के लिए अनसेट करें</string> + <string name="game_env_empty">कोई वेरिएबल सेट नहीं है।</string> + <string name="game_env_inherited">इनहेरिटेड</string> + <string name="game_env_custom">कस्टम वेरिएबल</string> + <string name="game_env_error">लोड या सेव नहीं हो सका। खाली स्टोरेज जांचें और फिर कोशिश करें।</string> + <string name="game_env_invalid">मान्य नाम और 8192 वर्णों तक का मान इस्तेमाल करें।</string> + <string name="game_env_unused">स्टैंडर्ड Proton रनटाइम यह वेरिएबल नहीं पढ़ता।</string> + <string name="game_env_loading">लोड हो रहा है…</string> + <string name="game_env_feature">D3D12 फ़ीचर-लेवल ओवरराइड। 12_2 DirectX 12 Ultimate क्षमताएं दिखाता है; असमर्थित फ़ीचर फेल हो सकते हैं। ऑटोमैटिक पहचान के लिए इसे अनसेट रहने दें।</string> + <string name="game_env_shader">D3D12 शेडर-मॉडल ओवरराइड। 6_9 के लिए पर्याप्त नया VKD3D और मिलते GPU फ़ीचर चाहिए। ऑटोमैटिक पहचान के लिए इसे अनसेट रहने दें।</string> + <string name="game_env_cache">बार-बार कंपाइल होने से बचने के लिए Mesa शेडर कैशिंग चालू रखें। false कैशिंग को चालू करता है।</string> + <string name="game_env_cache_size">Mesa शेडर-कैश का अधिकतम आकार, जैसे 1G। बड़े कैश ज़्यादा स्टोरेज लेते हैं।</string> + <string name="game_env_vkd3d_config">D3D12 के लिए VKD3D विकल्प। nodxr रे ट्रेसिंग बंद करता है; इसका उपयोग सिर्फ़ उन गेम के लिए करें जिन्हें इसकी ज़रूरत हो।</string> + <string name="game_env_proton_log">Proton डायग्नोस्टिक लॉग के लिए 1 सेट करें। लॉगिंग से परफ़ॉर्मेंस घट सकता है और स्टोरेज लगता है।</string> + <string name="game_env_wined3d">D3D9–11 संगतता जांच के लिए DXVK के बजाय OpenGL-आधारित WineD3D इस्तेमाल करने के लिए 1 सेट करें।</string> + <string name="game_env_no_ntsync">इस गेम को ntsync (droiddeck-ntsync या कर्नेल ड्राइवर) से दूर रखने के लिए 1 सेट करें; जहां पैक मौजूद है वहां यह droiddeck-esync इस्तेमाल करता है, वरना droiddeck-fsync।</string> + <string name="game_env_no_esync">इस गेम के लिए droiddeck-esync बंद करने के लिए 1 सेट करें; इसके बदले यह droiddeck-fsync इस्तेमाल करता है, या जहां fsync भी बंद है वहां droiddeck-ntsync।</string> + <string name="game_env_no_fsync">इस गेम को droiddeck-fsync से दूर रखने के लिए 1 सेट करें; जहां पैक मौजूद है वहां यह droiddeck-esync इस्तेमाल करता है, वरना droiddeck-ntsync।</string> + <string name="game_env_fsync_first">इस गेम को droiddeck-fsync पर चलाने के लिए 1 सेट करें, या droiddeck-esync को पहले रखने के लिए 0 — जहां कोई पैक इसके Proton से मेल खाता हो।</string> + <string name="game_env_xalia">इस गेम के लिए Proton के गेमपैड-नेविगेशन हेल्पर को बंद करने के लिए 0 सेट करें।</string> + <string name="game_env_hud">D3D9–11 के लिए DXVK ओवरले। fps फ़्रेम रेट दिखाता है; अनसेट करने पर यह छिप जाता है।</string> + <string name="game_env_dxvk_config">सेमीकोलन से अलग की गई DXVK सेटिंग। नए बिल्ड फ़्रेम सीमित करने के लिए dxvk.maxFrameRate इस्तेमाल करते हैं; सामान्य खेल के लिए सेशन का फ़्रेम लिमिटर इस्तेमाल करना बेहतर है।</string> + <string name="game_env_vkd3d_limit">D3D12 फ़्रेम-रेट सीमा। 0 सीमा को बंद कर देता है।</string> + <string name="game_env_glthread">Mesa OpenGL थ्रेडिंग। true, CPU-सीमित OpenGL गेम में मदद कर सकता है; हर गेम पर जांच लें।</string> + <string name="sync_backend_title">Wine सिंक</string> + <string name="sync_backend_hint">गेम अपने थ्रेड को किस तरह तालमेल में रखते हैं। अगले सेशन से लागू होता है।</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">FEX प्रीसेट</string> + <string name="fex_next_launch">अगली बार गेम शुरू होने पर लागू होता है।</string> + <string name="game_settings_title">गेम</string> + <string name="fex_default_label">FEX डिफ़ॉल्ट</string> + <string name="fex_default_detail">FEX के डिफ़ॉल्ट इस्तेमाल करता है।</string> + <string name="fex_stability_label">स्टेबिलिटी</string> + <string name="fex_stability_detail">सबसे सुरक्षित, सबसे धीमी सेटिंग।</string> + <string name="fex_compatibility_label">कम्पैटिबिलिटी</string> + <string name="fex_compatibility_detail">मल्टीब्लॉक के साथ पूरा TSO।</string> + <string name="fex_intermediate_label">इंटरमीडिएट</string> + <string name="fex_intermediate_detail">घटी हुई x87 प्रिसिज़न के साथ संतुलित TSO।</string> + <string name="fex_performance_label">परफ़ॉर्मेंस</string> + <string name="fex_performance_detail">सभी TSO बंद; मल्टीब्लॉक।</string> + <string name="fex_performance_tso_label">परफ़ॉर्मेंस + TSO</string> + <string name="fex_performance_tso_detail">मुख्य TSO चालू के साथ परफ़ॉर्मेंस सेटिंग।</string> + <string name="fex_extreme_label">एक्सट्रीम</string> + <string name="fex_extreme_detail">सेल्फ़-मॉडिफ़ाइंग-कोड जांच बंद करता है। JIT गेम और DRM काम करना बंद कर सकते हैं।</string> + <string name="fex_extreme_tso_label">एक्सट्रीम + TSO</string> + <string name="fex_extreme_tso_detail">मुख्य TSO चालू के साथ एक्सट्रीम सेटिंग।</string> + <string name="fex_extreme_gn_label">एक्सट्रीम-gn</string> + <string name="fex_extreme_gn_detail">TSC और मेटाडेटा हिंट के साथ परफ़ॉर्मेंस। एक्सट्रीम से पहले इसे आज़माएं।</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">पूरी कोड जांच के साथ परफ़ॉर्मेंस, और हाइपरवाइज़र बिट छिपा हुआ।</string> + <string name="game_env_custom_value">कस्टम मान</string> + <string name="game_env_select_value">मान चुनें</string> + <string name="user_apps_section">एमुलेटर और ऐप</string> + <string name="user_apps_add">जोड़ें</string> + <string name="user_apps_add_detail">आपका अपना ऐप या गेम</string> + <string name="user_apps_kind_script">स्क्रिप्ट</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d%%</string> + <string name="user_apps_starting">शुरू हो रहा है…</string> + <string name="user_apps_added">%1$s जोड़ा गया</string> + <string name="user_apps_removed">%1$s हटाया गया</string> + <string name="user_apps_back">डेस्कटॉप</string> + <string name="user_apps_gone">वह ऐप अब नहीं है। इसे डेस्कटॉप से फिर से चुनें।</string> + <string name="user_apps_open">%1$s खोलें</string> + <string name="user_apps_remove">हटाएं</string> + <string name="user_apps_remove_confirm">हटाने के लिए फिर से दबाएं</string> + <string name="user_apps_stop_session">हटाने के लिए सेशन बंद करें</string> + <string name="user_apps_script_linked">%1$s को उसके फ़ोल्डर से चलाता है, जो सेशन में वहीं से लिंक रहता है। ऐप हटाने पर वह फ़ोल्डर जैसा है वैसा ही रहता है।</string> + <string name="user_apps_script_copied">%1$s को DroidDeck के भीतर उसके फ़ोल्डर की एक कॉपी से चलाता है। ऐप हटाने पर कॉपी मिटती है, असली फ़ोल्डर नहीं।</string> + <string name="user_apps_runtime_required">पहले Linux रनटाइम इंस्टॉल करें</string> + <string name="user_apps_file_gone">फ़ाइल अब नहीं है</string> + <string name="user_apps_no_space">पर्याप्त खाली जगह नहीं है: फ़ोल्डर कॉपी करने के लिए %1$s चाहिए</string> + <string name="user_apps_copying">%1$s कॉपी हो रहा है</string> + <string name="user_apps_failed">ऐप जोड़ना विफल रहा</string> + <string name="user_apps_github_checking">%1$s का रिलीज़ खोजा जा रहा है</string> + <string name="user_apps_github_unreachable">%1$s के रिलीज़ नहीं पढ़े जा सके</string> + <string name="user_apps_github_no_appimage">%1$s के किसी भी रिलीज़ में ARM64 या x86_64 AppImage नहीं है</string> + <string name="user_apps_downloading">%1$s डाउनलोड हो रहा है</string> + <string name="user_apps_download_failed">डाउनलोड विफल रहा</string> + <string name="user_apps_update_failed">ऐप अपडेट करना विफल रहा</string> + <string name="user_apps_icon_failed">वह आइकन पढ़ा नहीं जा सका</string> + <string name="user_apps_saved">%1$s सेव हुआ</string> + <string name="user_apps_edit">संपादित करें</string> + <string name="user_apps_check_updates">अपडेट देखें</string> + <string name="user_apps_checking_updates">अपडेट देखे जा रहे हैं…</string> + <string name="user_apps_update_to">%1$s में अपडेट करें</string> + <string name="user_apps_up_to_date">नवीनतम · %1$s</string> + <string name="user_apps_updated">%1$s अपडेट हुआ</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">ऐप जोड़ें</string> + <string name="add_app_tab_script">स्क्रिप्ट</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">एक .sh फ़ाइल। यह अपने फ़ोल्डर से gamescope के अंदर फ़ुल स्क्रीन में चलती है।</string> + <string name="add_app_appimage_hint">एक ARM64 (aarch64) AppImage, या एक x86_64 वाली जो FEX के ज़रिए चलती है। यह Linux रनटाइम में एक बार अनपैक होती है।</string> + <string name="add_app_github_hint">रिपॉज़िटरी के सबसे नए रिलीज़ से ARM64 AppImage इंस्टॉल करता है, या FEX के ज़रिए इसकी x86_64 वाली।</string> + <string name="app_fex_title">FEX इस्तेमाल करें</string> + <string name="app_fex_auto">ऑटो</string> + <string name="app_fex_on">चालू</string> + <string name="app_fex_off">बंद</string> + <string name="app_fex_hint">FEX इस ARM64 डिवाइस पर x86 Linux प्रोग्राम चलाता है। प्रोग्राम x86 के लिए बना होने पर ऑटो इसे इस्तेमाल करता है।</string> + <string name="app_fex_x86">FEX के ज़रिए %1$s</string> + <string name="app_fex_not_ready">यह प्रोग्राम %1$s पीसी के लिए बना है और FEX के ज़रिए चलता है, जो अभी सेट नहीं है। साइन इन रहते हुए Steam एक बार खोलें: DroidDeck इससे FEX और ज़रूरी Steam Linux Runtime डाउनलोड करवा लेगा, फिर यह खुल जाएगा।</string> + <string name="session_end_program">यह प्रोग्राम</string> + <string name="session_end_fex_missing">%1$s x86 पीसी के लिए बना है और FEX के ज़रिए चलता है, जो अभी इंस्टॉल नहीं है। साइन इन रहते हुए Steam एक बार शुरू करें और इसे FEX और Steam Linux Runtime डाउनलोड करने दें, फिर इसे फिर से शुरू करें।</string> + <string name="add_app_flatpak_hint">एक Flathub ऐप ID या उसका Flathub पेज। अगर Flatpak अभी सेट नहीं है, तो पहले वह सेट होगा।</string> + <string name="add_app_file">फ़ाइल</string> + <string name="add_app_no_file">कोई फ़ाइल नहीं चुनी गई</string> + <string name="add_app_choose_file">फ़ाइल चुनें</string> + <string name="add_app_change_file">बदलें</string> + <string name="add_app_pick_script">स्क्रिप्ट चुनें</string> + <string name="add_app_pick_appimage">AppImage चुनें</string> + <string name="add_app_pick_icon">आइकन चुनें</string> + <string name="add_app_repository">रिपॉज़िटरी</string> + <string name="add_app_repository_placeholder">owner/repo या github.com का लिंक</string> + <string name="add_app_repository_invalid">यह GitHub रिपॉज़िटरी नहीं है</string> + <string name="add_app_flatpak_id">ऐप ID</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">यह Flathub ऐप ID नहीं है</string> + <string name="add_app_flatpak_bundle_hint">या एक .flatpak फ़ाइल, उस ऐप के लिए जो Flathub पर नहीं है।</string> + <string name="add_app_pick_flatpak">.flatpak फ़ाइल चुनें</string> + <string name="add_app_source">सोर्स</string> + <string name="add_app_appearance">आपके डेस्कटॉप पर</string> + <string name="add_app_preview_name">नया ऐप</string> + <string name="add_app_name">नाम</string> + <string name="add_app_name_placeholder">ऐप से</string> + <string name="add_app_icon">आइकन</string> + <string name="add_app_icon_default">डिफ़ॉल्ट आइकन</string> + <string name="add_app_choose_icon">आइकन चुनें</string> + <string name="add_app_clear_icon">हटाएं</string> + <string name="add_app_icon_suggestions">रिपॉज़िटरी से</string> + <string name="add_app_icon_searching">आइकन खोजे जा रहे हैं…</string> + <string name="add_app_icon_reset">डिफ़ॉल्ट</string> + <string name="edit_app_title">%1$s संपादित करें</string> + <string name="edit_app_save">सेव करें</string> + <string name="add_app_checking_folder">फ़ोल्डर जांचा जा रहा है…</string> + <string name="add_app_folder_linked">इसका फ़ोल्डर सेशन में लिंक है: %1$s</string> + <string name="add_app_folder_copied">इसके फ़ोल्डर में Linux प्रोग्राम हैं, जिन्हें शेयर्ड स्टोरेज चला नहीं सकता, इसलिए इसे DroidDeck में कॉपी किया जाता है (%1$s)।</string> + <string name="add_app_confirm">जोड़ें</string> + <string name="add_app_cancel">रद्द करें</string> + <string name="add_app_runtime_required">पहले Setup से Linux रनटाइम इंस्टॉल करें।</string> + <string name="add_app_busy">%1$s के पूरा होने का इंतज़ार करें।</string> + <string name="steam_compat_label">कम्पैटिबल</string> + <string name="proton_compatible_note">Steam के Compatibility मेनू में “%1$s” के निशान वाले बिल्ड में DroidDeck के प्रीफ़िक्स फ़िक्स होते हैं और यहां गेम शुरू कर पाते हैं। डिफ़ॉल्ट है Proton Experimental ARM64 (%1$s), या नया बिल्ड इंस्टॉल होने पर Proton 11.0 ARM64 (%1$s)। बिना निशान वाली Valve की खुद की Proton एंट्री इस डिवाइस पर गेम शुरू नहीं कर सकती।</string> + <string name="proton_installed_compatible">इंस्टॉल है %1$s · %2$s</string> + <string name="common_cancel">रद्द करें</string> + <string name="common_auto">ऑटो</string> + <string name="common_never">कभी नहीं</string> + <string name="common_always">हमेशा</string> + <string name="common_uninstall">अनइंस्टॉल करें</string> + <string name="common_check">जांचें</string> + <string name="common_update">अपडेट करें</string> + <string name="common_checking">जांचा जा रहा है…</string> + <string name="common_working">काम हो रहा है…</string> + <string name="common_forget">भूलें</string> + <string name="common_add_ellipsis">जोड़ें…</string> + <string name="common_use">इस्तेमाल करें</string> + <string name="common_applies_next_session">अगले सेशन से लागू होता है।</string> + <string name="mode_steam_title">Steam सेशन</string> + <string name="mode_desktop_title">डेस्कटॉप सेशन</string> + <string name="mode_display">डिस्प्ले</string> + <string name="mode_resolution">रिज़ॉल्यूशन</string> + <string name="mode_res_720">720p तक</string> + <string name="mode_res_900">900p तक</string> + <string name="mode_res_1080">1080p तक</string> + <string name="mode_res_native">नेटिव</string> + <string name="mode_res_default">%1$s (डिफ़ॉल्ट)</string> + <string name="mode_res_custom_value">कस्टम · %1$d×%2$d</string> + <string name="mode_res_custom">कस्टम…</string> + <string name="mode_res_note">720p मेनू को ज़्यादा रिस्पॉन्सिव बना सकता है।</string> + <string name="mode_ratio">स्क्रीन रेशियो</string> + <string name="mode_ratio_custom">कस्टम रिज़ॉल्यूशन से तय होता है।</string> + <string name="mode_ratio_auto">ऑटो कम से कम 16:9 इस्तेमाल करता है।</string> + <string name="mode_fps">फ़्रेम सीमा</string> + <string name="mode_fps_note">पूरे सेशन को सीमित करता है। डिस्प्ले उस रिफ्रेश रेट पर चला जाता है जिसे यह सीमा बराबर बांटती है।</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">HDR10 आउटपुट</string> + <string name="mode_hdr_unavailable">उपलब्ध नहीं: %1$s।</string> + <string name="mode_hdr_restart">लागू करने के लिए ऐप रीस्टार्ट करें।</string> + <string name="mode_drivers">ड्राइवर</string> + <string name="mode_gpu_drivers">GPU ड्राइवर</string> + <string name="mode_gpu_drivers_hint">Steam और डेस्कटॉप दोनों में साझा। Components पेज पर सेट करें।</string> + <string name="mode_touch_controls">टच और कंट्रोल</string> + <string name="mode_touch">टच</string> + <string name="mode_touch_touchpad">टचपैड</string> + <string name="mode_touch_direct">डायरेक्ट</string> + <string name="mode_touch_note">ऑटो: डेस्कटॉप पर टचपैड, Steam में डायरेक्ट।</string> + <string name="mode_osc">ऑन-स्क्रीन कंट्रोल</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">कोई कंट्रोलर कनेक्ट न होने पर ऑटो कंट्रोल दिखाता है।</string> + <string name="osc_steam_touch">Steam टच (बीटा)</string> + <string name="osc_style_next_session">Steam के टच कंट्रोल अगले Steam सेशन से शुरू होंगे</string> + <string name="osc_steam_layout">Steam टच लेआउट</string> + <string name="osc_steam_layout_hint">इस गेम के टच कंट्रोल को घुमाएं, आकार बदलें या छिपाएं; Steam में सेव होता है</string> + <string name="mode_controller">कंट्रोलर</string> + <string name="mode_controller_hint">Steam आपके कंट्रोलर को किस तरह पहचानता है। अगले सेशन से लागू होता है।</string> + <string name="mode_controller_deck">Steam Deck कंट्रोलर</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Xbox 360 कंट्रोलर</string> + <string name="mode_controller_note">Deck: Quick Access बटन और gyro। Steam Controller: वही, साथ में पीछे के बटन पर ग्रिप सेंस। DualShock 4 और DualSense: gyro, और ट्रैकपैड उनके टचपैड के तौर पर। Xbox 360: Guide+A से Quick Access खोलें।</string> + <string name="mode_back">बैक</string> + <string name="back_menu_then_qam">मेनू → QAM</string> + <string name="back_qam_then_menu">QAM → मेनू</string> + <string name="mode_session">सेशन</string> + <string name="mode_suspend">बैकग्राउंड व्यवहार</string> + <string name="mode_suspend_hint">ऐप छिपने या स्क्रीन बंद होने पर।</string> + <string name="mode_suspend_manual">मैनुअल</string> + <string name="mode_suspend_native">नेटिव</string> + <string name="mode_suspend_steam_note">ऑटो दिखने पर रुकता है और फिर से शुरू होता है। नेटिव पहले Steam को स्लीप के लिए तैयार करता है। मैनुअल फिर से शुरू करें का इंतज़ार करता है। कभी नहीं वाला चलता रहता है।</string> + <string name="mode_background_downloads">डाउनलोड चलते रहने दें</string> + <string name="mode_background_downloads_hint">Steam के सक्रिय रूप से डाउनलोड करते समय बैकग्राउंड सस्पेंशन को टालें।</string> + <string name="mode_suspend_note">ऑटो बैकग्राउंड में रुकता है और दिखने पर फिर से शुरू होता है। मैनुअल फिर से शुरू करें का इंतज़ार करता है। कभी नहीं वाला चलता रहता है।</string> + <string name="mode_startup">स्टार्टअप</string> + <string name="mode_steam_startup">DroidDeck के साथ Steam शुरू करें</string> + <string name="mode_steam_startup_hint">DroidDeck लॉन्च होने पर Steam सेशन खोलता है।</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">रिलीज़ देखी जा रही हैं…</string> + <string name="mode_decky_ready">%1$s इंस्टॉल करने के लिए तैयार।</string> + <string name="mode_decky_none">कोई कम्पैटिबल बिल्ड नहीं मिला। बाद में फिर कोशिश करें।</string> + <string name="mode_decky_installed">इंस्टॉल है · %1$s</string> + <string name="mode_decky_update">अपडेट उपलब्ध · %1$s</string> + <string name="mode_decky_current">नवीनतम · %1$s</string> + <string name="mode_decky_loader">लोडर</string> + <string name="mode_decky_install_latest">नवीनतम इंस्टॉल करें</string> + <string name="mode_decky_on">Steam के साथ शुरू होता है। चालू रहते हुए, इस डिवाइस के दूसरे ऐप Steam को नियंत्रित कर सकते हैं।</string> + <string name="mode_decky_off">बंद। Steam का रिमोट डिबगिंग पोर्ट बंद रहता है।</string> + <string name="mode_decky_remove_title">Decky Loader अनइंस्टॉल करें?</string> + <string name="mode_decky_remove_text">लोडर हटाता है। आपके प्लगइन और सेटिंग बनी रहती हैं।</string> + <string name="mode_client">क्लाइंट</string> + <string name="mode_mangoapp">परफ़ॉर्मेंस ओवरले</string> + <string name="mode_mangoapp_hint">Deck मोड में गेम क्रैश होने या काली स्क्रीन होने पर इसे बंद करें। अगले सेशन से लागू होता है।</string> + <string name="mode_steam_repair">Steam क्लाइंट रिपेयर करें</string> + <string name="mode_steam_repair_hint">अगली बार Steam शुरू होने पर Steam क्लाइंट फिर से डाउनलोड होता है। इसे इस्तेमाल करें जब Steam अपनी लोडिंग स्क्रीन पर अटका रह जाए। गेम, साइन-इन और सेटिंग बनी रहती हैं।</string> + <string name="mode_steam_repair_queued">अगली बार Steam शुरू होने पर Steam क्लाइंट फिर से डाउनलोड होगा।</string> + <string name="mode_steam_repair_button">रिपेयर करें</string> + <string name="mode_added_art">आर्टवर्क</string> + <string name="mode_sgdb_key">SteamGridDB API कुंजी</string> + <string name="mode_sgdb_key_yours">आपकी कुंजी</string> + <string name="mode_sgdb_edit">संपादित करें…</string> + <string name="mode_added_art_hint">पहले Steam से, फिर SteamGridDB से छूटी हुई आर्टवर्क भरता है। गेम फ़ोल्डर में आपकी अपनी cover.jpg, hero.jpg, logo.png या icon.png को प्राथमिकता मिलती है।</string> + <string name="summary_remove_game">%1$s हटाएं</string> + <string name="mode_added_remove_confirm">हटाएं?</string> + <string name="games_add">गेम जोड़ें</string> + <string name="games_add_title">गेम जोड़ें:</string> + <string name="games_pick_exe_title">exe चुनें:</string> + <string name="games_add_unreachable">यह जगह सेशन में उपलब्ध नहीं है</string> + <string name="mode_added_exe_check">इस exe को जांचें</string> + <string name="wincomp_title">Windows कॉम्पोनेंट</string> + <string name="wincomp_pad_hint">LB / RB से सेक्शन के बीच जाएं।</string> + <string name="wincomp_card_none">कोई नहीं</string> + <string name="wincomp_applies">कॉम्पोनेंट लिस्ट से वे Windows DLL जो Proton में शामिल नहीं हैं। जिन्हें इस गेम को ज़रूरत है उन्हें चालू करें: वे गेम के अपने Windows फ़ोल्डर में जाते हैं, उसकी फ़ाइलों में कभी नहीं, और जो भी Proton यह चलता है उसमें बने रहते हैं।</string> + <string name="wincomp_offline">कॉम्पोनेंट लिस्ट तक नहीं पहुंच सका। पहले से डाउनलोड किए कॉम्पोनेंट अब भी बदले जा सकते हैं।</string> + <string name="wincomp_recommended">इस गेम के लिए सुझाए गए</string> + <string name="wincomp_checking">यह गेम किसकी ज़रूरत है देखा जा रहा है…</string> + <string name="wincomp_when_online">ऑनलाइन होने पर उपलब्ध</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">%1$d कॉम्पोनेंट चालू किया जा रहा है…</item> + <item quantity="other">%1$d कॉम्पोनेंट चालू किए जा रहे हैं…</item> + </plurals> + <string name="wincomp_auto_section">इस गेम के लिए चालू</string> + <string name="wincomp_auto_steam">Steam %2$s (ऐप %3$s) के साथ %1$s इंस्टॉल करता है</string> + <string name="wincomp_auto_gog">इस गेम के लिए GOG की लिस्ट से</string> + <string name="wincomp_auto_epic">इस गेम के लिए Epic की ज़रूरी शर्तों से</string> + <string name="wincomp_auto_amazon">इस गेम के लिए Amazon के इंस्टॉल स्टेप से</string> + <string name="wincomp_steam_by_name">Steam नाम से मिलान करके ऐप %2$s के साथ %1$s इंस्टॉल करता है</string> + <string name="wincomp_suggestions">ये भी सूचीबद्ध</string> + <string name="wincomp_suggestion_reason">%1$s द्वारा सूचीबद्ध; यहां कॉम्पोनेंट नहीं है</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">गेम %1$s के साथ आता है</string> + <string name="wincomp_found_shipped">गेम में %1$s है (अक्सर ऐसे ही काम करता है)</string> + <string name="wincomp_already_here">इस गेम में पहले से मौजूद</string> + <string name="wincomp_no_recommendation">गेम के फ़ोल्डर में किसी कॉम्पोनेंट का कोई संकेत नहीं है।</string> + <string name="wincomp_active">इस गेम के लिए इंस्टॉल (%1$d)</string> + <string name="wincomp_all">सभी कॉम्पोनेंट (%1$d)</string> + <string name="wincomp_installers">Windows इंस्टॉलर की ज़रूरत (%1$d)</string> + <string name="wincomp_installers_note">ये अपने खुद के सेटअप प्रोग्राम हैं, जिनके भीतर खोलने लायक कोई पैकेज नहीं है, और अभी तक यह रिकॉर्ड नहीं है कि वे क्या इंस्टॉल करते हैं। ज़्यादातर पुराने रनटाइम हैं जिनकी जगह Proton के भीतर का Mono पहले से ले लेता है; रिकॉर्ड बनते ही जोड़े जाते हैं।</string> + <string name="wincomp_show">दिखाएं</string> + <string name="wincomp_hide">छिपाएं</string> + <string name="wincomp_needs_installer">अपना सेटअप प्रोग्राम चलाता है (अभी रिकॉर्ड नहीं)</string> + <string name="wincomp_unsupported">अभी यहां समर्थित नहीं</string> + <string name="wincomp_loading">कॉम्पोनेंट लोड हो रहे हैं…</string> + <string name="wincomp_failed">इंस्टॉल नहीं हो सका: %1$s</string> + <string name="wincomp_downloaded">डाउनलोड हो गया</string> + <string name="wincomp_downloading">%1$s डाउनलोड हो रहा है</string> + <string name="wincomp_installing">इंस्टॉल हो रहा है: %1$s</string> + <string name="wincomp_reverting">बंद किया जा रहा है…</string> + <string name="wincomp_download_size">%1$s डाउनलोड</string> + <string name="wincomp_next_launch">अगली बार गेम शुरू होने पर लागू होगा।</string> + <string name="mode_stretch169">16:9 गेम को स्ट्रेच करें</string> + <string name="mode_stretch169_hint">16:9 से ज़्यादा चौकोर स्क्रीन पर, बार दिखाने के बजाय गेम को भरने के लिए स्ट्रेच करता है। अगले सेशन से लागू होता है।</string> + <string name="mode_audio">ऑडियो</string> + <string name="mode_directaudio">गेम के लिए DirectAudio</string> + <string name="mode_directaudio_hint">कम लेटेंसी के लिए PulseAudio को बायपास करता है।</string> + <string name="mode_mic">माइक्रोफ़ोन</string> + <string name="mode_mic_hint">वॉइस चैट के लिए।</string> + <string name="mode_network">नेटवर्क</string> + <string name="mode_wifi_discovery">Wi-Fi खोज</string> + <string name="mode_wifi_discovery_hint">Steam में Wi-Fi नाम और आस-पास के नेटवर्क दिखाएं।</string> + <string name="mode_wifi_location_off">लोकेशन सेवाएं बंद हैं।</string> + <string name="mode_wifi_permission_denied">लोकेशन अनुमति नहीं दी गई।</string> + <string name="mode_wifi_open_settings">सेटिंग खोलें</string> + <string name="mode_wifi_continue">जारी रखें</string> + <string name="mode_wifi_explain_title">Wi-Fi खोज चालू करें?</string> + <string name="mode_wifi_explain_text">Wi-Fi नाम पढ़ने और आस-पास के नेटवर्क खोजने के लिए Android को लोकेशन अनुमति चाहिए। DroidDeck इस एक्सेस का उपयोग Steam में वे नेटवर्क दिखाने के लिए करता है। हम आपकी लोकेशन ट्रैक नहीं करते। इंटरनेट एक्सेस इस अनुमति के बिना भी काम करता है।</string> + <string name="mode_storage">गेम स्टोरेज</string> + <string name="mode_storage_auto">ऑटोमैटिक - मौजूद होने पर SD कार्ड, और आपके Games फ़ोल्डर</string> + <string name="mode_storage_auto_none">ऑटोमैटिक - आपके Games फ़ोल्डर, और मौजूद होने पर SD कार्ड (अभी कोई नहीं)</string> + <string name="mode_storage_internal">केवल इंटरनल</string> + <string name="mode_storage_folder">फ़ोल्डर: %1$s</string> + <string name="mode_second_library">दूसरी लाइब्रेरी</string> + <string name="mode_storage_note">SD या शेयर्ड स्टोरेज से एसेट स्ट्रीम करने वाले गेम अटक सकते हैं।</string> + <string name="mode_storage_diagnostics">स्टोरेज डायग्नोस्टिक्स</string> + <string name="mode_storage_diagnostics_hint">Share logs में एलोकेशन नतीजे और सैंपल किए स्टोरेज टाइमिंग रिकॉर्ड करें। चालू रहते हुए इंस्टॉल धीमे हो सकते हैं। अगले सेशन से लागू होता है।</string> + <string name="mode_choose_folder">फ़ोल्डर चुनें…</string> + <string name="mode_custom_res_title">कस्टम रिज़ॉल्यूशन</string> + <string name="mode_custom_res_text">सीमा और रेशियो को बदल देता है। स्क्रीन से न मिलने वाले आकार में बार दिखेंगे।</string> + <string name="mode_width">चौड़ाई</string> + <string name="mode_height">ऊंचाई</string> + <string name="mode_custom_res_range">320×240 और 3840×2160 के बीच।</string> + <plurals name="mode_added_count"> + <item quantity="one">%1$d गेम</item> + <item quantity="other">%1$d गेम</item> + </plurals> + <string name="common_ok">ठीक है</string> + <string name="common_delete">हटाएं</string> + <string name="common_restore">वापस लाएं</string> + <string name="common_download">डाउनलोड करें</string> + <string name="common_delete_named">%1$s हटाएं</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">रनटाइम ड्राइवर</string> + <string name="comp_runtime_driver_hint">Steam, गेम और डेस्कटॉप में उपयोग होता है। अगले सेशन से लागू होता है।</string> + <string name="comp_import_turnip">Turnip zip इंपोर्ट करें…</string> + <string name="comp_display_driver">डिस्प्ले ड्राइवर</string> + <string name="comp_display_driver_hint">दोनों मोड में स्क्रीन पर फ़्रेम बनाता है। लागू करने के लिए ऐप रीस्टार्ट करें।</string> + <string name="comp_import_adrenotools">AdrenoTools zip इंपोर्ट करें…</string> + <string name="comp_never">कभी नहीं</string> + <string name="comp_nightlies_checked">Nightlies की जांच %1$s को हुई</string> + <string name="comp_title">कॉम्पोनेंट</string> + <string name="comp_about">कॉम्पोनेंट के बारे में</string> + <string name="comp_check_drivers">नए ड्राइवर देखें</string> + <string name="comp_check_nightlies">नए पैकेज के लिए Nightlies देखें</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">यहां चुना गया Proton उन सभी गेम को चलाता है जिन्होंने Steam में अपना अलग Proton नहीं चुना है, और Steam में किया गया बदलाव यहां भी दिखता है। Steam अपडेट होने पर Valve का Proton बदल देता है; हर बिल्ड के ओरिजिनल सुरक्षित रहते हैं।</string> + <string name="comp_scope">कॉन्फ़िगरेशन</string> + <string name="comp_profile_configure">प्रोफ़ाइल कॉन्फ़िगर करें</string> + <string name="comp_profile">प्रोफ़ाइल</string> + <string name="comp_scope_general">डिफ़ॉल्ट प्रोफ़ाइल</string> + <string name="comp_scope_general_detail">बिना ओवरराइड वाले गेम इसे खुद इस्तेमाल करते हैं।</string> + <string name="comp_profile_game_detail">%1$s शुरू होने पर सेव की गई सेटिंग खुद इस्तेमाल होती हैं।</string> + <string name="comp_game_hint">सिर्फ़ वे सेटिंग चुनें जिनकी इस गेम को ज़रूरत है। बाकी सब डिफ़ॉल्ट प्रोफ़ाइल का इस्तेमाल करते रहते हैं।</string> + <string name="comp_game_fex">FEX प्रीसेट</string> + <string name="comp_game_fex_inherited">डिफ़ॉल्ट प्रोफ़ाइल का प्रीसेट इस्तेमाल हो रहा है।</string> + <string name="comp_game_fex_override">इस गेम के लिए कस्टम प्रीसेट सेव किया गया है।</string> + <string name="comp_game_fex_custom">कस्टम FEX वेरिएबल</string> + <string name="comp_game_use_general">डिफ़ॉल्ट इस्तेमाल करें</string> + <string name="comp_game_compatibility">गेम कम्पैटिबिलिटी</string> + <string name="comp_game_environment_hint">इस गेम के लिए Proton, DXVK, VKD3D और सिंक्रोनाइज़ेशन एनवायरनमेंट विकल्प।</string> + <string name="comp_game_proton_inherited">डिफ़ॉल्ट प्रोफ़ाइल का Proton इस्तेमाल हो रहा है।</string> + <string name="comp_game_proton_override">Steam कम्पैटिबिलिटी मैपिंग के ज़रिए इस गेम के लिए कस्टम Proton सेव किया गया है।</string> + <string name="comp_no_proton_short">कोई Proton नहीं</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">अगर यह गेम कोई और पैकेज न चुने, तो %1$s से डिफ़ॉल्ट प्रोफ़ाइल का पैकेज इस्तेमाल होता है।</string> + <string name="comp_game_component_general">डिफ़ॉल्ट प्रोफ़ाइल का कॉम्पोनेंट पैकेज इस्तेमाल होता है।</string> + <string name="comp_game_component_override">सिर्फ़ इस गेम के लिए %1$s इस्तेमाल होता है।</string> + <string name="comp_game_runtime_options">रनटाइम विकल्प</string> + <string name="comp_game_environment_other_hint">हर गेम के लिए अन्य Proton, DXVK, VKD3D-Proton और सिंक्रोनाइज़ेशन एनवायरनमेंट विकल्प।</string> + <string name="comp_game_saved_next">%1$s की प्रोफ़ाइल अगली बार गेम शुरू होने के लिए सेव है।</string> + <string name="game_no_steam_title">Steam के बिना शुरू करें</string> + <string name="game_no_steam_detail">ऐसे गेम के लिए जो तुरंत बंद हो जाता है: इसे Proton के Steam सेटअप के बिना शुरू करता है। इसके लिए ओवरले जैसी Steam सुविधाएं बंद रहती हैं।</string> + <string name="game_no_steam_on">चालू</string> + <string name="comp_game_proton_live">%1$s का Proton चुनाव सेव है और अभी Steam में उपलब्ध है।</string> + <string name="comp_game_proton_next">%1$s का Proton चुनाव सेव है। गेम शुरू करने से पहले Steam रीस्टार्ट करें।</string> + <string name="comp_game_proton_not_runnable">%1$s इस डिवाइस पर चुना गया Proton इस्तेमाल नहीं कर सकता।</string> + <string name="comp_game_load_failed">%1$s की प्रोफ़ाइल लोड नहीं हो सकी: %2$s</string> + <string name="comp_game_save_failed">गेम प्रोफ़ाइल सेव नहीं हो सकी: %1$s</string> + <string name="comp_game_reset">गेम प्रोफ़ाइल रीसेट करें</string> + <string name="comp_game_reset_title">%1$s की प्रोफ़ाइल रीसेट करें?</string> + <string name="comp_game_reset_body">इस गेम का Proton, FEX, कॉम्पोनेंट पैकेज, एनवायरनमेंट, टेक्सचर फ़िल्टरिंग, और Windows कॉम्पोनेंट ओवरराइड हटाएं? यह डिफ़ॉल्ट प्रोफ़ाइल इस्तेमाल करेगा।</string> + <string name="comp_game_reset_done">%1$s अब डिफ़ॉल्ट प्रोफ़ाइल इस्तेमाल करता है।</string> + <string name="comp_game_reset_failed">गेम प्रोफ़ाइल पूरी तरह रीसेट नहीं हो सकी: %1$s</string> + <string name="comp_default_live">%1$s अब उन सभी गेम को चलाता है जिन्होंने Steam में अपना Proton नहीं चुना है।</string> + <string name="comp_default_next">अगली बार Steam शुरू होने से, %1$s उन सभी गेम को चलाएगा जिन्होंने अपना Proton नहीं चुना है।</string> + <string name="comp_default_not_runnable">%1$s यहां गेम शुरू नहीं कर सकता, इसलिए यह डिफ़ॉल्ट नहीं हो सकता।</string> + <string name="comp_default_failed">डिफ़ॉल्ट Proton सेव नहीं हो सका: %1$s</string> + <string name="comp_swapped">%1$s · %2$d बदला गया</string> + <string name="comp_all_original">%1$s · सभी ओरिजिनल</string> + <string name="comp_game_running_suffix">%1$s · गेम चल रहा है</string> + <string name="comp_esync_pack">%1$s · droiddeck-esync पैक</string> + <string name="comp_esync_wanted">%1$s · अभी droiddeck-esync पैक नहीं है</string> + <string name="comp_esync_builtin">%1$s · esync पहले से मौजूद, पैक की ज़रूरत नहीं</string> + <string name="comp_gpu_drivers">GPU ड्राइवर</string> + <string name="comp_reapplied">%1$s · लॉन्च %2$s पर फिर से लागू हुआ</string> + <string name="comp_next_queued">आगे: %1$s · गेम बंद होने के बाद</string> + <string name="comp_in_use">उपयोग में %1$s: %2$s</string> + <string name="comp_waits">गेम बंद होने तक इंतज़ार करता है</string> + <string name="comp_applies_next_game">अगली बार गेम शुरू होने पर लागू होता है</string> + <string name="comp_import_zip">.zip इंपोर्ट करें</string> + <string name="comp_reading">Proton पढ़े जा रहे हैं…</string> + <string name="comp_no_proton">अभी कोई Proton इंस्टॉल नहीं है। इसका ARM64 Proton डाउनलोड करने के लिए Steam एक बार शुरू करें, या Setup से GE-Proton, CachyOS या DroidDeck-Proton जोड़ें।</string> + <string name="comp_original">ओरिजिनल · %1$s</string> + <string name="comp_this_build">%1$s · यह बिल्ड</string> + <string name="comp_earlier_build">%1$s · पहले का बिल्ड</string> + <string name="comp_installed_count">इंस्टॉल है · %1$d</string> + <string name="comp_nothing_stored">%1$s के लिए अभी कुछ सेव नहीं है। इस Proton की अपनी फ़ाइलें इसके ओरिजिनल के रूप में सेव हैं।</string> + <string name="comp_waits_long">इस Proton पर एक गेम चल रहा है, इसलिए यह बंद होने तक इंतज़ार करता है।</string> + <string name="comp_applies_long">अगली बार गेम शुरू होने पर लागू होता है।</string> + <string name="comp_swap_title">%1$s बदलें?</string> + <string name="comp_swap_body">%1$s को %2$s में डालें? इसकी ओरिजिनल फ़ाइलें सेव रहती हैं। %3$s</string> + <string name="comp_restore_title">%1$s वापस लाएं?</string> + <string name="comp_restore_body">%1$s का ओरिजिनल %2$s में वापस डालें? %3$s</string> + <string name="comp_delete_package_body">ऐप से पैकेज हटाता है। आप इसे फिर से डाउनलोड कर सकते हैं।</string> + <string name="comp_delete_original_title">यह ओरिजिनल हटाएं?</string> + <string name="comp_delete_original_body">यह %1$s ओरिजिनल %2$s के पहले वाले बिल्ड से है। मौजूदा बिल्ड का ओरिजिनल हमेशा रखा जाता है।</string> + <string name="comp_nightlies_count">Nightlies पर · %1$d</string> + <string name="comp_refresh_to_list">Nightlies के पैकेज सूचीबद्ध करने के लिए रीफ़्रेश करें।</string> + <string name="comp_nothing_new">पिछली जांच के बाद कुछ नया नहीं है।</string> + <string name="comp_import_wcp">.wcp इंपोर्ट करें</string> + <string name="comp_restore_original">इस Proton का ओरिजिनल वापस लाएं</string> + <string name="comp_restore_original_body">%1$s का ओरिजिनल %2$s वापस डालें?</string> + <string name="comp_swap">बदलें</string> + <string name="comp_about_text">हर उस Proton के लिए FEX, DXVK और VKD3D-Proton जिससे Steam गेम चलाता है।\n\nकिसी इंस्टॉल की गई पंक्ति को चुनकर उसे बदलें। यह अगली बार गेम शुरू होने पर, या चल रहा गेम बंद होने के बाद लागू होता है।\n\nआपके चुनाव हर लॉन्च से पहले फिर से जांचे जाते हैं और कुछ बदल गया हो तो वापस रखे जाते हैं। हर Proton की ओरिजिनल फ़ाइलें रखी जाती हैं, इसलिए Steam अपडेट से वे कभी नहीं खोती।</string> + <string name="comp_tag_original">ओरिजिनल</string> + <string name="comp_tag_in_use">उपयोग में</string> + <string name="comp_tag_new">नया</string> + <string name="comp_tag_stored">सेव है</string> + <string name="perf_title">परफ़ॉर्मेंस</string> + <string name="perf_lede">CPU कोर, सेशन फ़िक्स और होस्ट नाम। अगले सेशन से लागू होता है।</string> + <string name="perf_client_cores_group">Steam क्लाइंट कोर</string> + <string name="perf_override">Steam के कोर चुनाव को ओवरराइड करें</string> + <string name="perf_override_hint">Steam, उसके UI हेल्पर और gamescope को नीचे दिए कोर पर पिन करता है।</string> + <string name="perf_client_cores">क्लाइंट कोर</string> + <string name="perf_client_cores_on">Steam के लिए कोर।</string> + <string name="perf_client_cores_off">चुनने के लिए ओवरराइड चालू करें।</string> + <string name="perf_game_cores_group">गेम कोर</string> + <string name="perf_game_cores">गेम कोर</string> + <string name="perf_game_cores_hint">गेम के लिए कोर। सभी चुने होने पर, Android खुद शेड्यूल करता है।</string> + <string name="perf_client_ui">क्लाइंट इंटरफ़ेस</string> + <string name="perf_glthread">थ्रेडेड GL</string> + <string name="perf_glthread_hint">Steam मेनू को ज़्यादा रिस्पॉन्सिव बना सकता है।</string> + <string name="perf_zink">Zink: लेज़ी डिस्क्रिप्टर</string> + <string name="perf_zink_hint">डिस्क्रिप्टर बफ़र न रखने वाले ड्राइवर के लिए सुझाया गया।</string> + <string name="perf_noglerror">GL एरर जांच छोड़ें</string> + <string name="perf_noglerror_hint">हर कॉल पर होने वाली GL वैलिडेशन बंद करता है।</string> + <string name="perf_gsrealtime">gamescope: रीयलटाइम GPU क्यू</string> + <string name="perf_gsrealtime_hint">कम्पोज़िटर के GPU काम को प्राथमिकता देता है। गेम की कुछ कीमत पर फ़्रेम पेसिंग को स्मूथ कर सकता है।</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">GPU को उसकी टॉप क्लॉक पर बनाए रखें</string> + <string name="perf_gpuclock_hint">सिर्फ़ Adreno के लिए। कम हिचकोले, लेकिन ज़्यादा बैटरी खर्च और गर्मी।</string> + <string name="perf_fixes">सेशन फ़िक्स</string> + <string name="perf_sysmem">Turnip: sysmem रेंडरिंग</string> + <string name="perf_sysmem_hint">Steam और ज़्यादातर गेम के लिए तेज़। Adreno 710/720/722 पर ज़रूरी।</string> + <string name="perf_xalia">Steam के xalia हेल्पर को छोड़ें</string> + <string name="perf_fastsync">droiddeck-ntsync (एक्सपेरिमेंटल)</string> + <string name="perf_fastsync_hint">जांच के लिए Linux ntsync ड्राइवर की यूज़रस्पेस जगह लेने वाली चीज़। चालू रहते हुए, Proton इसे droiddeck-esync और droiddeck-fsync के बदले इस्तेमाल करता है। यह Proton Experimental ARM64, GE-Proton और proton-cachyos के साथ काम करता है। अगले सेशन से लागू होता है।</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync पहले</string> + <string name="perf_fsync_hint">स्टॉक Proton फ़ाइलों पर Proton का अपना fsync चलाता है, futex_waitv यूज़रस्पेस में दिया जाता है, इसलिए इसे किसी पैक की ज़रूरत नहीं और Steam के Proton अपडेट करते ही यह काम करता रहता है। चालू रहते हुए, गेम इसे droiddeck-esync से पहले इस्तेमाल करते हैं, सिवाय उन कुछ के जिन्हें Proton fsync से दूर रखता है। अगले सेशन से लागू होता है।</string> + <string name="perf_esync_hint">गेम के Windows थ्रेड wineserver के राउंड ट्रिप के बजाय eventfd और शेयर्ड मेमोरी से एक-दूसरे का इंतज़ार करते हैं। उस खास Proton बिल्ड के लिए बने droiddeck-esync पैक का उपयोग करता है; पैक अपने आप मिल जाते हैं। किसी नए Proton बिल्ड के लिए पैक न होने तक droiddeck-fsync जगह लेता है। अगले सेशन से लागू होता है।</string> + <string name="perf_xalia_hint">Proton के गेमपैड नेविगेशन हेल्पर को बंद करता है, जो हर गेम में CPU खर्च करता है।</string> + <string name="perf_seccomp">proot को seccomp के बिना चलाएं</string> + <string name="perf_seccomp_hint">कुछ कर्नेल पर missing-syscall एरर ठीक कर सकता है। परफ़ॉर्मेंस घटा सकता है।</string> + <string name="perf_fastpath">proot फ़ास्ट पाथ</string> + <string name="perf_fastpath_needs_seccomp">proot के seccomp फ़िल्टर की ज़रूरत है।</string> + <string name="perf_fastpath_hint">तेज़ फ़ाइल लुकअप और कम अटकाव। अगर कोई प्रोग्राम अपनी फ़ाइलें न खोज पाए तो इसे बंद करें।</string> + <string name="perf_identity">सेशन पहचान</string> + <string name="perf_hostname">होस्ट नाम</string> + <string name="perf_hostname_hint">वह नाम जो Steam इस डिवाइस के लिए देखता है। खाली छोड़ने पर %1$s वापस आ जाता है।</string> + <string name="perf_hostname_invalid">सिर्फ़ अक्षर, अंक और बीच के हाइफ़न, 63 वर्णों तक।</string> + <string name="perf_phantom_warning">Android इस सेशन को बंद करने के लिए सेट है</string> + <string name="driver_installed">इंस्टॉल है</string> + <string name="driver_available">डाउनलोड के लिए उपलब्ध</string> + <string name="driver_nothing_new">पिछली जांच के बाद कुछ नया नहीं है।</string> + <string name="driver_restore_builtin">बिल्ट-इन ड्राइवर वापस लाएं</string> + <string name="driver_delete_builtin">ऐप में बिल्ट-इन: यह इसकी अनपैक की गई फ़ाइलें हटाता है और इसे छिपा देता है। बिल्ट-इन ड्राइवर वापस लाएं इसे फिर से वापस लाता है।</string> + <string name="driver_delete_bundle">Android + Linux बंडल का आधा हिस्सा: दोनों हिस्से हटाए जाते हैं। उपयोग में मौजूद ड्राइवर डिफ़ॉल्ट पर वापस चला जाता है।</string> + <string name="driver_delete_plain">इसकी फ़ाइलें ऐप से हटा दी जाती हैं।</string> + <string name="driver_delete_in_use">इसकी फ़ाइलें हटा दी जाती हैं। यह उपयोग में है, इसलिए डिफ़ॉल्ट इसकी जगह ले लेता है।</string> + <string name="drawer_page_display">डिस्प्ले</string> + <string name="drawer_page_effects">इफ़ेक्ट</string> + <string name="drawer_page_controls">कंट्रोल</string> + <string name="drawer_page_components">कॉम्पोनेंट</string> + <string name="drawer_page_controller">कंट्रोलर</string> + <string name="drawer_page_settings">सेटिंग</string> + <string name="drawer_page_desc">%1$s पेज</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">डेस्कटॉप</string> + <string name="drawer_steam_menu">Steam मेनू</string> + <string name="drawer_quick_access">Quick Access</string> + <string name="drawer_open_qam">Quick Access Menu खोलें</string> + <string name="drawer_prev_page">पिछला पेज</string> + <string name="drawer_next_page">अगला पेज</string> + <string name="drawer_hud">परफ़ॉर्मेंस HUD</string> + <string name="drawer_scaling">स्केलिंग मोड</string> + <string name="drawer_scaling_sharpness">स्केलिंग शार्पनेस</string> + <string name="drawer_effects">स्क्रीन इफ़ेक्ट</string> + <string name="drawer_effects_hint">लाइव। कोई इफ़ेक्ट चालू होने पर थोड़ा GPU समय लगता है।</string> + <string name="drawer_look">प्रीसेट</string> + <string name="drawer_look_custom">कस्टम</string> + <string name="drawer_effects_advanced">एडवांस्ड</string> + <string name="widgets_expanded">विस्तृत</string> + <string name="widgets_collapsed">संक्षिप्त</string> + <string name="drawer_cas">शार्पनिंग (CAS)</string> + <string name="drawer_cas_level">शार्पनिंग स्तर</string> + <string name="drawer_fake_hdr">फ़ेक HDR</string> + <string name="drawer_deband">डीबैंडिंग</string> + <string name="drawer_deband_strength">डीबैंडिंग तीव्रता</string> + <string name="drawer_brightness">ब्राइटनेस</string> + <string name="drawer_contrast">कंट्रास्ट</string> + <string name="drawer_gamma">गामा</string> + <string name="drawer_saturation">सैचुरेशन</string> + <string name="drawer_fxaa">FXAA (एज स्मूदिंग)</string> + <string name="drawer_toon">टून आउटलाइन</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">टेक्सचर फ़िल्टरिंग</string> + <string name="drawer_texture_hint">DirectX 9–11 गेम, उनके अगले लॉन्च से।</string> + <string name="drawer_anisotropy">एनिसोट्रॉपिक फ़िल्टरिंग</string> + <string name="drawer_texture_sharpness">टेक्सचर शार्पनेस</string> + <string name="drawer_texture_sharpness_note">एक नेगेटिव mip बायस: ज़्यादा शार्प टेक्सचर, कुछ शिमर। ऑटो उस हद से मेल खाता है जितना सेशन को स्क्रीन के लिए बड़ा किया गया है।</string> + <string name="drawer_touch_auto">ऑटो (%1$s)</string> + <string name="drawer_keyboard">कीबोर्ड</string> + <string name="drawer_pc_keyboard">पीसी कीबोर्ड</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">दूसरी स्क्रीन</string> + <string name="drawer_second_screen_mode">मोड</string> + <string name="drawer_second_screen_shows">दिखाता है</string> + <string name="drawer_android_apps">Android ऐप</string> + <string name="drawer_launch_app">ऐप लॉन्च करें</string> + <string name="common_hide">छिपाएं</string> + <string name="common_show">दिखाएं</string> + <string name="drawer_no_apps">लॉन्च करने लायक कोई ऐप नहीं</string> + <string name="drawer_session_behavior">सेशन व्यवहार</string> + <string name="drawer_suspend_hint">अभी और इस मोड के भविष्य के सेशन पर लागू होता है।</string> + <string name="drawer_next_session">अगला सेशन</string> + <string name="drawer_support">सपोर्ट</string> + <string name="drawer_logs">सेशन लॉग</string> + <string name="drawer_logs_hint">बग रिपोर्ट में जोड़ें</string> + <string name="drawer_share_logs">लॉग शेयर करें</string> + <string name="drawer_background">बैकग्राउंड</string> + <string name="drawer_stop">बंद करें</string> + <string name="drawer_stop_title">सेशन बंद करें?</string> + <string name="drawer_running_now">अभी चल रहा है</string> + <string name="drawer_no_game">किसी Proton पर कोई गेम नहीं चल रहा</string> + <string name="drawer_selected_profile">गेम प्रोफ़ाइल</string> + <string name="drawer_selected_profile_none">प्रोफ़ाइल जानकारी अभी उपलब्ध नहीं है।</string> + <string name="drawer_profile_refresh_failed_initial">गेम प्रोफ़ाइल लोड नहीं हो सकीं। अपने आप फिर कोशिश हो रही है।</string> + <string name="drawer_profile_refresh_failed_stale">गेम प्रोफ़ाइल रीफ़्रेश नहीं हो सकीं। आख़िरी उपलब्ध डेटा दिखाया जा रहा है; अपने आप फिर कोशिश हो रही है।</string> + <string name="drawer_profile_inspect_hint">अगले लॉन्च पर इस्तेमाल होने वाली सेव की गई सेटिंग देखें और बदलें।</string> + <string name="drawer_profile_default">डिफ़ॉल्ट प्रोफ़ाइल इस्तेमाल हो रही है</string> + <string name="drawer_profile_custom">कस्टम प्रोफ़ाइल</string> + <string name="drawer_component_inherited">डिफ़ॉल्ट प्रोफ़ाइल से %1$s इस्तेमाल हो रहा है।</string> + <string name="drawer_game_components_note">बदलाव %1$s की प्रोफ़ाइल में सेव होते हैं और अगली बार गेम लॉन्च होने पर लागू होते हैं।</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d गेम ओवरराइड</item> + <item quantity="other">%1$d गेम ओवरराइड</item> + </plurals> + <string name="drawer_no_proton">अभी कोई Proton इंस्टॉल नहीं है।</string> + <string name="drawer_game_running">गेम चल रहा है</string> + <string name="drawer_reapplied">लॉन्च %1$s पर फिर से लागू हुआ</string> + <string name="drawer_original">ओरिजिनल</string> + <string name="drawer_components_note">Components पेज पर इंपोर्ट और डिलीट करें।</string> + <string name="setup_check_done">पूरा हुआ</string> + <string name="setup_check_warn">ध्यान देने की ज़रूरत</string> + <string name="setup_check_busy">काम हो रहा है</string> + <string name="setup_limit_adb_failed">वायरलेस डीबगिंग का IP एड्रेस और पोर्ट जांचें, या अगर Android ने इस डिवाइस को हटा दिया है तो फिर से पेयर करें।</string> + <string name="setup_install">इंस्टॉल करें</string> + <string name="setup_manage">मैनेज करें</string> + <string name="setup_tab_overview">ओवरव्यू</string> + <string name="setup_tab_controller">कंट्रोलर</string> + <string name="setup_tab_session">सेशन</string> + <string name="setup_tab_launcher">लॉन्चर</string> + <string name="setup_title">सेटअप</string> + <string name="setup_all_set">● सब तैयार</string> + <string name="setup_n_ready">%2$d में से %1$d तैयार</string> + <string name="setup_system_check">सिस्टम जांच</string> + <string name="setup_gpu_supported">डिवाइस समर्थित</string> + <string name="setup_gpu_untested">अनटेस्टेड GPU</string> + <string name="setup_gpu_unsupported">GPU समर्थित नहीं</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">Steam को Adreno ड्राइवर चाहिए; %1$s में काली स्क्रीन दिख सकती है</string> + <string name="setup_runtime">Linux रनटाइम</string> + <string name="setup_runtime_progress">%1$s · %2$d%%</string> + <string name="setup_runtime_missing">इंस्टॉल नहीं है · लगभग 3 GB, पहली बार Play पर इंस्टॉल होगा</string> + <string name="setup_installed">इंस्टॉल है</string> + <string name="setup_runtime_update">%1$s · अपडेट उपलब्ध</string> + <string name="setup_runtime_current">%1$s · नवीनतम</string> + <string name="setup_limit">चाइल्ड-प्रोसेस सीमा</string> + <string name="setup_limit_on">चालू · Android Steam बंद कर सकता है। इसे बंद करने में एक मिनट लगता है।</string> + <string name="setup_limit_unset">सेट नहीं · Android Steam बंद कर सकता है। इसे बंद करने में एक मिनट लगता है।</string> + <string name="setup_limit_unknown">जांचा नहीं जा सका · इसे बंद करने में एक मिनट लगता है।</string> + <string name="setup_fix_it">ठीक करें</string> + <string name="setup_details">जानकारी</string> + <string name="setup_dev_options">डेवलपर विकल्प</string> + <string name="setup_use_wireless">वायरलेस डीबगिंग इस्तेमाल करें</string> + <string name="setup_check_again">फिर से जांचें</string> + <string name="setup_limit_turn_on">सीमा चालू करें</string> + <string name="setup_account">Steam खाता</string> + <string name="setup_signed_in_offline">%1$s के रूप में साइन इन · ऑफ़लाइन मोड</string> + <string name="setup_signed_in">%1$s के रूप में साइन इन</string> + <string name="setup_sign_in">Play दबाएं और Steam में साइन इन करें</string> + <string name="setup_tools">टूल</string> + <string name="setup_controller">कंट्रोलर</string> + <string name="setup_controller_unavailable">कंट्रोलर सेटिंग उपलब्ध नहीं हैं।</string> + <string name="setup_session">सेशन</string> + <string name="setup_logs">सेशन लॉग</string> + <string name="setup_logs_hint">हर सेशन के बाद सेव होता है</string> + <string name="setup_agent_commands">डिबगिंग टूल</string> + <string name="setup_latest_logs">नवीनतम सेशन लॉग</string> + <string name="setup_saved_logs">सेव किए सेशन लॉग</string> + <string name="setup_saved_logs_hint">सबसे नए %1$d रखता है</string> + <string name="setup_clear_logs">लॉग हटाएं</string> + <string name="setup_offline">ऑफ़लाइन मोड</string> + <string name="setup_sign_in_first">पहले Steam में साइन इन करें</string> + <string name="setup_launcher">लॉन्चर</string> + <string name="setup_language">भाषा</string> + <string name="setup_language_hint">अगली बार शुरू होने पर Steam भी इसे इस्तेमाल करता है।</string> + <string name="setup_language_system">सिस्टम डिफ़ॉल्ट (%1$s)</string> + <string name="setup_language_system_short">सिस्टम डिफ़ॉल्ट</string> + <string name="setup_app_scale">ऐप स्केल</string> + <string name="setup_app_scale_hint">DroidDeck टेक्स्ट और कंट्रोल</string> + <string name="setup_app_scale_default">%1$d%% (डिफ़ॉल्ट)</string> + <string name="setup_theme">थीम</string> + <string name="setup_theme_hint">लॉन्चर की दिखावट</string> + <string name="setup_home">होम स्क्रीन के तौर पर इस्तेमाल करें</string> + <string name="setup_home_on">DroidDeck होम ऐप हो सकता है</string> + <string name="setup_home_off">DroidDeck को होम ऐप के तौर पर कभी नहीं दिखाया जाता</string> + <string name="setup_orientation">ओरिएंटेशन</string> + <string name="setup_orientation_auto">ऑटोमैटिक लैंडस्केप</string> + <string name="setup_orientation_landscape">लैंडस्केप</string> + <string name="setup_orientation_reverse">उलटा लैंडस्केप</string> + <string name="setup_fullscreen">फ़ुलस्क्रीन</string> + <string name="setup_fullscreen_on">Android स्टेटस और नेविगेशन बार छिपाता है</string> + <string name="setup_fullscreen_off">Android स्टेटस और नेविगेशन बार दिखाता है</string> + <string name="setup_default_home">डिफ़ॉल्ट होम ऐप</string> + <string name="setup_choose_home">होम ऐप चुनें</string> + <string name="setup_choose">चुनें</string> + <string name="setup_linux_apps">Linux ऐप (बीटा)</string> + <string name="setup_tool_files">फ़ाइलें</string> + <string name="setup_tool_files_hint">फ़ाइलें ब्राउज़ और मैनेज करें</string> + <string name="setup_tool_protons">Proton वर्शन</string> + <string name="setup_tool_protons_hint">ARM64 Proton बिल्ड इंस्टॉल करें</string> + <string name="setup_tool_performance">परफ़ॉर्मेंस</string> + <string name="setup_tool_performance_hint">CPU कोर असाइनमेंट</string> + <string name="setup_tool_roms">ROM फ़ोल्डर</string> + <string name="setup_tool_roms_hint">एमुलेटर गेम जहां रखे जाते हैं</string> + <string name="setup_card_components">कॉम्पोनेंट</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">कंट्रोल</string> + <string name="setup_card_controls_hint">बटन मैपिंग</string> + <string name="store_tab_discover">खोजें</string> + <string name="store_tab_search">सर्च</string> + <string name="store_tab_installed">इंस्टॉल किए</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">स्टोर</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak तैयार</string> + <string name="store_needs_runtime">पहले सेटअप से Linux रनटाइम इंस्टॉल करें।</string> + <string name="store_setup_title">Flatpak सेट करें</string> + <string name="store_setup_text">Flatpak जोड़ता है और इसे Flathub से जोड़ता है (लगभग 20 MB)। पहला ऐप अपना रनटाइम भी डाउनलोड करता है।</string> + <string name="store_setting_up">सेट हो रहा है…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">अपडेट</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d%%</string> + <string name="store_starting">शुरू हो रहा है…</string> + <string name="store_categories">कैटेगरी</string> + <string name="store_unreachable">Flathub तक नहीं पहुंच सका। अपना कनेक्शन जांचें और फिर कोशिश करें।</string> + <string name="store_try_again">फिर कोशिश करें</string> + <string name="store_search_label">Flathub में सर्च करें</string> + <string name="store_search_placeholder">Firefox, गेम, एमुलेटर…</string> + <string name="store_search">सर्च</string> + <string name="store_all">सभी</string> + <string name="store_searching">सर्च हो रहा है…</string> + <string name="store_search_hint">नाम से सर्च करें या कोई कैटेगरी चुनें। सिर्फ़ ARM64 ऐप सूचीबद्ध हैं।</string> + <string name="store_no_results">Flathub पर इससे मेल खाने वाला कोई ARM64 ऐप नहीं है।</string> + <string name="store_results">नतीजे</string> + <string name="store_check_updates">अपडेट देखें</string> + <string name="store_update_all">सभी अपडेट करें (%1$d)</string> + <string name="store_all_current">● सब कुछ नवीनतम है</string> + <string name="store_no_apps">अभी कोई ऐप नहीं है। खोजें या सर्च में देखें।</string> + <string name="store_setup_first_note">ऐप इंस्टॉल करने के लिए Flatpak सेट करें।</string> + <string name="store_update_available_prefix">अपडेट उपलब्ध · %1$s</string> + <string name="store_open">खोलें</string> + <string name="store_details">जानकारी</string> + <string name="store_remove">हटाएं</string> + <string name="store_press_again">हटाने के लिए फिर से दबाएं</string> + <string name="store_installing">इंस्टॉल हो रहा है…</string> + <string name="store_install">इंस्टॉल करें</string> + <string name="store_setup_first">पहले Flatpak सेट करें</string> + <string name="store_download_size">%1$s डाउनलोड</string> + <string name="store_installed_chip">● इंस्टॉल है</string> + <string name="store_details_failed">इस ऐप का Flathub पेज लोड नहीं हो सका।</string> + <string name="store_loading">Flathub से लोड हो रहा है…</string> + <string name="store_no_arm">Flathub पर कोई ARM64 बिल्ड नहीं है, इसलिए इसे यहां इंस्टॉल नहीं किया जा सकता।</string> + <string name="store_screenshots">स्क्रीनशॉट</string> + <string name="store_about">परिचय</string> + <string name="store_app_id">ऐप ID</string> + <string name="store_license">लाइसेंस</string> + <string name="store_runtime">रनटाइम</string> + <string name="store_installed_size">इंस्टॉल आकार</string> + <string name="store_installed_size_value">%1$s (साथ में इसका शेयर्ड रनटाइम)</string> + <string name="store_website">वेबसाइट</string> + <string name="store_verified">वेरिफ़ाइड</string> + <string name="upd_title">अपडेट</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">%1$s को जांचा गया</string> + <string name="upd_not_checked">अभी जांचा नहीं गया</string> + <string name="upd_perm_title">DroidDeck को खुद अपडेट होने दें</string> + <string name="upd_perm_text">अगली स्क्रीन पर, “इस सोर्स से अनुमति दें” चालू करें, फिर वापस आएं। अपडेट खुद जारी रहेगा।</string> + <string name="upd_open_settings">सेटिंग खोलें</string> + <string name="upd_not_now">अभी नहीं</string> + <string name="upd_signed_status">अलग तरीके से साइन किया गया</string> + <string name="upd_signed_headline">यहीं अपडेट नहीं हो सकता</string> + <string name="upd_signed_detail">यह कॉपी DroidDeck की रिलीज़ कुंजी से साइन नहीं है, इसलिए अपडेट इसके ऊपर इंस्टॉल नहीं हो सकते। बदलने के लिए इसे अनइंस्टॉल करें।</string> + <string name="upd_available">अपडेट उपलब्ध</string> + <string name="upd_blocked_status">इंस्टॉल नहीं हो सकता</string> + <string name="upd_switch">बदलने के लिए तैयार</string> + <string name="upd_ahead_status">स्टेबल से आगे</string> + <string name="upd_ahead_headline">आप स्टेबल से आगे हैं</string> + <string name="upd_ahead_detail">यह बिल्ड नवीनतम स्टेबल से नया है। इसके अगले रिलीज़ के साथ आप स्टेबल पर चले जाएंगे।</string> + <string name="upd_gone_status">टेस्ट खत्म हुआ</string> + <string name="upd_gone_headline">यह टेस्ट खत्म हो गया है</string> + <string name="upd_gone_detail">इसका फ़िक्स मर्ज हो गया या हटा दिया गया। नवीनतम फ़िक्स के लिए प्रीव्यू फ़ॉलो करें।</string> + <string name="upd_current_status">नवीनतम</string> + <string name="upd_current_headline">आपके पास नवीनतम %1$s है</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">अपडेट%1$s</string> + <string name="upd_install_button">इंस्टॉल%1$s</string> + <string name="upd_install_anyway">फिर भी स्टेबल %1$s इंस्टॉल करें</string> + <string name="upd_follow_preview">प्रीव्यू फ़ॉलो करें</string> + <string name="upd_stop_session">अपडेट करने के लिए चल रहा सेशन बंद करें।</string> + <string name="upd_less">कम</string> + <string name="upd_more">ज़्यादा</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">नया प्रीव्यू बिल्ड</string> + <string name="upd_new_test">PR #%1$d टेस्ट बिल्ड</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">अपडेट चैनल</string> + <string name="upd_stable">स्टेबल</string> + <string name="upd_stable_hint">जांचे गए रिलीज़, ज़्यादातर लोगों के लिए</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">प्रीव्यू</string> + <string name="upd_preview_hint">नवीनतम फ़िक्स, स्टेबल से पहले</string> + <string name="upd_preview_current">नवीनतम</string> + <string name="upd_tests">टेस्ट बिल्ड</string> + <string name="upd_tests_hint">रिलीज़ होने से पहले किसी फ़िक्स को आज़माएं</string> + <string name="upd_tests_none">अभी कोई नहीं</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">DroidDeck की इस कॉपी के लिए नहीं बना</string> + <string name="upd_updated">%1$s को अपडेट किया गया</string> + <string name="upd_test_name">PR #%1$d टेस्ट</string> + <string name="upd_while_ago">कुछ समय पहले</string> + <string name="upd_just_now">अभी-अभी</string> + <plurals name="upd_tests_count"> + <item quantity="one">%1$d आज़माने के लिए</item> + <item quantity="other">%1$d आज़माने के लिए</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="one">आपके बिल्ड के बाद से बदलाव · %1$d बिल्ड पीछे</item> + <item quantity="other">आपके बिल्ड के बाद से बदलाव · %1$d बिल्ड पीछे</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="one">नए बदलाव · %1$d बिल्ड</item> + <item quantity="other">नए बदलाव · %1$d बिल्ड</item> + </plurals> + <string name="gate_title">बदलने के लिए एक Android सेटिंग</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Android उन ऐप को रोकता है जो कई बैकग्राउंड प्रोसेस शुरू करते हैं, और Steam दर्जनों शुरू करता है। जब तक यह नहीं बदलता, गेम बिना किसी एरर के बंद हो जाते हैं।</string> + <string name="gate_fix_for_me">मेरे लिए ठीक करें</string> + <string name="gate_toggle_steps">“Disable child process restrictions” चालू करें, फिर वापस आएं। डेवलपर विकल्प चालू रहने दें, नहीं तो यह स्विच फिर से बंद हो जाएगा।</string> + <string name="gate_open_dev">डेवलपर विकल्प खोलें</string> + <string name="gate_no_toggle">इस Android वर्शन में इसके लिए कोई स्विच नहीं है, इसलिए DroidDeck इसे वायरलेस डीबगिंग के ज़रिए बदलता है। इसमें लगभग एक मिनट लगता है, किसी कंप्यूटर की ज़रूरत नहीं।</string> + <string name="gate_needs_wifi">वायरलेस डीबगिंग के लिए Wi-Fi चाहिए। कोई भी नेटवर्क काम करेगा; इंटरनेट की ज़रूरत नहीं है।</string> + <string name="gate_needs_notifications">पेयरिंग कोड एक नोटिफ़िकेशन में आता है। DroidDeck के लिए नोटिफ़िकेशन की अनुमति दें, या अन्य तरीके इस्तेमाल करें।</string> + <string name="gate_allow_notifications">नोटिफ़िकेशन की अनुमति दें</string> + <string name="gate_step1">Settings → डेवलपर विकल्प → वायरलेस डीबगिंग खोलें और उसे चालू करें</string> + <string name="gate_step2">“Pair device with pairing code” पर टैप करें</string> + <string name="gate_step3">DroidDeck नोटिफ़िकेशन में कोड टाइप करें</string> + <string name="gate_waiting">पेयरिंग पॉप-अप का इंतज़ार हो रहा है…</string> + <string name="gate_code_needed">पॉप-अप मिल गया। इसका कोड नोटिफ़िकेशन में डालें।</string> + <string name="gate_status">स्थिति</string> + <string name="gate_polling">हर 2 सेकंड में जांच हो रही है। यह बंद होने पर यह पेज बंद हो जाता है।</string> + <string name="gate_other_ways">अन्य तरीके</string> + <string name="gate_other_toggle">वायरलेस डीबगिंग, मैनुअल एड्रेस, कंप्यूटर ADB</string> + <string name="gate_other_no_toggle">मैनुअल एड्रेस, कंप्यूटर ADB, डेवलपर विकल्प</string> + <string name="gate_use_wireless_instead">इसके बदले वायरलेस डीबगिंग इस्तेमाल करें</string> + <string name="gate_some_roms">कुछ ROM फिर भी यह स्विच जोड़ देते हैं। डेवलपर विकल्प में “child process” देखें।</string> + <string name="gate_manual_hint">अगर नोटिफ़िकेशन काम न करे, तो पॉप-अप का एड्रेस और कोड खुद डालें।</string> + <string name="gate_enter_manually">एड्रेस खुद डालें</string> + <string name="gate_from_computer">ADB वाले कंप्यूटर से:</string> + <string name="gate_copy_command">कमांड कॉपी करें</string> + <string name="gate_choose_display">Android Settings किस डिस्प्ले पर खोलना है चुनें।</string> + <string name="gate_main_screen">मुख्य स्क्रीन</string> + <string name="wadb_finding_port">पेयर हो गया। वायरलेस डीबगिंग का पोर्ट खोजा जा रहा है…</string> + <string name="wadb_enter_port">पेयर हो गया। Settings से पोर्ट डालें।</string> + <string name="wadb_applying">डिवाइस मिल गया। सेटिंग लागू की जा रही है…</string> + <string name="wadb_apply_failed">खुद लागू नहीं हो सका। नीचे दिया एड्रेस जांचें और फिर कोशिश करें। %1$s</string> + <string name="wadb_title_wireless">वायरलेस डीबगिंग</string> + <string name="wadb_title_done">चाइल्ड-प्रोसेस सीमा अपडेट हुई</string> + <string name="wadb_title">चाइल्ड-प्रोसेस सीमा बदलें</string> + <string name="wadb_lede_compact0">अगर यह विकल्प न हो, तो वायरलेस डीबगिंग से पेयर करें।</string> + <string name="wadb_lede_compact1">पेयर हो गया। वायरलेस डीबगिंग में IP एड्रेस और पोर्ट जांचें।</string> + <string name="wadb_lede_done">चाइल्ड-प्रोसेस सीमा अपडेट हो गई।</string> + <string name="wadb_lede0">अगर डेवलपर विकल्प में चाइल्ड-प्रोसेस सेटिंग न हो तो स्टेप 2 इस्तेमाल करें। डेवलपर विकल्प → वायरलेस डीबगिंग → Pair device with pairing code खोलें।</string> + <string name="wadb_lede1">पेयर हो गया। नीचे दिया एड्रेस इस्तेमाल करें, या इसे वायरलेस डीबगिंग → IP address & Port से अपडेट करें।</string> + <string name="wadb_lede_done_long">चाइल्ड-प्रोसेस सीमा वायरलेस डीबगिंग के ज़रिए अपडेट हो गई।</string> + <string name="wadb_confirmed_on">Android ने पुष्टि की कि सीमा चालू है।</string> + <string name="wadb_confirmed_off">Android ने पुष्टि की कि सीमा बंद है।</string> + <string name="wadb_pair_group">2 · वायरलेस डीबगिंग पेयर करें</string> + <string name="wadb_pair_address">पेयरिंग पॉप-अप का IP एड्रेस और पोर्ट</string> + <string name="wadb_pin">पेयरिंग कोड (PIN)</string> + <string name="wadb_pin_placeholder">6 अंक</string> + <string name="wadb_pair">पेयर करें</string> + <string name="wadb_apply_group">2 · वायरलेस डीबगिंग के ज़रिए लागू करें</string> + <string name="wadb_connect_address">कनेक्शन का IP एड्रेस और पोर्ट</string> + <string name="wadb_pair_again">फिर से पेयर करें</string> + <string name="wadb_turn_off">सीमा बंद करें</string> + <string name="wadb_other_ways">इसे बदलने के अन्य तरीके</string> + <string name="wadb_dev_try_first">1 · डेवलपर विकल्प (पहले आज़माएं)</string> + <string name="wadb_restrict_on">“Restrict child processes” चालू करें।</string> + <string name="wadb_restrict_off">“Restrict child processes” बंद करें।</string> + <string name="wadb_computer_last">3 · कंप्यूटर ADB (आख़िरी विकल्प)</string> + <string name="wadb_only_if">सिर्फ़ तब जब वायरलेस डीबगिंग उपलब्ध न हो।</string> + <string name="wadb_computer_group">3 · कंप्यूटर ADB (आख़िरी विकल्प)</string> + <string name="ctrl_osc_hint">सेशन में टच कंट्रोल कब दिखते हैं</string> + <string name="ctrl_color">रंग</string> + <string name="ctrl_color_hint">ऑन-स्क्रीन बटन का रंग</string> + <string name="ctrl_custom">कस्टम</string> + <string name="ctrl_opacity">ओपैसिटी</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">बटन का आकार</string> + <string name="ctrl_size_hint" formatted="false">100% मानक आकार है</string> + <string name="ctrl_stick_click">स्टिक क्लिक</string> + <string name="ctrl_stick_click_hint">L3 या R3 के लिए किसी स्टिक को डबल-टैप करके दबाए रखें</string> + <string name="ctrl_adaptive">एडैप्टिव स्टिक</string> + <string name="ctrl_rumble">गेम वाइब्रेशन</string> + <string name="ctrl_steam_button">Steam बटन</string> + <string name="ctrl_qam_button">QAM बटन</string> + <string name="ctrl_keyboard_button">कीबोर्ड बटन</string> + <string name="ctrl_select_steam">Steam मेनू खोलने के लिए Select दबाएं</string> + <string name="ctrl_select_steam_hint">View/Select के बदले।</string> + <string name="setup_animations">एनिमेशन</string> + <string name="ctrl_adaptive_hint">आप जहां टच करें वहां स्टिक दिखते हैं और छोड़ने पर छिप जाते हैं</string> + <string name="ctrl_layout">लेआउट</string> + <string name="ctrl_layout_custom">कस्टम पोज़िशन सेव हैं</string> + <string name="ctrl_layout_default">इस स्क्रीन के हिसाब से फ़िट किया गया</string> + <string name="ctrl_edit">संपादित करें</string> + <string name="ctrl_reset">रीसेट करें</string> + <string name="ctrl_mapping">बटन मैपिंग</string> + <string name="ctrl_mapping_default">हर बटन अपना इनपुट भेजता है</string> + <string name="ctrl_mapping_count">%2$d में से %1$d बटन रीमैप किए गए</string> + <string name="ctrl_configure">कॉन्फ़िगर करें</string> + <string name="ctrl_reset_all">कंट्रोलर रीसेट करें</string> + <string name="ctrl_reset_all_hint">सभी कंट्रोलर सेटिंग और लेआउट वापस लाता है</string> + <string name="ctrl_mapping_lede">हर ऑन-स्क्रीन बटन गेम को क्या भेजता है। Hidden बटन हटा देता है।</string> + <string name="ctrl_onscreen_buttons">ऑन-स्क्रीन बटन</string> + <string name="ctrl_defaults">डिफ़ॉल्ट</string> + <string name="ctrl_reset_mapping">मैपिंग रीसेट करें</string> + <string name="content_android_apps">Android ऐप</string> + <string name="content_apps_count">%1$d ऐप</string> + <string name="content_no_android_apps">लॉन्च करने लायक कोई Android ऐप नहीं मिला।</string> + <string name="content_desktop_installed">● डेस्कटॉप इंस्टॉल है</string> + <string name="content_desktop_first_open">पहली बार खोलने पर इंस्टॉल होता है</string> + <string name="content_available">इंस्टॉल के लिए उपलब्ध</string> + <string name="content_not_installed">इंस्टॉल नहीं है।</string> + <string name="content_not_installed_suffix">%1$s · इंस्टॉल नहीं है</string> + <string name="content_open_named">%1$s खोलें</string> + <string name="content_games">गेम</string> + <string name="content_choose_roms">ROM फ़ोल्डर चुनें।</string> + <string name="content_retroarch_roms">RetroArch में /root/ROMs तक ब्राउज़ करें।</string> + <string name="content_add_roms">%1$s गेम ROMs/%2$s में जोड़ें।</string> + <string name="content_install_named">%1$s इंस्टॉल करें</string> + <string name="content_stop_to_install">इंस्टॉल करने के लिए सेशन बंद करें</string> + <string name="content_runtime_required">रनटाइम ज़रूरी</string> + <string name="content_loading_details">इंस्टॉल जानकारी लोड हो रही है…</string> + <string name="content_details_unavailable">इंस्टॉल जानकारी उपलब्ध नहीं है। बाद में फिर कोशिश करें।</string> + <string name="content_needs_runtime">पहले सेटअप से Linux रनटाइम इंस्टॉल करें।</string> + <string name="content_stop_first">डेस्कटॉप ऐप इंस्टॉल करने के लिए चल रहा सेशन बंद करें।</string> + <string name="content_install_into">%1$s को Linux डेस्कटॉप में इंस्टॉल करता है।</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">छिपाने से यह डेस्कटॉप से हट जाता है। इसकी फ़ाइलें Linux रनटाइम में रहती हैं।</string> + <string name="content_rom_gone">वह गेम अब ROM फ़ोल्डर में नहीं है।</string> + <string name="content_desktop_system">डेस्कटॉप · %1$s</string> + <string name="content_launch_in">%1$s में लॉन्च करें</string> + <string name="content_also_in">%1$s में भी</string> + <string name="content_page_gone">वह पेज अब नहीं है। बाईं तरफ़ से कोई सेक्शन चुनें।</string> + <string name="content_open_desktop">डेस्कटॉप खोलें</string> + <string name="content_install_open_desktop">डेस्कटॉप इंस्टॉल करें और खोलें</string> + <string name="desktop_remove_title">डेस्कटॉप अनइंस्टॉल करें</string> + <string name="desktop_remove_text">डेस्कटॉप और इसके साथ आए ऐप हटाएं। गेम, जोड़े गए ऐप, सेटिंग और शेयर्ड लाइब्रेरी बनी रहेंगी।</string> + <string name="desktop_removing">डेस्कटॉप हटाया जा रहा है</string> + <string name="desktop_removed">डेस्कटॉप हटा दिया गया</string> + <string name="desktop_cleaning_legacy">पुरानी LXQt फ़ाइलें हटाई जा रही हैं</string> + <string name="desktop_removal_unknown">इस डेस्कटॉप वर्शन को हटाने से पहले DroidDeck अपडेट करें।</string> + <string name="content_linux_desktop">Linux डेस्कटॉप</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox और आपके एमुलेटर</string> + <string name="games_empty">Steam में इंस्टॉल किए गेम सेशन के बाद यहां दिखते हैं।</string> + <string name="games_play_steam">Steam चलाएं</string> + <string name="games_launch_settings">लॉन्च सेटिंग</string> + <string name="games_launch">लॉन्च करें</string> + <string name="games_files">गेम फ़ाइलें</string> + <string name="games_prefix">Proton प्रीफ़िक्स</string> + <string name="games_saves_looking">सेव खोजे जा रहे हैं…</string> + <string name="games_saves_none">नाम से कोई सेव फ़ोल्डर नहीं मिला। एक्सपोर्ट पूरा यूज़र फ़ोल्डर लेता है।</string> + <string name="games_saves_folders">%1$d सेव फ़ोल्डर</string> + <string name="games_saves_summary">%1$s · %2$d फ़ाइलें · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">सेव मैनेज करें</string> + <string name="games_saves">गेम सेव</string> + <string name="games_import_saves">सेव इंपोर्ट करें…</string> + <string name="games_export_gamehub">GameHub zip के रूप में एक्सपोर्ट करें…</string> + <string name="games_export_winlator">Winlator zip के रूप में एक्सपोर्ट करें…</string> + <string name="games_open_save_folder">सेव फ़ोल्डर खोलें</string> + <string name="games_never_played">कभी नहीं खेला</string> + <string name="games_runtime_busy_percent">रनटाइम व्यस्त · %1$d%%</string> + <string name="games_runtime_busy">रनटाइम व्यस्त</string> + <string name="games_runtime_first_play">पहली बार Play पर रनटाइम इंस्टॉल होता है</string> + <string name="games_runtime_update">रनटाइम अपडेट उपलब्ध</string> + <string name="games_runtime_ready">● रनटाइम तैयार</string> + <string name="games_last_played">आख़िरी बार %1$s को खेला</string> + <string name="games_internal">इंटरनल स्टोरेज</string> + <string name="games_added">जोड़ा गया गेम</string> + <string name="games_custom">कस्टम गेम</string> + <string name="games_store_storage">गेम स्टोरेज</string> + <string name="gpu_auto_hint">इस GPU के लिए सबसे बेहतर जोड़ी, हमेशा नवीनतम</string> + <string name="gpu_manual_hint">खुद एक जोड़ी चुनें</string> + <string name="gpu_checking">ड्राइवर देखे जा रहे हैं…</string> + <string name="gpu_none_listed">अभी कोई ड्राइवर सूचीबद्ध नहीं है। उन्हें देखने के लिए रीफ़्रेश करें।</string> + <string name="gpu_hide_other">दूसरे GPU के ड्राइवर छिपाएं</string> + <string name="gpu_show_other">दूसरे GPU के ड्राइवर दिखाएं</string> + <string name="gpu_advanced">एडवांस्ड</string> + <string name="gpu_set_with_display">डिस्प्ले ड्राइवर के साथ सेट: %1$s दोनों साथ लाता है।</string> + <string name="gpu_set_with_runtime">रनटाइम ड्राइवर के साथ सेट: %1$s दोनों साथ लाता है।</string> + <string name="gpu_runtime_hint">गेम और डेस्कटॉप में उपयोग होता है।</string> + <string name="gpu_runtime_default">रनटाइम डिफ़ॉल्ट</string> + <string name="gpu_display_hint">स्क्रीन पर फ़्रेम बनाता है। DroidDeck रीस्टार्ट होने के बाद लागू होता है।</string> + <string name="gpu_auto_by_gpu">ऑटो - GPU के हिसाब से चुना गया</string> + <string name="gpu_manage">डाउनलोड, इंपोर्ट या डिलीट करें…</string> + <string name="gpu_in_use_caps">उपयोग में</string> + <string name="gpu_builtin">DroidDeck में बिल्ट-इन ड्राइवर</string> + <string name="gpu_matched_pair">रनटाइम और डिस्प्ले ड्राइवर एक साथ मिलती-जुलती जोड़ी के रूप में सेट हैं।</string> + <string name="gpu_recommended_for">इस GPU के लिए सुझाया गया: %1$s।</string> + <string name="gpu_refresh_hint">इस GPU के लिए ड्राइवर देखने के लिए रीफ़्रेश करें।</string> + <string name="gpu_recommended_caps">सुझाया गया</string> + <string name="gpu_unavailable">उपलब्ध नहीं</string> + <string name="load_runtime">Linux रनटाइम</string> + <string name="load_runtime_detail">Linux रनटाइम तैयार किया जा रहा है</string> + <string name="load_session">सेशन</string> + <string name="load_session_detail">डिस्प्ले और ऑडियो शुरू किया जा रहा है</string> + <string name="load_steam">Steam क्लाइंट</string> + <string name="load_steam_detail">Steam और उसके कम्पैटिबिलिटी टूल जांचे जा रहे हैं</string> + <string name="load_opening_steam">Steam खोला जा रहा है</string> + <string name="load_opening_steam_detail">Steam के पहले फ़्रेम का इंतज़ार हो रहा है</string> + <string name="load_desktop">डेस्कटॉप</string> + <string name="load_desktop_detail">डेस्कटॉप तैयार किया जा रहा है</string> + <string name="load_opening">खोला जा रहा है</string> + <string name="load_opening_detail">पहले फ़्रेम का इंतज़ार हो रहा है</string> + <string name="load_starting_steam">Steam शुरू हो रहा है</string> + <string name="load_ended">सेशन खत्म हो गया</string> + <string name="load_step">स्टेप %2$d में से %1$d</string> + <string name="load_steam_paused">Steam रुका हुआ है</string> + <string name="deck_tab_grips">ग्रिप</string> + <string name="deck_tab_left">बायां पैड</string> + <string name="deck_tab_right">दायां पैड</string> + <string name="deck_tab_both">दोनों</string> + <string name="deck_tab_gamepad">गेमपैड</string> + <string name="deck_surface_stick">स्टिक</string> + <string name="deck_surface_trackpad">ट्रैकपैड</string> + <string name="deck_surface_switch">स्टिक और ट्रैकपैड के बीच बदलें</string> + <string name="deck_more_controls">और Deck कंट्रोल</string> + <string name="deck_panels">पैनल</string> + <string name="deck_close">दूसरी स्क्रीन के कंट्रोल बंद करें</string> + <string name="deck_open_steam_menu">Steam मेनू खोलें</string> + <string name="deck_left_upper">बायां · ऊपर</string> + <string name="deck_left_lower">बायां · नीचे</string> + <string name="deck_right_upper">दायां · ऊपर</string> + <string name="deck_right_lower">दायां · नीचे</string> + <string name="deck_click">क्लिक</string> + <string name="deck_right_trackpad">दायां ट्रैकपैड</string> + <string name="deck_left_trackpad">बायां ट्रैकपैड</string> + <string name="second_trackpad">ट्रैकपैड</string> + <string name="second_show_keyboard">कीबोर्ड दिखाएं</string> + <string name="second_hide_keyboard">कीबोर्ड छिपाएं</string> + <string name="second_terminal">Linux टर्मिनल</string> + <string name="second_terminal_not_ready">Linux सेशन अभी टर्मिनल के लिए तैयार नहीं है।</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">गेम</string> + <string name="rail_desktop">डेस्कटॉप</string> + <string name="rail_store">स्टोर</string> + <string name="rail_components">कॉम्पोनेंट</string> + <string name="rail_apps">ऐप</string> + <string name="rail_setup">सेटअप</string> + <string name="rail_updates">अपडेट</string> + <string name="rail_percent">%1$d%%</string> + <string name="rail_resume_named">%1$s फिर से शुरू करें</string> + <string name="roms_text">हर सेशन में /root/ROMs के रूप में दिखता है। इंटरनल स्टोरेज /root/Storage पर है।</string> + <string name="roms_none">कोई फ़ोल्डर नहीं चुना गया</string> + <string name="roms_applies">अगले सेशन से लागू होता है। SD कार्ड काम करते हैं।</string> + <string name="roms_choose">फ़ोल्डर चुनें</string> + <string name="roms_change">फ़ोल्डर बदलें</string> + <string name="proton_lede">एक ARM64 Proton बिल्ड इंस्टॉल करें, फिर इसे Steam → Properties → Compatibility में हर गेम के लिए चुनें।</string> + <string name="proton_stop_first">कम्पैटिबिलिटी टूल इंस्टॉल या हटाने के लिए चल रहा सेशन बंद करें।</string> + <string name="proton_available">उपलब्ध बिल्ड</string> + <string name="proton_installed">इंस्टॉल है %1$s</string> + <string name="proton_queued">पहले की रिक्वेस्ट से पेंडिंग</string> + <string name="proton_not_installed">इंस्टॉल नहीं है</string> + <string name="proton_update_available">अपडेट उपलब्ध → %1$s</string> + <string name="proton_remove_older">पुराने %1$s वर्शन हटाएं</string> + <string name="proton_remove_older_body">%1$s हटाता है। इन वर्शन पर सेट गेम को किसी और Proton पर बदलना होगा।</string> + <string name="proton_remove_body">इस वर्शन पर सेट गेम को किसी और Proton पर बदलना होगा।</string> + <string name="proton_install_now">अभी इंस्टॉल करें</string> + <string name="proton_cancel_queued">पेंडिंग रद्द करें</string> + <string name="pckb_android">Android कीबोर्ड</string> + <string name="pckb_hide">छिपाएं ✕</string> + <string name="main_launch_named">%1$s लॉन्च करें</string> + <string name="main_choose_screen">स्क्रीन चुनें। सेकेंडरी: %1$s।</string> + <string name="main_secondary_gone">सेकेंडरी डिस्प्ले अब उपलब्ध नहीं है।</string> + <string name="main_primary">प्राइमरी स्क्रीन</string> + <string name="main_secondary">सेकेंडरी स्क्रीन</string> + <string name="widgets_prev_tab">पिछला टैब</string> + <string name="widgets_next_tab">अगला टैब</string> + <string name="widgets_on">चालू</string> + <string name="widgets_all_n">सभी %1$d</string> + <string name="widgets_none">कोई नहीं</string> + <string name="widgets_n_of_m">%2$d में से %1$d</string> + <string name="widgets_off">बंद</string> + <string name="screen_bottom">नीचे की स्क्रीन</string> + <string name="game_env_applies">अगली बार गेम लॉन्च होने पर लागू होता है। गेम की प्रोफ़ाइल शेयर्ड प्रोफ़ाइल से ऊपर रहती है।</string> + <string name="game_env_other">ऐप ID से कोई और गेम…</string> + <string name="game_env_done">हो गया</string> + <string name="game_env_default">डिफ़ॉल्ट</string> + <string name="game_env_off">बंद</string> + <string name="game_env_change">बदलें…</string> + <string name="game_env_change_title">वेरिएबल बदलें</string> + <string name="game_env_restore_default">डिफ़ॉल्ट पर वापस जाएं</string> + <string name="game_env_eyebrow">Proton गेम</string> + <string name="pip_title">पिक्चर इन पिक्चर</string> + <string name="pip_auto">अपने आप PiP में जाएं</string> + <string name="unfolded_controls">खुली हुई स्क्रीन पर कंट्रोल</string> + <string name="unfolded_controls_hint">खुली होने पर, गेम Steam Deck के ट्रैकपैड और पीछे की ग्रिप के ऊपर दिखता है। बंद होने पर, गेम फ़ुल स्क्रीन में रहता है।</string> + <string name="pip_suspended">सस्पेंडेड</string> + <string name="pip_not_ready">सेशन तैयार होने पर पिक्चर इन पिक्चर उपलब्ध होता है।</string> + <string name="pip_permission">Android सेटिंग में DroidDeck के लिए पिक्चर इन पिक्चर की अनुमति दें।</string> + <string name="pip_unavailable">Android पिक्चर इन पिक्चर नहीं खोल सका।</string> + <string name="game_shortcut">शॉर्टकट</string> + <string name="game_shortcut_add">होम स्क्रीन पर जोड़ें</string> + <string name="game_shortcut_choose">गेम चुनें</string> + <string name="game_shortcut_scanning">इंस्टॉल किए गेम स्कैन हो रहे हैं…</string> + <string name="game_link_copy">लॉन्च लिंक कॉपी करें</string> + <string name="game_link_copied">लॉन्च लिंक कॉपी हो गया</string> + <string name="game_shortcut_unsupported">यह लॉन्चर शॉर्टकट नहीं जोड़ सकता। इसके बदले लॉन्च लिंक कॉपी करें इस्तेमाल करें।</string> + <string name="game_link_invalid">गेम लॉन्च लिंक मान्य नहीं है।</string> + <string name="game_link_missing">यह गेम अब आपकी DroidDeck लाइब्रेरी में नहीं है।</string> + <string name="game_link_session_busy">कोई Steam गेम लॉन्च करने से पहले चल रहा सेशन बंद करें।</string> + <string name="game_link_failed">गेम Steam को नहीं भेजा जा सका। इसे फिर से लॉन्च करने की कोशिश करें।</string> + <string name="setup_retry_removal">हटाना फिर से कोशिश करें</string> + <string name="runtime_removing">हटाया जा रहा है</string> + <string name="runtime_removal_incomplete">हटाना पूरा नहीं हुआ</string> + <string name="game_file_export">गेम फ़ाइल एक्सपोर्ट करें</string> + <string name="game_file_sync">सभी गेम फ़ाइलें फ़ोल्डर में सिंक करें</string> + <string name="game_file_stop_sync">गेम फ़ाइलें सिंक करना बंद करें</string> + <string name="game_file_folder">फ़्रंटएंड गेम फ़ाइलों के लिए फ़ोल्डर चुनें</string> + <string name="game_file_exported">गेम फ़ाइलें %1$s में सेव हुईं</string> + <string name="game_frontend_files">फ़्रंटएंड फ़ाइलें</string> + + <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> + <string name="main_saves_importing">%1$s में इंपोर्ट हो रहा है</string> + <plurals name="main_saves_imported"> + <item quantity="one">%2$s से %1$d फ़ाइल %3$s में इंपोर्ट हुई</item> + <item quantity="other">%2$s से %1$d फ़ाइलें %3$s में इंपोर्ट हुईं</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">%2$s से %1$d फ़ाइल %3$s में इंपोर्ट हुई। पुराने सेव Download/DroidDeck/Saves/backups में बैकअप हो गए</item> + <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> + <string name="main_saves_exporting">%1$s एक्सपोर्ट हो रहा है</string> + <plurals name="main_saves_exported"> + <item quantity="one">%1$d फ़ाइल %2$s के रूप में एक्सपोर्ट हुई: %3$s</item> + <item quantity="other">%1$d फ़ाइलें %2$s के रूप में एक्सपोर्ट हुईं: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">%1$s (%2$s) को सेव करने के लिए जगह चुनें</string> + <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> + <plurals name="main_logs_cleared"> + <item quantity="one">%1$d सेशन लॉग फ़ोल्डर हटाया गया।</item> + <item quantity="other">%1$d सेशन लॉग फ़ोल्डर हटाए गए।</item> + </plurals> + <string name="main_wadb_pair_failed">वायरलेस डीबगिंग पेयरिंग विफल रही</string> + <string name="main_wadb_command_failed">वायरलेस डीबगिंग कमांड विफल रहा</string> + <string name="main_wadb_unavailable">वायरलेस डीबगिंग उपलब्ध नहीं है</string> + <string name="main_adb_copied">ADB कमांड कॉपी हो गया</string> + <string name="main_pick_roms">ROM फ़ोल्डर चुनें</string> + <string name="main_non_adreno_title">Adreno GPU नहीं है</string> + <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> + <string name="main_applied">लागू हुआ: %1$s</string> + <string name="android_app_open_failed_primary">%1$s प्राइमरी स्क्रीन पर नहीं खुल सका</string> + <string name="android_app_open_failed_on">%1$s %2$s पर नहीं खुल सका</string> + <string name="android_app_open_failed_display">%1$s डिस्प्ले %2$d पर नहीं खुल सका</string> + <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> + <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> + <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> + <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> + <string name="main_running_program">प्रोग्राम</string> + <string name="main_removing_runtime">Linux रनटाइम हटाया जा रहा है</string> + <string name="main_settings_main_screen">नीचे की स्क्रीन पर Settings नहीं खुल सकी; इसे मुख्य स्क्रीन पर खोला गया।</string> + <string name="main_open_dev_options">Settings में डेवलपर विकल्प खोलें</string> + + <string name="upd_unknown_apps_failed">Android की “Install unknown apps” सेटिंग नहीं खुल सकी</string> + <string name="upd_couldnt_check">जांच नहीं हो सकी: %1$s</string> + <string name="upd_no_download">इस बिल्ड में DroidDeck की इस कॉपी के लिए कोई डाउनलोड नहीं है</string> + <string name="upd_stop_first">पहले चल रहा सेशन बंद करें, फिर अपडेट करें</string> + <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> + + <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> + + <string name="drivers_not_checked">अभी जांचा नहीं गया - नए ड्राइवर देखने के लिए रीफ़्रेश पर टैप करें</string> + <string name="drivers_bundle_sets_display">Android + Linux: इसके साथ डिस्प्ले ड्राइवर सेट करता है</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: इसके साथ रनटाइम ड्राइवर सेट करता है</string> + <string name="drivers_runtime_builtin">रनटाइम में बिल्ट-इन Turnip</string> + <string name="drivers_system_vulkan">सिस्टम Vulkan: इस GPU के लिए कोई बंडल बिल्ड नहीं</string> + <string name="drivers_bundled">%1$s (बंडल)</string> + <string name="drivers_manual">मैनुअल</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">अभी सिर्फ़ एक आधा हिस्सा पब्लिश है</string> + <string name="drivers_pair_size">दोनों के लिए %1$.0f MB</string> + <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> + <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> + <string name="drivers_switched_to">%1$s %2$s में बदल दिया गया।</string> + <string name="drivers_switched_to_restart">%1$s %2$s में बदल दिया गया। DroidDeck रीस्टार्ट होने के बाद डिस्प्ले ड्राइवर लागू होता है।</string> + <string name="drivers_using">%1$s %2$s इस्तेमाल हो रहा है।</string> + <string name="drivers_using_restart">%1$s %2$s इस्तेमाल हो रहा है। DroidDeck रीस्टार्ट होने के बाद डिस्प्ले ड्राइवर लागू होता है।</string> + <string name="drivers_not_a_driver">यह ड्राइवर zip नहीं है: न AdrenoTools ड्राइवर, न -Linux Turnip, न Android + Linux बंडल</string> + <string name="drivers_import_failed">इंपोर्ट विफल: %1$s</string> + <string name="drivers_imported_unnamed">इंपोर्ट किया गया ड्राइवर</string> + <string name="drivers_using_bundle">%1$s को रनटाइम और डिस्प्ले दोनों ड्राइवर के रूप में इस्तेमाल किया जा रहा है।</string> + <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> + <string name="drivers_release_status">नवीनतम: %1$s · %2$s को जांचा गया</string> + <string name="drivers_release_status_failed">नवीनतम: %1$s · %2$s तक नहीं पहुंचा जा सका · %3$s को जांचा गया</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">%1$d मिनट पहले</item> + <item quantity="other">%1$d मिनट पहले</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">%1$d घंटे पहले</item> + <item quantity="other">%1$d घंटे पहले</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">%1$d दिन पहले</item> + <item quantity="other">%1$d दिन पहले</item> + </plurals> + <string name="drivers_checking_sources">Banners-Turnip, WinNative और DroidDeck जांचे जा रहे हैं…</string> + <string name="drivers_check_failed">जांच विफल रही</string> + <string name="drivers_installed_pick_either">इंस्टॉल है %1$s - दोनों ड्राइवर सेट करने के लिए इसे किसी भी लिस्ट में चुनें</string> + <string name="drivers_installed_pick">इंस्टॉल है %1$s - इसे मेनू में चुनें</string> + + <string name="session_wait_removal">रनटाइम हटाना खत्म होने का इंतज़ार करें</string> + <string name="session_title_game">गेम</string> + <string name="session_touch_auto_touchpad">टचपैड</string> + <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> + <string name="session_named_paused">%1$s रुका हुआ है</string> + <string name="session_program_paused">प्रोग्राम रुका हुआ है</string> + <string name="session_starting_desktop">डेस्कटॉप शुरू हो रहा है</string> + <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> + <string name="session_runtime_downloading">Linux रनटाइम डाउनलोड हो रहा है</string> + <string name="session_runtime_downloading_mb">Linux रनटाइम डाउनलोड हो रहा है · %2$d में से %1$d MB</string> + <string name="session_runtime_checking">Linux रनटाइम जांचा जा रहा है</string> + <string name="session_runtime_unpacking">Linux रनटाइम अनपैक हो रहा है</string> + <string name="session_desktop_downloading">डेस्कटॉप डाउनलोड हो रहा है</string> + <string name="session_desktop_downloading_mb">डेस्कटॉप डाउनलोड हो रहा है · %2$d में से %1$d MB</string> + <string name="session_desktop_checking">डेस्कटॉप जांचा जा रहा है</string> + <string name="session_desktop_unpacking">डेस्कटॉप अनपैक हो रहा है</string> + <string name="session_proton_downloading">Proton Experimental (ARM64) डाउनलोड हो रहा है</string> + <string name="session_proton_downloading_mb">Proton Experimental (ARM64) डाउनलोड हो रहा है · %2$d में से %1$d MB</string> + <string name="session_proton_checking">Proton Experimental (ARM64) जांचा जा रहा है</string> + <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> + <string name="session_desktop_install_failed">डेस्कटॉप इंस्टॉल नहीं हुआ (%1$s)। कनेक्शन जांचें और फिर से Desktop दबाएं।</string> + <string name="session_ended_named">%1$s अचानक बंद हो गया। बग रिपोर्ट के साथ लॉग शेयर करें, या फिर कोशिश करें।</string> + <string name="session_ended_program">प्रोग्राम अचानक बंद हो गया। बग रिपोर्ट के साथ लॉग शेयर करें, या फिर कोशिश करें।</string> + <string name="session_ended_desktop">डेस्कटॉप अचानक बंद हो गया। बग रिपोर्ट के साथ लॉग शेयर करें, या फिर कोशिश करें।</string> + <string name="session_ended_detail">एग्ज़िट स्टेटस %1$d · %2$s</string> + <string name="session_ended_no_log">कोई लॉग नहीं</string> + <plurals name="session_hint_enosys"> + <item quantity="one">लॉग में %1$d “Function not implemented” एरर दिखता है: इस डिवाइस पर proot का seccomp एक्सेलरेशन एक हेल्पर को फेल कर रहा है। %2$s → “%3$s” (या “%4$s”) आज़माएं और फिर से शुरू करें।</item> + <item quantity="other">लॉग में %1$d “Function not implemented” एरर दिखते हैं: इस डिवाइस पर proot का seccomp एक्सेलरेशन एक हेल्पर को फेल कर रहा है। %2$s → “%3$s” (या “%4$s”) आज़माएं और फिर से शुरू करें।</item> + </plurals> + + <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> + <string name="store_cat_games">गेम</string> + <string name="store_cat_audio_video">ऑडियो और वीडियो</string> + <string name="store_cat_graphics">ग्राफ़िक्स</string> + <string name="store_cat_internet">इंटरनेट</string> + <string name="store_cat_office">ऑफ़िस</string> + <string name="store_cat_developer">डेवलपर</string> + <string name="store_cat_education">शिक्षा</string> + <string name="store_cat_science">विज्ञान</string> + <string name="store_cat_system">सिस्टम</string> + <string name="store_cat_utilities">यूटिलिटी</string> + <string name="store_section_popular">पॉपुलर</string> + <string name="store_section_trending">ट्रेंडिंग</string> + <string name="store_section_new">नया</string> + <string name="store_section_updated">अपडेट किए गए</string> + <string name="store_section_loading">लोड हो रहा है…</string> + <string name="store_flatpak_setup">Flatpak सेटअप</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Flathub पर अपडेट नहीं जांचे जा सके</string> + <string name="content_rom_installed">इंस्टॉल है</string> + <string name="content_rom_folder">फ़ोल्डर</string> + <string name="theme_graphite">ग्रेफ़ाइट</string> + <string name="theme_graphite_detail">ग्रेफ़ाइट सतहें, और चुने, फ़ोकस या दबाए गए हर चीज़ के लिए लोगो के अपने ऑर्ब ब्लू रंग का इस्तेमाल।</string> + <string name="theme_paper">काले पर पेपर</string> + <string name="theme_paper_detail">सफ़ेद मुख्य रंग है, आइकन का ब्लू रंग लाइन, रिंग और डॉट के लिए संकेत है।</string> + <string name="theme_icon_blue">आइकन ब्लू</string> + <string name="theme_icon_blue_detail">काली ज़मीन, सफ़ेद टाइप, चुने, फ़ोकस या दबाए गए हर चीज़ के लिए ब्लू रंग।</string> + <string name="theme_electric">इलेक्ट्रिक नेवी</string> + <string name="theme_electric_detail">ब्लू रंग ज़्यादा चमकीला, उन सतहों पर जो ग्रे के बजाय नेवी की ओर झुकती हैं।</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Space</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <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> + <string name="content_system_many">कई सिस्टम</string> + <string name="fm_drive_internal">इंटरनल</string> + <string name="fm_drive_linux">Linux सिस्टम</string> + <string name="fm_drive_sd">SD कार्ड</string> + <string name="fm_drive_storage">स्टोरेज</string> + <string name="fm_select">चुनें</string> + <string name="fm_properties">प्रॉपर्टी</string> + <string name="fm_favorite_add">Favorites में जोड़ें</string> + <string name="fm_favorite_remove">Favorites से हटाएं</string> + <string name="fm_rename">नाम बदलें</string> + <string name="fm_copy">कॉपी करें</string> + <string name="fm_cut">कट करें</string> + <string name="fm_actions">एक्शन</string> + <string name="fm_favorites">Favorites</string> + <string name="fm_pin_current">मौजूदा फ़ोल्डर पिन करें</string> + <string name="fm_no_favorites">अभी कोई favorite नहीं है - किसी फ़ोल्डर को उसके ⋮ मेनू से पिन करें ताकि यहां जल्दी वापस आ सकें।</string> + <string name="fm_unpin_favorite">favorites से हटाएं</string> + <string name="fm_size_zero">0 बाइट</string> + <string name="fm_unit_bytes">बाइट</string> + <string name="fm_prop_location">जगह</string> + <string name="fm_prop_type">टाइप</string> + <string name="fm_prop_size">आकार</string> + <string name="fm_prop_modified">बदला गया</string> + <string name="fm_type_folder">फ़ोल्डर</string> + <string name="fm_type_file">फ़ाइल</string> + <string name="fm_type_ext_file">%1$s फ़ाइल</string> + <string name="fm_read_only">रीड-ओनली</string> + <string name="fm_read_only_hint">गेम (और Wine) को इस फ़ाइल को ओवरराइट या डिलीट करने से रोकता है।</string> + <string name="fm_read_only_failed">रीड-ओनली नहीं बदला जा सका</string> + <string name="fm_hidden">Hidden</string> + <string name="fm_hidden_unsupported">यह स्टोरेज hidden फ़्लैग सेव नहीं कर सकता।</string> + <string name="fm_hidden_hint">फ़ाइल को Windows में hidden के तौर पर चिह्नित करता है (Wine का hidden एट्रिब्यूट)।</string> + <string name="fm_hidden_failed">इस स्टोरेज पर Hidden नहीं बदला जा सका</string> + <string name="fm_done">हो गया</string> + <string name="fm_op_deleting">हटाया जा रहा है...</string> + <string name="fm_delete_failed">हटाना विफल रहा</string> + <string name="fm_op_moving">ले जाया जा रहा है...</string> + <string name="fm_op_copying">कॉपी हो रहा है...</string> + <string name="fm_op_moving_item">ले जाया जा रहा है - %1$s</string> + <string name="fm_op_copying_item">कॉपी हो रहा है - %1$s</string> + <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> + <string name="fm_paste_failed">%1$d पूरा, %2$d विफल</string> + <string name="fm_paste_skipped">%1$d पूरा, %2$d छोड़ा गया</string> + <plurals name="fm_paste_moved"> + <item quantity="one">%1$d आइटम ले जाया गया</item> + <item quantity="other">%1$d आइटम ले जाए गए</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">%1$d आइटम कॉपी हुआ</item> + <item quantity="other">%1$d आइटम कॉपी हुए</item> + </plurals> + <string name="fm_already_exists">“%1$s” पहले से मौजूद है</string> + <string name="fm_op_renaming">नाम बदला जा रहा है...</string> + <string name="fm_rename_failed">नाम बदलना विफल रहा</string> + <string name="fm_op_creating_folder">फ़ोल्डर बनाया जा रहा है...</string> + <string name="fm_create_folder_failed">फ़ोल्डर नहीं बन सका</string> + <string name="fm_new_folder">नया फ़ोल्डर</string> + <string name="fm_folder_name">फ़ोल्डर का नाम</string> + <string name="fm_create">बनाएं</string> + <string name="fm_new_name">नया नाम</string> + <string name="fm_delete_title">हटाएं?</string> + <string name="fm_delete_text">“%1$s” को स्थायी रूप से हटाएं?</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">%1$d आइटम हटाएं?</item> + <item quantity="other">%1$d आइटम हटाएं?</item> + </plurals> + <string name="fm_cannot_undo">यह पूर्ववत नहीं हो सकता।</string> + <plurals name="fm_and_more"> + <item quantity="one">…और %1$d और</item> + <item quantity="other">…और %1$d और</item> + </plurals> + <string name="fm_op_deleting_batch">हटाया जा रहा है %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">%1$d नहीं हटाया जा सका</item> + <item quantity="other">%1$d नहीं हटाए जा सके</item> + </plurals> + <string name="fm_conflict_merge_hint">मर्ज मौजूदा फ़ोल्डर के भीतर फ़ाइलें जोड़ता और बदलता है।</string> + <string name="fm_conflict_overwrite_hint">ओवरराइट मौजूदा फ़ाइल को बदल देता है।</string> + <string name="fm_conflict_apply_all">सभी कॉन्फ़्लिक्ट पर लागू करें</string> + <string name="fm_conflict_merge">मर्ज करें</string> + <string name="fm_conflict_overwrite">ओवरराइट करें</string> + <string name="fm_conflict_keep_both">दोनों रखें</string> + <string name="fm_conflict_skip">छोड़ें</string> + <string name="fm_select_this_folder">यह फ़ोल्डर चुनें</string> + <string name="fm_back">बैक</string> + <string name="fm_drive_not_readable">%1$s माउंट है लेकिन अभी पढ़ा नहीं जा सकता</string> + <string name="fm_list_view">लिस्ट व्यू</string> + <string name="fm_grid_view">ग्रिड व्यू</string> + <string name="fm_search">सर्च</string> + <string name="fm_sort">सॉर्ट</string> + <string name="fm_sort_name">नाम</string> + <string name="fm_sort_date">बदलने की तारीख</string> + <string name="fm_rows_comfortable">आरामदायक पंक्तियां</string> + <string name="fm_rows_compact">कॉम्पैक्ट पंक्तियां</string> + <string name="fm_hide_hidden">छिपी फ़ाइलें छिपाएं</string> + <string name="fm_show_hidden">छिपी फ़ाइलें दिखाएं</string> + <string name="fm_hide_favorites">favorites छिपाएं</string> + <string name="fm_show_favorites">favorites दिखाएं</string> + <string name="fm_filter_folder">इस फ़ोल्डर को फ़िल्टर करें</string> + <string name="fm_free_space">%1$s खाली</string> + <string name="fm_selected_count">%1$d चुने गए</string> + <string name="fm_select_none">कोई नहीं</string> + <string name="fm_select_all">सभी</string> + <string name="fm_paste">पेस्ट करें</string> + <plurals name="fm_items"> + <item quantity="one">%1$d आइटम</item> + <item quantity="other">%1$d आइटम</item> + </plurals> + <string name="fm_paste_here">%1$s यहां पेस्ट करें</string> + <string name="fm_paste_move_here">%1$s (मूव) यहां पेस्ट करें</string> + <string name="fm_rail_title">फ़ाइलें</string> + <string name="fm_rail_storage">स्टोरेज</string> + <string name="fm_rail_quick">क्विक</string> + <string name="fm_rail_favorites">FAVORITES</string> + <string name="fm_rail_downloads">डाउनलोड</string> + <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> + <string name="fm_favorite_added">“%1$s” को Favorites में जोड़ा गया</string> + <string name="fm_favorite_removed">“%1$s” को Favorites से हटाया गया</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> + <string name="ctrl_tint_steam_blue">Steam ब्लू</string> + <string name="ctrl_tint_sky">स्काई</string> + <string name="ctrl_tint_white">सफ़ेद</string> + <string name="ctrl_tint_violet">वायलेट</string> + <string name="ctrl_tint_green">हरा</string> + <string name="ctrl_tint_amber">एम्बर</string> + <string name="ctrl_tint_red">लाल</string> + <string name="ctrl_tint_pink">गुलाबी</string> + <string name="ctrl_button_a">A बटन</string> + <string name="ctrl_button_b">B बटन</string> + <string name="ctrl_button_x">X बटन</string> + <string name="ctrl_button_y">Y बटन</string> + <string name="ctrl_button_lb">लेफ़्ट बंपर</string> + <string name="ctrl_button_rb">राइट बंपर</string> + <string name="ctrl_button_lt">लेफ़्ट ट्रिगर</string> + <string name="ctrl_button_rt">राइट ट्रिगर</string> + <string name="ctrl_button_view">View बटन</string> + <string name="ctrl_button_menu">Menu बटन</string> + <string name="ctrl_target_view">View</string> + <string name="ctrl_target_menu">Menu</string> + <string name="ctrl_target_up">D-pad ऊपर</string> + <string name="ctrl_target_down">D-pad नीचे</string> + <string name="ctrl_target_left">D-pad बायां</string> + <string name="ctrl_target_right">D-pad दायां</string> + <string name="ctrl_target_hidden">Hidden</string> + <string name="ctrl_editor_drag">ले जाने के लिए ड्रैग करें</string> + <string name="ctrl_editor_snap">स्नैप</string> + <string name="ctrl_editor_save">सेव करें</string> + <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> + + <string name="rtinst_removing">Linux रनटाइम हटाया जा रहा है</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">Linux रनटाइम हटाया जा रहा है · %1$d एंट्री</item> + <item quantity="other">Linux रनटाइम हटाया जा रहा है · %1$d एंट्री</item> + </plurals> + <string name="rtinst_removed">Linux रनटाइम हटाया गया</string> + <string name="rtinst_removal_failed">Linux रनटाइम हटाना पूरा नहीं हो सका। सेटअप में फिर से हटाने की कोशिश करें।</string> + <string name="rtinst_removing_leftovers">रनटाइम का बाकी हिस्सा हटाया जा रहा है</string> + <string name="rtinst_downloading">डाउनलोड हो रहा है</string> + <string name="rtinst_verifying">जांचा जा रहा है</string> + <string name="rtinst_extracting">एक्सट्रैक्ट हो रहा है</string> + <string name="rtinst_channel_description">Linux रनटाइम इंस्टॉल या हटाए जाते समय दिखता है</string> + <string name="rtinst_title_installing">Linux रनटाइम इंस्टॉल हो रहा है</string> + <string name="rtinst_title_removing">Linux रनटाइम हटाया जा रहा है</string> + + <string name="deskpkg_runtime_missing">Linux रनटाइम इंस्टॉल नहीं है</string> + <string name="deskpkg_no_checksum">कैटलॉग में %1$s के लिए कोई चेकसम नहीं है</string> + <string name="deskpkg_checksum_mismatch">चेकसम मेल नहीं खाया - कुछ नहीं बदला गया</string> + <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> + + <string name="flatpakmgr_setup_failed">Flatpak सेटअप विफल रहा (exit %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 status %1$d के साथ बंद हुआ</string> + <string name="flatpakmgr_installing">%1$s इंस्टॉल हो रहा है</string> + <string name="flatpakmgr_updating">%1$s अपडेट हो रहा है</string> + <string name="flatpakmgr_removing">%1$s हटाया जा रहा है</string> + <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> + <string name="flatpakmgr_ref_translations">अनुवाद</string> + <string name="flatpakmgr_ref_graphics_drivers">ग्राफ़िक्स ड्राइवर</string> + <string name="flatpakmgr_ref_media_codecs">मीडिया कोडेक</string> + <string name="flatpakmgr_ref_runtime">%1$s रनटाइम</string> + <string name="flatpakmgr_ref_runtime_branch">%1$s रनटाइम %2$s</string> + + <string name="appimg_too_small">यह फ़ाइल AppImage होने के लिए बहुत छोटी है</string> + <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> + <string name="appimg_no_space_import">पर्याप्त खाली जगह नहीं है: इंपोर्ट के लिए लगभग %1$s चाहिए</string> + <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> + <string name="appimg_extracting">%1$s एक्सट्रैक्ट हो रहा है</string> + <string name="appimg_extract_failed">AppImage एक्सट्रैक्ट नहीं हो सका</string> + <string name="appimg_extract_failed_detail">AppImage एक्सट्रैक्ट नहीं हो सका: %1$s</string> + + <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> + <string name="deckymgr_checksum_download_failed">रिलीज़ चेकसम डाउनलोड नहीं हो सका</string> + <string name="deckymgr_checksum_missing">रिलीज़ चेकसम मौजूद नहीं है</string> + <string name="deckymgr_checksum_invalid">रिलीज़ चेकसम मान्य नहीं है</string> + <string name="deckymgr_checksum_mismatch">Decky Loader चेकसम मेल नहीं खाया</string> + <string name="deckymgr_not_elf">रिलीज़ asset एक कम्पैटिबल 64-बिट Linux एग्ज़िक्यूटेबल नहीं है</string> + <string name="deckymgr_session_started_loader">डाउनलोड के दौरान एक सेशन शुरू हुआ; Decky Loader इंस्टॉल करने से पहले इसे बंद करें</string> + <string name="deckymgr_loader_not_executable">PluginLoader को एग्ज़िक्यूटेबल के तौर पर चिह्नित नहीं किया जा सका</string> + <string name="deckymgr_cef_failed">Steam CEF रिमोट डिबगिंग चालू नहीं हो सकी</string> + <string name="deckymgr_stage_failed">PluginLoader स्टेज नहीं किया जा सका</string> + <string name="deckymgr_loader_preserve_failed">मौजूदा PluginLoader को सुरक्षित नहीं रखा जा सका</string> + <string name="deckymgr_loader_activate_failed">नया PluginLoader चालू नहीं हो सका</string> + <string name="deckymgr_stop_session_plugin">प्लगइन इंस्टॉल करने से पहले चल रहा सेशन बंद करें</string> + <string name="deckymgr_loader_required">प्लगइन इंपोर्ट करने से पहले Decky Loader इंस्टॉल करें</string> + <string name="deckymgr_zip_unreadable">चुनी गई ZIP फ़ाइल नहीं पढ़ी जा सकी</string> + <string name="deckymgr_plugins_dir_failed">Decky प्लगइन डायरेक्टरी नहीं बन सकी</string> + <string name="deckymgr_extracting_zip">प्लगइन ZIP एक्सट्रैक्ट हो रहा है</string> + <string name="deckymgr_zip_one_folder">प्लगइन ZIP में एक टॉप-लेवल प्लगइन फ़ोल्डर होना चाहिए</string> + <string name="deckymgr_zip_missing_files">प्लगइन ZIP में plugin.json, package.json, या dist/index.js नहीं है</string> + <string name="deckymgr_plugin_name_missing">प्लगइन का नाम मौजूद नहीं है</string> + <string name="deckymgr_session_started_plugin">इंस्टॉलेशन के दौरान एक सेशन शुरू हुआ; प्लगइन इंस्टॉल करने से पहले इसे बंद करें</string> + <string name="deckymgr_plugin_installed">प्लगइन इंस्टॉल हो गया · इसे लोड करने के लिए Steam रीस्टार्ट करें</string> + <string name="deckymgr_plugin_install_failed">प्लगइन ZIP इंस्टॉलेशन विफल रहा</string> + <string name="deckymgr_zip_too_many">प्लगइन ZIP में बहुत ज़्यादा एंट्री हैं</string> + <string name="deckymgr_zip_invalid_path">प्लगइन ZIP में एक अमान्य पाथ है</string> + <string name="deckymgr_zip_duplicate_paths">प्लगइन ZIP में डुप्लीकेट पाथ हैं</string> + <string name="deckymgr_zip_unsafe_path">प्लगइन ZIP में एक असुरक्षित पाथ है</string> + <string name="deckymgr_folder_failed">प्लगइन फ़ोल्डर नहीं बन सका</string> + <string name="deckymgr_zip_too_large">प्लगइन ZIP 1 GiB की सीमा से आगे बढ़ जाता है</string> + <string name="deckymgr_bundled_not_executable">बंडल किए प्लगइन बाइनरी को एग्ज़िक्यूटेबल के तौर पर चिह्नित नहीं किया जा सका</string> + <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> + <string name="deckymgr_remote_hash_invalid">रिमोट बाइनरी %1$s का SHA-256 हैश मान्य नहीं है</string> + <string name="deckymgr_bin_dir_failed">प्लगइन bin डायरेक्टरी नहीं बन सकी</string> + <string name="deckymgr_downloading_binary">प्लगइन बाइनरी डाउनलोड हो रही है · %1$s</string> + <string name="deckymgr_binary_download_failed">प्लगइन बाइनरी %1$s डाउनलोड नहीं हो सकी</string> + <string name="deckymgr_binary_checksum_mismatch">प्लगइन बाइनरी चेकसम मेल नहीं खाया · %1$s</string> + <string name="deckymgr_binary_not_executable">प्लगइन बाइनरी को एग्ज़िक्यूटेबल के तौर पर चिह्नित नहीं किया जा सका · %1$s</string> + <string name="deckymgr_binary_replace_failed">प्लगइन बाइनरी बदली नहीं जा सकी · %1$s</string> + <string name="deckymgr_binary_install_failed">प्लगइन बाइनरी इंस्टॉल नहीं हो सकी · %1$s</string> + <string name="deckymgr_folder_taken">प्लगइन फ़ोल्डर किसी और प्लगइन का है</string> + <string name="deckymgr_preserve_failed">मौजूदा प्लगइन सुरक्षित नहीं रखा जा सका</string> + <string name="deckymgr_activate_failed">इंपोर्ट किए प्लगइन को चालू नहीं किया जा सका</string> + <string name="deckymgr_settings_failed">Decky प्लगइन सेटिंग अपडेट नहीं हो सकी</string> + <string name="deckymgr_restore_failed">मौजूदा प्लगइन वापस नहीं लाया जा सका</string> + + <string name="adbfix_save_failed">वायरलेस डीबगिंग पहचान सेव नहीं हो सकी</string> + <string name="adbfix_command_failed">ADB कमांड विफल रहा (%1$d)</string> + <string name="adbfix_not_confirmed">Android ने पुष्टि नहीं की कि चाइल्ड-प्रोसेस सीमा बदली गई: %1$s</string> + <string name="adbfix_no_pairing">कोई वायरलेस डीबगिंग पेयरिंग सेव नहीं है</string> + <string name="adbfix_enter_address">मौजूदा वायरलेस डीबगिंग का IP एड्रेस और पोर्ट डालें</string> + + <string name="adbpair_timed_out">पेयरिंग पॉप-अप का इंतज़ार करते-करते समय खत्म हो गया। DroidDeck से फिर कोशिश करें।</string> + <string name="adbpair_discovery_failed_code">पेयरिंग पॉप-अप नहीं खोजा जा सका (%1$d)। जांचें कि Wi-Fi चालू है।</string> + <string name="adbpair_discovery_failed">पेयरिंग पॉप-अप नहीं खोजा जा सका: %1$s</string> + <string name="adbpair_six_digits">कोड के सभी 6 अंक डालें</string> + <string name="adbpair_pairing">पेयरिंग हो रही है…</string> + <string name="adbpair_code_rejected">यह कोड काम नहीं आया। पेयरिंग पॉप-अप फिर से खोलें और इसका नया कोड डालें।</string> + <string name="adbpair_connecting">पेयर हो गया। कनेक्ट हो रहा है…</string> + <string name="adbpair_port_not_found">पेयर हो गया, लेकिन वायरलेस डीबगिंग का पोर्ट नहीं मिला। वायरलेस डीबगिंग चालू रखें और फिर कोशिश करें।</string> + <string name="adbpair_applying">सेटिंग लागू की जा रही है…</string> + <string name="adbpair_command_failed">वायरलेस डीबगिंग कमांड विफल रहा</string> + <string name="adbpair_channel_name">वायरलेस डीबगिंग सेटअप</string> + <string name="adbpair_channel_description">Settings खुली रहते हुए वायरलेस डीबगिंग का पेयरिंग कोड मांगता है</string> + <string name="adbpair_title_pair">वायरलेस डीबगिंग पेयर करें</string> + <string name="adbpair_waiting_text">वायरलेस डीबगिंग चालू करें, फिर “Pair device with pairing code” पर टैप करें।</string> + <string name="adbpair_waiting_long">वायरलेस डीबगिंग चालू करें, फिर “Pair device with pairing code” पर टैप करें। Settings में बने रहें: कोड इस नोटिफ़िकेशन में आता है।</string> + <string name="adbpair_code_label">6 अंकों का पेयरिंग कोड</string> + <string name="adbpair_code_hint">Enter code पर टैप करें और Settings में दिखाए गए 6 अंक टाइप करें।</string> + <string name="adbpair_title_code">Wi-Fi पेयरिंग कोड डालें</string> + <string name="adbpair_title_retry">कोड फिर से आज़माएं</string> + <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> + + <string name="selfinst_http">डाउनलोड ने HTTP %1$d जवाब दिया</string> + <string name="selfinst_size_mismatch">डाउनलोड का आकार पब्लिश किए APK से मेल नहीं खाया और इसे हटा दिया गया - फिर कोशिश करें</string> + <string name="selfinst_checksum_mismatch">डाउनलोड अपने चेकसम से मेल नहीं खाया और इसे हटा दिया गया - फिर कोशिश करें</string> + <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> + <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 इंस्टॉल किए %2$d के ऊपर versionCode %1$d इंस्टॉल नहीं करेगा</string> + <string name="selfinst_no_signing">डाउनलोड किए APK में कोई साइनिंग जानकारी नहीं है</string> + <string name="selfinst_wrong_signer">डाउनलोड किया APK DroidDeck की रिलीज़ कुंजी से साइन नहीं है</string> + <string name="appupd_http">अपडेट कैटलॉग ने HTTP %1$d जवाब दिया</string> + + <string name="loadstate_starting">सेशन शुरू हो रहा है…</string> + <string name="loadstate_elapsed">%1$d:%2$02d बीत गया · अभी भी काम हो रहा है</string> + <string name="loadstate_steam_update">Steam क्लाइंट अपडेट डाउनलोड हो रहा है · %1$d%%</string> + <string name="loadstate_steam_client">Steam क्लाइंट डाउनलोड हो रहा है · %1$s</string> + + <string name="pextra_busy">एक कम्पैटिबिलिटी टूल इंस्टॉल पहले से चल रहा है</string> + <string name="pextra_stop_session">कम्पैटिबिलिटी टूल इंस्टॉल करने से पहले चल रहा सेशन बंद करें</string> + <string name="pextra_no_release">%1$s के लिए कोई ARM64 रिलीज़ नहीं मिली</string> + <string name="pextra_no_space">%1$s इंस्टॉल करने के लिए कम से कम 4 GB खाली जगह और डाउनलोड आकार चाहिए</string> + <string name="pextra_downloading">%1$s %2$s डाउनलोड हो रहा है</string> + <string name="pextra_retrying">डाउनलोड फिर से कोशिश हो रहा है · %2$d में से %1$d प्रयास</string> + <string name="pextra_interrupted">डाउनलोड बाधित हुआ</string> + <string name="pextra_download_failed">डाउनलोड विफल रहा; आंशिक फ़ाइल रखी गई है ताकि फिर कोशिश करने पर यह जारी रह सके</string> + <string name="pextra_too_big">डाउनलोड रिलीज़ के आकार से बड़ा हो गया और हटा दिया गया</string> + <string name="pextra_incomplete">डाउनलोड अधूरा रहा; आंशिक फ़ाइल को फिर से जारी रखने लायक कोशिश के लिए रखा गया है</string> + <string name="pextra_verifying">डाउनलोड जांचा जा रहा है</string> + <string name="pextra_checksum_unreadable">रिलीज़ चेकसम नहीं पढ़ा जा सका; फिर कोशिश करें</string> + <string name="pextra_no_checksum">%1$s %2$s कोई चेकसम पब्लिश नहीं करता; कुछ भी इंस्टॉल नहीं हुआ</string> + <string name="pextra_checksum_mismatch">चेकसम मेल नहीं खाया; डाउनलोड हटा दिया गया</string> + <string name="pextra_session_started">डाउनलोड के दौरान एक सेशन शुरू हुआ; कम्पैटिबिलिटी टूल इंस्टॉल करने से पहले इसे बंद करें</string> + <string name="pextra_installing">%1$s इंस्टॉल हो रहा है</string> + <string name="pextra_install_failed">%1$s इंस्टॉलेशन विफल रहा (exit %2$d)</string> + <string name="pextra_could_not_install">%1$s इंस्टॉल नहीं हो सका</string> + <string name="esync_fetching">droiddeck-esync पैक लिया जा रहा है</string> + + <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">Components के बाहर बदला गया</string> + <string name="cmgr_not_present">मौजूद नहीं</string> + <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> + <string name="cmgr_restore_queued">जब इस पर चल रहा गेम बंद होगा, तो %2$s का %1$s अपने ओरिजिनल पर वापस चला जाएगा।</string> + <string name="cmgr_restore_done">%2$s का %1$s अपने %3$s ओरिजिनल से वापस लाया गया।</string> + <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> + <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> + + <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> + + <string name="look_off_desc">कुछ लागू नहीं।</string> + <string name="look_game_clarity">गेम क्लैरिटी</string> + <string name="look_game_clarity_desc">थोड़े कंट्रास्ट के साथ शार्पनिंग - रोज़ के लिए वाला।</string> + <string name="look_vivid">विविड</string> + <string name="look_vivid_desc">रंग को ज़ोर से बढ़ाया गया। आसमान में पट्टियां न आएं इसलिए डीबैंड चालू।</string> + <string name="look_cinematic">सिनेमैटिक</string> + <string name="look_cinematic_desc">गहरा कंट्रास्ट, ठंडा रंग, स्मूथ किए गए एज।</string> + <string name="look_competitive">कॉम्पिटिटिव</string> + <string name="look_competitive_desc">शैडो उठाए गए और भारी शार्पनिंग - पहले लोगों को पहचानें।</string> + <string name="look_adaptive_sharpen">एडैप्टिव शार्पन</string> + <string name="look_adaptive_sharpen_desc">सिर्फ़ शार्पनिंग, रंग जैसा है वैसा।</string> + <string name="look_filmic">फ़िल्मिक</string> + <string name="look_filmic_desc">फ़िल्म जैसा कर्व, थोड़ा हल्का रंग।</string> + <string name="look_arcade">आर्केड</string> + <string name="look_arcade_desc">तेज़ और दमदार।</string> + <string name="look_retro_crt">रेट्रो CRT</string> + <string name="look_retro_crt_desc">CRT ट्यूब और एनालॉग सिग्नल का असर एक साथ।</string> + <string name="look_upscale_sharp">अपस्केल शार्प</string> + <string name="look_upscale_sharp_desc">पैनल रिज़ॉल्यूशन से कम पर चलाने के लिए।</string> + <string name="look_pixel_clean">पिक्सल क्लीन</string> + <string name="look_pixel_clean_desc">2D और पुराने गेम के लिए साफ़ पिक्सल।</string> + <string name="look_anime_edge">एनिमे एज</string> + <string name="look_anime_edge_desc">टून आउटलाइन के साथ शार्पनिंग।</string> + + <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> + + <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> + <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> + <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> + <string name="lvd_no_library">इस zip में कोई libvulkan_freedreno*.so नहीं है। Android (AdrenoTools) या -Wayland Turnip zip Linux रनटाइम ड्राइवर नहीं है।</string> + <string name="lvd_not_aarch64">%1$s एक 64-बिट AArch64 ELF शेयर्ड लाइब्रेरी नहीं है।</string> + <string name="lvd_not_glibc">%1$s glibc ड्राइवर नहीं है - यह Android की libc से जुड़ा है। Linux रनटाइम को एक “-Linux” zip चाहिए; सादा या “-Wayland” Turnip Steam क्लाइंट द्वारा बिल्कुल भी लोड नहीं हो सकता।</string> + <string name="bundle_no_manifest">यह ड्राइवर बंडल नहीं है: कोई manifest.json नहीं जो एक android और एक linux ड्राइवर का नाम ले</string> + <string name="bundle_entry_outside">यह ड्राइवर बंडल नहीं है: %1$s इसके बाहर इशारा करता है</string> + <string name="bundle_too_large">यह ड्राइवर बंडल नहीं है: यह %1$d MB से ज़्यादा में अनपैक होता है</string> + + <string name="gstore_sd_card">SD कार्ड</string> + <string name="gstore_folder">फ़ोल्डर</string> + + <string name="logshare_chooser">सेशन लॉग शेयर करें</string> + + <string name="hdr_needs_android_10">Android 10 या नया चाहिए (कलर स्पेस वाली डिस्प्ले लेयर)</string> + <string name="hdr_none">यह डिस्प्ले HDR सपोर्ट नहीं बताता</string> + <string name="hdr_not_hdr10">यह डिस्प्ले %1$s को सपोर्ट करता है, HDR10 को नहीं</string> + + <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> + <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">डेवलपर विकल्प में “Disable child process restrictions” चालू करें, और बाद में डेवलपर विकल्प चालू रहने दें।</string> + <string name="phantom_fix_no_toggle">इस Android वर्शन में इसके लिए कोई स्विच नहीं है। इसे इस डिवाइस से बदलने के लिए वायरलेस डीबगिंग इस्तेमाल करें।</string> + <string name="upd_block_no_apk">इस बिल्ड में DroidDeck की इस कॉपी के लिए कोई डाउनलोड नहीं है।</string> + <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> + <string name="upd_fail_with_message">अपडेट इंस्टॉल नहीं हुआ: %1$s</string> + <string name="upd_fail_generic">अपडेट इंस्टॉल नहीं हुआ।</string> + <string name="saves_layout_zip">%1$s zip</string> + <string name="saves_zip">zip</string> + <string name="saves_not_launched">%1$s अभी तक लॉन्च नहीं हुआ</string> + <string name="saves_none_found">%1$s के लिए कोई सेव फ़ाइल नहीं मिली</string> + <string name="saves_not_save_zip">%1$s में कोई drive_c फ़ोल्डर नहीं है, इसलिए यह GameHub या Winlator सेव zip नहीं है</string> + <string name="gstore_cannot_create">%1$s में फ़ोल्डर नहीं बनाए जा सकते</string> + <string name="gstore_cannot_write">%1$s में लिखा नहीं जा सकता</string> + <string name="gstore_cannot_write_detail">%1$s में लिखा नहीं जा सकता (%2$s)</string> + <string name="gfiles_folder_unavailable">फ़ोल्डर उपलब्ध नहीं है</string> + <string name="gfiles_name_taken">%1$s का उपयोग एक अलग फ़ाइल पहले से कर रही है</string> + <string name="gfiles_art_unavailable">आर्टवर्क फ़ोल्डर उपलब्ध नहीं है</string> + <string name="gfiles_unrelated_inventory">फ़ोल्डर में एक असंबंधित सिंक इन्वेंटरी है</string> + <string name="gfiles_remove_failed">%1$s हटाया नहीं जा सका</string> + <string name="gfiles_outside_folder">फ़ाइल एक्सपोर्ट फ़ोल्डर से बाहर चली जाती है</string> + <string name="cmgr_fex_unnamed">FEX (वर्शन नाम के बिना बिल्ड)</string> + <string name="cmgr_no_profile">%1$s में कोई profile.json नहीं</string> + <string name="cmgr_not_component_package">%1$s एक Linux कॉम्पोनेंट पैकेज नहीं है (टाइप %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s खाली</string> + <string name="mode_storage_auto_short">ऑटोमैटिक</string> + <string name="appupd_catalog_rejected">अपडेट कैटलॉग अपनी जांच में विफल रहा (%1$s)</string> + <string name="main_adb_clip_label">DroidDeck चाइल्ड-प्रोसेस सेटिंग</string> + <string name="setup_signed_in_offline_unnamed">साइन इन है · ऑफ़लाइन मोड</string> + <string name="setup_signed_in_unnamed">साइन इन है</string> + <string name="driver_tag_bundled">बंडल</string> + <string name="driver_tag_downloaded">डाउनलोड किया गया</string> + <string name="driver_tag_imported">इंपोर्ट किया गया</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s। %2$s\n\n%3$s</string> + <string name="display_session">सेशन डिस्प्ले</string> + <string name="display_resolution">रिज़ॉल्यूशन</string> + <string name="display_game_fit">16:9 गेम फ़िट</string> + <string name="display_game_fit_hint">16:9 गेम इस डिस्प्ले में कैसे फ़िट होते हैं। अगले सेशन से।</string> + <string name="display_game_fit_unavailable">Match screen चुनें या 16:9 से पतला कस्टम आकार चुनें।</string> + <string name="display_preserve">अनुपात बनाए रखें</string> + <string name="display_stretch">डिस्प्ले में स्ट्रेच करें</string> + <string name="display_image_scaling">इमेज स्केलिंग</string> + <string name="display_filter">स्केलिंग फ़िल्टर</string> + <string name="display_filter_hint">Steam और डेस्कटॉप में साझा।</string> + <string name="display_filter_live">लाइव लागू होता है। Steam और डेस्कटॉप में साझा।</string> + <string name="display_filter_note">FSR, GSR और NIS सेशन इमेज को बड़ा करने पर लागू होते हैं। सिर्फ़ शार्पन बिना बड़ा किए भी काम करता है।</string> + <string name="display_sharpness">फ़िल्टर शार्पनेस</string> + <string name="display_frame_rate">फ़्रेम रेट</string> + <string name="display_look_filter">%1$s स्केलिंग को %2$s पर सेट करता है।</string> + <string name="display_match_screen">Match screen (%1$d×%2$d)</string> + <string name="display_follow_screen">Follow screen (खुलने या बंद होने पर आकार बदलता है)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">डिवाइस जानकारी दिखाएं</string> + <string name="device_info_hide">डिवाइस जानकारी छिपाएं</string> + <string name="device_info_loading">डिवाइस पढ़ा जा रहा है…</string> + <string name="device_info_copy">कॉपी करें</string> + <string name="device_info_copy_hint">इस जानकारी को बग रिपोर्ट में कॉपी करें।</string> + <string name="device_info_copied">कॉपी हो गया।</string> + <string name="force_ssbs_title">SSBS फ़ोर्स करें</string> + <string name="force_ssbs_hint">एक CPU स्पीड सेटिंग चालू रखता है जिसे Wine बार-बार बंद कर देता है। Snapdragon 8 Elite पर Windows गेम तेज़ हो सकते हैं; पुराने चिप पर बहुत कम फ़र्क़ दिखता है। अगली बार गेम लॉन्च होने पर लागू होता है।</string> + + <string name="rail_stores">स्टोर</string> + <string name="source_steam">Steam</string> + <string name="source_added">कस्टम</string> + <string name="setup_stores">स्टोर</string> + <string name="setup_stores_open_on">स्टोर खोलें</string> + <string name="setup_stores_speed">डाउनलोड स्पीड टियर</string> + <string name="setup_stores_engine">स्टोर इंजन</string> + <string name="setup_stores_engine_chip_ready">● इंजन तैयार</string> + <string name="setup_stores_engine_chip_missing">नहीं बना</string> + <string name="stores_tier_balanced">बैलेंस्ड</string> + <string name="stores_tier_fast">फ़ास्ट</string> + <string name="stores_tier_max">मैक्स</string> + <string name="stores_title">स्टोर</string> + <string name="stores_settings">स्टोर सेटिंग</string> + <string name="stores_settings_eyebrow">सेटिंग</string> + <string name="stores_chip_signed_out">(साइन इन नहीं है)</string> + <string name="stores_tab_store">स्टोर</string> + <string name="stores_tab_installed">इंस्टॉल किए</string> + <string name="stores_tab_library">लाइब्रेरी</string> + <string name="stores_tab_all">सभी</string> + <string name="stores_tab_downloads">डाउनलोड</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">साइन इन करें</string> + <string name="stores_signout_named">%1$s · साइन आउट करें</string> + <string name="stores_signout_title">%1$s से साइन आउट करें?</string> + <string name="stores_signout_note">इंस्टॉल किए गेम बने रहते हैं। जब आप फिर से साइन इन करेंगे तो लाइब्रेरी फिर से मिलेगी।</string> + <string name="stores_signout_stay">साइन इन रहें</string> + <string name="stores_signout_go">साइन आउट करें</string> + <string name="stores_search_catalog">%1$s के कैटलॉग में सर्च करें…</string> + <string name="stores_search_installed">इंस्टॉल किए गेम में सर्च करें…</string> + <string name="stores_search_library">अपनी लाइब्रेरी में सर्च करें…</string> + <string name="stores_search_open">सर्च</string> + <string name="stores_installed_empty">कुछ इंस्टॉल नहीं है</string> + <string name="stores_library_loading">लोड हो रहा है</string> + <string name="stores_nothing_matches">कोई गेम नहीं</string> + <string name="stores_shelves_loading">स्टोर लोड हो रहा है…</string> + <string name="stores_amazon_no_catalog">कोई कैटलॉग नहीं</string> + <string name="stores_shelf_new">नया क्या है</string> + <string name="stores_shelf_deals">डील</string> + <string name="stores_shelf_free">फ़्री गेम</string> + <string name="stores_shelf_trending">ट्रेंडिंग</string> + <string name="stores_shelf_library">आपकी लाइब्रेरी</string> + <string name="stores_play">Play</string> + <string name="stores_install">इंस्टॉल करें</string> + <string name="stores_resume_install">इंस्टॉल फिर से शुरू करें</string> + <string name="stores_dl_interrupted">बाधित</string> + <string name="stores_install_size">इंस्टॉल · %1$s</string> + <string name="stores_get_free">मुफ़्त में पाएं</string> + <string name="stores_view_on">%1$s पर देखें</string> + <string name="stores_buy_price">खरीदें · %1$s</string> + <string name="stores_installed_chip">इंस्टॉल है</string> + <string name="stores_in_library">लाइब्रेरी में</string> + <string name="stores_free">फ़्री</string> + <string name="stores_resume">फिर से शुरू करें</string> + <string name="stores_refresh">रीफ़्रेश करें</string> + <string name="stores_show_mature">वयस्क कंटेंट दिखाएं</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">ऑफ़लाइन</string> + <string name="epic_card_none">बंद</string> + <string name="epic_eos">Epic साइन-इन (EOS)</string> + <string name="epic_offline">ऑफ़लाइन लॉन्च करें</string> + <string name="epic_resolve">Epic साइन-इन हल करें</string> + <string name="store_cloud_saves">क्लाउड सेव</string> + <string name="cloud_upload">क्लाउड पर अपलोड करें</string> + <string name="cloud_download">क्लाउड से डाउनलोड करें</string> + <string name="cloud_none">कोई क्लाउड सेव नहीं</string> + <string name="cloud_conflict">क्लाउड कॉन्फ़्लिक्ट</string> + <string name="cloud_conflict_files">क्लाउड कॉन्फ़्लिक्ट · %1$d फ़ाइलें</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">क्लाउड रखें</string> + <string name="cloud_keep_local">लोकल रखें</string> + <string name="cloud_checking">क्लाउड जांचा जा रहा है…</string> + <string name="cloud_last_sync">आख़िरी सिंक %1$s</string> + <string name="cloud_never">अभी सिंक नहीं हुआ</string> + <string name="cloud_done">%1$d फ़ाइलें</string> + <string name="stores_eyebrow_installed">इंस्टॉल है</string> + <string name="stores_eyebrow_library">आपकी लाइब्रेरी में</string> + <string name="stores_eyebrow_store">स्टोर</string> + <string name="achievements_title">उपलब्धियां</string> + <string name="achievements_count">%2$d में से %1$d अनलॉक</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">अनलॉक · %1$d</string> + <string name="achievements_group_locked">लॉक · %1$d</string> + <string name="achievements_group_hidden">छिपी · %1$d</string> + <string name="achievements_hidden">छिपी हुई उपलब्धि</string> + <string name="achievements_hidden_detail">अनलॉक होने पर विवरण दिखाए जाते हैं।</string> + <string name="achievements_all">सभी उपलब्धियां अनलॉक</string> + <string name="achievements_reading">लोड हो रहा है…</string> + <string name="achievements_signed_out">अपनी उपलब्धियां देखने के लिए %1$s में साइन इन करें।</string> + <string name="achievements_unreachable">%1$s तक नहीं पहुंच सका। ऑनलाइन होने पर फिर से देखें।</string> + <string name="achievements_none">%1$s इस गेम के लिए कोई उपलब्धि सूचीबद्ध नहीं करता।</string> + <string name="achievements_show_all">सभी %1$d दिखाएं</string> + <string name="achievements_dialog_title">उपलब्धियां · %1$s</string> + <string name="stores_in_steam">✓ Steam में</string> + <string name="stores_uninstall">अनइंस्टॉल करें</string> + <string name="stores_uninstalling">अनइंस्टॉल हो रहा है…</string> + <string name="stores_uninstall_title">%1$s अनइंस्टॉल करें?</string> + <string name="stores_uninstall_body_size">%1$s खाली करता है और इसे Steam से हटाता है।</string> + <string name="stores_uninstall_body">इसकी फ़ाइलें और इसका Steam शॉर्टकट हटाता है।</string> + <string name="stores_launch_not_registered">Steam अगली बार शुरू होने पर इस गेम को ले लेगा; इसे वहां से लॉन्च करें, या अभी Steam शुरू करें।</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">हर गेम के लिए Steam का चुनाव</string> + <string name="stores_card_audio">ऑडियो</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">कॉम्पोनेंट</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">इंजन</string> + <string name="stores_card_engine_value">%1$s · नेटिव</string> + <string name="stores_card_engine_java">%1$s · बिल्ट-इन</string> + <string name="stores_how_it_runs">यह कैसे चलता है</string> + <string name="stores_kv_fetched">कहां से लिया गया</string> + <string name="stores_kv_fetched_value">%1$s, आपके खाते से</string> + <string name="stores_kv_installed_to">कहां इंस्टॉल हुआ</string> + <string name="stores_kv_installed_to_value">गेम स्टोरेज (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">किससे लॉन्च होता है</string> + <string name="stores_kv_launched_by_value">Steam और Proton, Steam गेम वाला ही रास्ता</string> + <string name="stores_kv_launched_by_epic">Steam और Proton · हर लॉन्च पर Epic साइन-इन, EOS ओवरले बंद</string> + <string name="stores_kv_launched_by_amazon">Steam और Proton · Amazon Games Services वेरिएबल सेट</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">इंस्टॉल पर Steam में जोड़ा गया (Deck UI + Steam Input)</string> + <string name="stores_downloads_empty">कोई डाउनलोड नहीं</string> + <string name="stores_dl_paused">रुका हुआ</string> + <string name="stores_dl_queued">कतार में</string> + <string name="stores_dl_queued_at">कतार में · #%1$d</string> + <string name="stores_dl_cancelled">रद्द</string> + <string name="stores_dl_failed">विफल</string> + <string name="stores_dl_failed_reason">विफल · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">रोकें</string> + <string name="stores_dl_resume">फिर से शुरू करें</string> + <string name="stores_dl_cancel">रद्द करें</string> + <string name="stores_dl_retry">फिर कोशिश करें</string> + <string name="stores_dl_clear">हटाएं</string> + <string name="stores_dl_clear_all">पूरे हो गए हटाएं</string> + <string name="stores_stage_manifest">मेनिफ़ेस्ट</string> + <string name="stores_stage_download">डाउनलोड</string> + <string name="stores_stage_verify">जांचें</string> + <string name="stores_stage_install">इंस्टॉल</string> + <string name="stores_stage_done">पूरा हुआ</string> + <string name="stores_stage_checking">जांचा जा रहा है</string> + <string name="stores_stage_downloading">डाउनलोड हो रहा है</string> + <string name="stores_dl_cancel_confirm">डाउनलोड हटाएं?</string> + <string name="stores_dl_failed_kept">विफल · %1$s रखा गया</string> + <string name="stores_stage_verifying">जांचा जा रहा है</string> + <string name="stores_stage_installing">इंस्टॉल हो रहा है</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d फ़ाइलें</string> + <string name="stores_download_manager">डाउनलोड मैनेजर</string> + <string name="stores_parallel">एक साथ डाउनलोड</string> + <string name="stores_install_where">कहां इंस्टॉल करें</string> + <string name="stores_install_title">%1$s इंस्टॉल करें:</string> + <string name="stores_target_internal">इंटरनल</string> + <string name="stores_target_free">%1$s खाली</string> + <string name="stores_notification_channel">स्टोर डाउनलोड</string> + <string name="stores_notification_channel_hint">गेम डाउनलोड की प्रोग्रेस दिखाता है और डाउनलोड को बैकग्राउंड में चलते रहने देता है</string> + <string name="stores_notification_one">डाउनलोड हो रहा है</string> + <string name="stores_notification_many">%1$d डाउनलोड</string> + <string name="stores_notification_waiting">तैयार किया जा रहा है…</string> + <string name="stores_notification_done_channel">पूरे हुए स्टोर डाउनलोड</string> + <string name="stores_notification_installed">%1$s इंस्टॉल हुआ</string> + <string name="stores_notification_failed">%1$s डाउनलोड विफल रहा</string> + <string name="stores_notification_delete">हटाएं</string> + <string name="stores_notification_keep">रखें</string> + <string name="stores_login_title">%1$s में साइन इन करें</string> + <string name="stores_login_provider_title">अपने प्रोवाइडर से साइन इन करें</string> + <string name="stores_login_reload">फिर से लोड करें</string> + <string name="stores_login_close">बंद करें</string> + <string name="stores_login_retry">फिर कोशिश करें</string> + <string name="stores_login_finishing">साइन-इन पूरा हो रहा है…</string> + <string name="stores_login_error_network">साइन-इन पेज लोड नहीं हो सका: %1$s</string> + <string name="stores_login_error_idp_blocked">यह प्रोवाइडर एम्बेडेड पेज से साइन इन करने से मना करता है। इसके बदले अपने स्टोर पासवर्ड से साइन इन करें।</string> + <string name="stores_login_error_http">साइन-इन पेज ने एक एरर के साथ जवाब दिया (HTTP %1$d)।</string> + <string name="stores_login_error_ssl">साइन-इन पेज से कनेक्शन सुरक्षित नहीं है।</string> + <string name="stores_login_error_verification">साइन-इन सत्यापित नहीं हो सका। फिर कोशिश करें।</string> + <string name="stores_login_error_generic">साइन-इन विफल रहा। फिर कोशिश करें।</string> + <string name="common_close">बंद करें</string> + <string name="common_back">बैक</string> + <string name="common_edit">संपादित करें</string> + <string name="games_add_all">इस फ़ोल्डर के सभी गेम जोड़ें</string> + <string name="games_added_none">इस फ़ोल्डर में कुछ नया नहीं है</string> + <string name="edit_name">नाम</string> + <string name="edit_target">टार्गेट</string> + <string name="edit_start_in">यहां शुरू करें</string> + <string name="edit_launch_options">लॉन्च विकल्प</string> + <string name="edit_artwork">आर्टवर्क</string> + <string name="edit_other_exe">दूसरा exe…</string> + <string name="edit_remove">हटाएं</string> + <string name="art_cover">कवर</string> + <string name="art_background">बैकग्राउंड</string> + <string name="art_logo">लोगो</string> + <string name="art_icon">आइकन</string> + <string name="art_src_folder">गेम फ़ोल्डर</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">आपकी फ़ाइल</string> + <string name="art_src_exe">exe से</string> + <string name="art_src_auto">ऑटोमैटिक</string> + <string name="art_pick_image">इमेज चुनें…</string> + <string name="art_pick_title">एक इमेज चुनें: %1$s</string> + <string name="art_reset">ऑटोमैटिक पर रीसेट करें</string> + <plurals name="games_added_n"> + <item quantity="one">%1$d गेम जोड़ा गया</item> + <item quantity="other">%1$d गेम जोड़े गए</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="one">%1$d पहले से Games में है</item> + <item quantity="other">%1$d पहले से Games में हैं</item> + </plurals> + <string name="hop_back">B · %1$s पर वापस जाएं</string> + <string name="hop_from_games">गेम</string> + <string name="hop_from_play">Play सेटिंग</string> + <string name="sheet_graphics_drivers">ग्राफ़िक्स ड्राइवर</string> + <string name="comp_this_proton">यह Proton</string> + <string name="comp_every_proton">हर Proton</string> + <string name="comp_for_every_proton">हर Proton के लिए</string> + <string name="comp_available_count">उपलब्ध · %1$d</string> + <string name="comp_checked">%1$s को जांचा गया</string> + <string name="comp_check_all">नए कॉम्पोनेंट और ड्राइवर देखें</string> + <string name="comp_get">लें</string> + <string name="comp_proton_bar">%1$s · नीचे दिए DXVK, VKD3D-Proton और FEX इस Proton के हैं</string> + <string name="comp_device_tested">%1$s · जांचा गया</string> + <string name="comp_device_untested">%1$s · अनटेस्टेड</string> + <string name="comp_device_unsupported">%1$s · असमर्थित</string> + <string name="gpu_auto_keeps">सुझाई गई जोड़ी को हमेशा नवीनतम रखता है</string> + <string name="gpu_bar_auto">ऑटो · %1$s</string> + <string name="gpu_separately">रनटाइम और डिस्प्ले ड्राइवर अलग-अलग</string> + <string name="gpu_use_title">%1$s इस्तेमाल करें?</string> + <string name="proton_remove_title">%1$s हटाएं?</string> + <string name="setup_tab_diagnostics">डायग्नोस्टिक्स</string> + <string name="setup_ready">खेलने के लिए तैयार</string> + <string name="setup_not_ready">अभी खेलने के लिए तैयार नहीं</string> + <string name="setup_ready_runtime">रनटाइम %1$s</string> + <string name="setup_ready_limit_off">प्रोसेस सीमा बंद</string> + <string name="setup_ready_signed_in">%1$s के रूप में साइन इन</string> + <string name="setup_needs_you">जब किसी चीज़ को आपकी ज़रूरत हो</string> + <string name="setup_group_look">लुक</string> + <string name="setup_group_device">यह डिवाइस</string> + <string name="setup_group_art">लाइब्रेरी आर्ट</string> + <string name="flathub_eyebrow">LINUX ऐप</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Linux डेस्कटॉप के लिए डेस्कटॉप ऐप, डेस्कटॉप से खोले जाते हैं।</string> + <string name="flathub_setup">Flathub सेट करें</string> + <string name="flathub_remove_title">%1$s हटाएं?</string> + <string name="flathub_remove_note">इसकी सेटिंग और फ़ाइलें बनी रहती हैं।</string> + <string name="flathub_keep">रखें</string> + <string name="desktop_size">डिस्क पर आकार</string> + <string name="desktop_uninstall_title">डेस्कटॉप अनइंस्टॉल करें?</string> + <string name="desktop_all_systems">सभी</string> + <string name="gog_achievements">GOG उपलब्धियां</string> + <string name="gog_achievements_hint">समर्थित GOG गेम के लिए उपलब्धियां सिंक करता है। यह अगली बार लॉन्च करने पर लागू होगा।</string> + <string name="gog_achievements_off">उपलब्धियां बंद</string> + <string name="gog_achievements_on">उपलब्धियां चालू</string> + <string name="gog_achievements_signin">GOG में फिर से साइन इन करें</string> + <string name="gog_achievements_uploaded">उपलब्धियां अपलोड हुईं</string> + <string name="gog_achievements_pending">उपलब्धि अपलोड होना बाकी है</string> + <string name="gog_achievements_enable_store">Stores सेटिंग में GOG उपलब्धियां चालू करें</string> + <string name="gog_achievements_unavailable">उपलब्धियां उपलब्ध नहीं; सेशन लॉग देखें</string> + <string name="achievement_unlocked">उपलब्धि अनलॉक हुई</string> + <string name="touch_paste">पेस्ट करें</string> + <string name="touch_dpad">डी-पैड</string> + <string name="touch_left_stick">बायां स्टिक</string> + <string name="touch_right_stick">दायां स्टिक</string> + <string name="touch_menu">मेन्यू</string> + <string name="touch_keyboard">कीबोर्ड</string> + <string name="touch_dpad_up">डी-पैड ऊपर</string> + <string name="touch_dpad_down">डी-पैड नीचे</string> + <string name="touch_dpad_left">डी-पैड बायां</string> + <string name="touch_dpad_right">डी-पैड दायां</string> + <string name="touch_title">टच कंट्रोल</string> + <string name="touch_controller">टच कंट्रोलर</string> + <string name="touch_mouse">माउस</string> + <string name="touch_on">ऑन</string> + <string name="touch_off">ऑफ</string> + <string name="touch_speed_slow">धीमी</string> + <string name="touch_speed_normal">सामान्य</string> + <string name="touch_speed_fast">तेज़</string> + <string name="touch_edit_layout">लेआउट बदलें</string> + <string name="touch_droiddeck_menu">DroidDeck मेन्यू</string> + <string name="touch_drag_hint">कंट्रोल को स्क्रीन पर खींचकर रखें</string> + <string name="touch_arrange">व्यवस्थित करें</string> + <string name="touch_discard">रद्द करें</string> + <string name="touch_saving">सेव हो रहा है…</string> + <string name="touch_icon">आइकॉन</string> + <string name="touch_narrower">चौ−</string> + <string name="touch_wider">चौ+</string> + <string name="touch_shorter">ऊं−</string> + <string name="touch_taller">ऊं+</string> + <string name="touch_hide">छिपाएं</string> + <string name="touch_color_opacity">रंग और ओपैसिटी…</string> + <string name="touch_copy_layout">यह लेआउट कॉपी करें</string> + <string name="touch_layout_copied">लेआउट कॉपी हो गया; इसे किसी भी गेम के टच लेआउट में पेस्ट करें</string> + <string name="touch_paste_layout">कॉपी किया लेआउट पेस्ट करें</string> + <string name="touch_reset_layout">Steam के डिफ़ॉल्ट पर रीसेट करें</string> + <string name="touch_no_binding">इस कंट्रोल की Steam में कोई बाइंडिंग नहीं है जिसे आइकॉन दिया जा सके</string> + <string name="touch_icon_for">%1$s के लिए आइकॉन</string> + <string name="touch_layout_color">टच लेआउट का रंग</string> + <string name="touch_done">हो गया</string> + <string name="touch_save_failed">टच लेआउट सेव नहीं हो सका</string> + <string name="touch_saved">टच लेआउट Steam में सेव हो गया</string> + <string name="touch_app_id">ऐप %1$d</string> + <string name="touch_label_hint">लेबल (वैकल्पिक)</string> + <string name="touch_icons_missing">Steam के आइकॉन नहीं मिले</string> + <plurals name="touch_icons_found"> + <item quantity="one">Steam से %1$d आइकॉन</item> + <item quantity="other">Steam से %1$d आइकॉन</item> + </plurals> + <string name="touch_icon_color">आइकॉन का रंग</string> + <string name="touch_button_color">बटन का रंग</string> + <string name="touch_no_icon">कोई आइकॉन नहीं</string> + + <string name="stores_sync_sign_in">साइन-इन जांचा जा रहा है…</string> + <string name="stores_sync_library">आपकी लाइब्रेरी लाई जा रही है…</string> + <string name="stores_sync_game_list">गेम लिस्ट लाई जा रही है…</string> + <string name="stores_sync_details">गेम की जानकारी लोड हो रही है… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">%1$d गेम सिंक हो रहा है…</item> + <item quantity="other">%1$d गेम सिंक हो रहे हैं…</item> + </plurals> + <string name="stores_sync_progress">सिंक हो रहा है… %1$d/%2$d</string> + <string name="stores_sync_cover_art">कवर इमेज लाई जा रही हैं…</string> + <string name="stores_sync_failed">सिंक नहीं हो सका</string> + <string name="stores_sync_no_games">आपकी %1$s लाइब्रेरी में कोई गेम नहीं मिला</string> + <string name="stores_sync_amazon_no_answer">आपकी लाइब्रेरी के लिए Amazon Games ने जवाब नहीं दिया: रिफ्रेश करें, या फिर से साइन इन करें</string> + <string name="stores_sync_gog_unreachable">GOG से कनेक्ट नहीं हो सका</string> + <string name="stores_sync_gog_unreadable">आपकी GOG लाइब्रेरी पढ़ी नहीं जा सकी</string> + <string name="stores_registering">Steam में रजिस्टर हो रहा है…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Cardboard मोड</string> + <string name="cardboard_mode_hint">फ़ोन VR व्यूअर के लिए बाईं और दाईं आंख के व्यू।</string> + <string name="cardboard_source">तस्वीर</string> + <string name="cardboard_source_screen">वर्चुअल स्क्रीन</string> + <string name="cardboard_source_sbs">साइड-बाय-साइड 3D</string> + <string name="cardboard_viewer">व्यूअर</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">कोई लेंस नहीं</string> + <string name="cardboard_ipd">आंखों की दूरी</string> + <string name="cardboard_screen_width">स्क्रीन की चौड़ाई</string> + <string name="cardboard_screen_distance">स्क्रीन की दूरी</string> + <string name="cardboard_head_tracking">हेड ट्रैकिंग</string> + <string name="cardboard_no_sensor">इस डिवाइस में रोटेशन सेंसर नहीं है।</string> + <string name="cardboard_recenter">व्यू को फिर से बीच में लाएं</string> + <string name="cardboard_recenter_button">बीच में लाएं</string> + <string name="cardboard_steamvr">SteamVR (प्रायोगिक)</string> + <string name="cardboard_steamvr_install">इंस्टॉल करें</string> + <string name="cardboard_steamvr_install_hint">Valve का ARM64 SteamVR डाउनलोड करता है (571 MB)।</string> + <string name="cardboard_steamvr_installing">SteamVR डाउनलोड और अनपैक हो रहा है…</string> + <string name="cardboard_steamvr_start">शुरू करें</string> + <string name="cardboard_steamvr_ready_hint">आपके सिर को हेडसेट मानकर SteamVR चलाता है; तस्वीर साइड-बाय-साइड 3D में बदल जाती है।</string> + <string name="cardboard_steamvr_stop">रोकें</string> + <string name="cardboard_steamvr_running_hint">SteamVR चल रहा है।</string> + <string name="cardboard_steamvr_unavailable">इस सेशन में उपलब्ध नहीं है।</string> + <string name="cardboard_steamvr_failed">SteamVR ने जवाब नहीं दिया; सेशन लॉग देखें।</string> + <string name="drawer_component_downloads">कंपोनेंट डाउनलोड करें</string> + <string name="drawer_component_downloads_hint">डाउनलोड Components पेज के साथ साझा होते हैं। अगली बार गेम शुरू होने पर इस्तेमाल करने के लिए ऊपर डाउनलोड किया गया वर्शन चुनें।</string> + <string name="drawer_component_choose_download">डाउनलोड करने के लिए वर्शन चुनें</string> + <string name="stores_fact_developer">डेवलपर</string> + <string name="stores_fact_publisher">पब्लिशर</string> + <string name="stores_fact_released">रिलीज़</string> + <string name="stores_hltb_main">मुख्य</string> + <string name="stores_hltb_extras">+ एक्स्ट्रा</string> + <string name="stores_hltb_completionist">पूरा</string> + <string name="stores_hltb_hours">%1$s घं</string> + <string name="stores_hltb_minutes">%1$d मि</string> +</resources> diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml new file mode 100644 index 000000000..51f234a08 --- /dev/null +++ b/app/src/main/res/values-hu/strings.xml @@ -0,0 +1,2235 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>Az első indítás letölti a Steamet. Ez néhány percet vehet igénybe.</item> + <item>Alapértelmezés szerint az alkalmazás elhagyása szünetelteti a Steamet; visszatéréskor ott folytatja, ahol abbahagyta.</item> + <item>A kontroller Xbox 360 kontrollerként jelenik meg. Egyébként használja a képernyőn lévő vezérlőket.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>Az első indítás beállítja az asztalt. Ez néhány percet vehet igénybe.</item> + <item>Alapértelmezés szerint az alkalmazás elhagyása szünetelteti a munkamenetet; visszatéréskor ott folytatja, ahol abbahagyta.</item> + <item>Az érintés érintőpadként működik: húzással mozgathatja a kurzort, koppintással kattinthat.</item> + <item>A kontroller Xbox 360 kontrollerként jelenik meg. Egyébként használja a képernyőn lévő vezérlőket.</item> + </string-array> + <string name="session_channel">Munkamenet</string> + <string name="session_channel_description">Megjelenik, amíg egy Steam- vagy asztali munkamenet aktív</string> + <string name="session_notification">A Steam fut</string> + <string name="session_notification_downloads">Steam-letöltés folyamatban</string> + <string name="session_notification_paused">A munkamenet szünetel – koppintson a Folytatásra, ha készen áll</string> + <string name="resume_session">Folytatás</string> + <string name="suspend_now">Felfüggesztés most</string> + <string name="stop_session">Munkamenet leállítása</string> + <string name="frame_gen_title">Képkocka-generálás</string> + <string name="frame_gen_hint">Válassza ki a képkocka-generálás módját</string> + <string name="frame_gen_off">Ki</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG adaptív %1$d</string> + <string name="lsfg_needs_steam">A Lossless Scaling szükséges a Steamen</string> + <string name="lsfg_needs_install">Telepítse a Lossless Scalinget a Steamen, vagy importálja a Lossless.dll fájlt</string> + <string name="lsfg_import">Lossless.dll importálása…</string> + <string name="lsfg_using_steam">A Steamről származó másolat használatban</string> + <string name="lsfg_using_import">Egy importált másolat használatban</string> + <string name="lsfg_pick_title">Válassza ki a Lossless.dll fájlt</string> + <string name="lsfg_imported">A Lossless.dll importálva</string> + <string name="lsfg_import_unchanged">Ez a Lossless.dll már használatban van</string> + <string name="lsfg_import_not_owned">A Lossless Scaling nincs a tulajdonában ezen a Steam-fiókon</string> + <string name="lsfg_import_invalid">Ez a fájl nem a Lossless Scaling Lossless.dll fájlja</string> + <string name="lsfg_import_failed">Nem sikerült előkészíteni az LSFG-t ebből a Lossless.dll fájlból</string> + <string name="lsfg_problem_missing">Az LSFG-hez szükséges a Lossless Scaling a Steamről vagy egy importált Lossless.dll</string> + <string name="lsfg_problem_failed">Az LSFG nem tudta előkészíteni a shadereket a Lossless.dll fájlból</string> + <string name="hud_fg_unsupported">%1$s: nem támogatott</string> + <string name="hud_fg_failed">%1$s: a motor hibát jelzett</string> + <string name="hud_fg_needs_lossless">%1$s: a Lossless Scaling szükséges</string> + <string name="hud_fg_starting">%1$s %2$s · indítás</string> + <string name="second_library_import_hint">Hozzáad egy Steam-könyvtárat, és Windows-os játékokat keres benne. A tulajdonában lévő játékok a Steam-bejegyzéseiket használják; a többi nem Steam-parancsikonként jelenik meg.</string> + <string name="game_env_title">Játékkörnyezet</string> + <string name="game_env_hint">Környezeti változók Proton-játékokhoz.</string> + <string name="game_env_edit">Szerkesztés</string> + <string name="game_env_shared">Összes Proton-játék</string> + <string name="game_env_scope">Profil</string> + <string name="game_env_app_id">Steam alkalmazásazonosító</string> + <string name="game_env_open">Megnyitás</string> + <string name="game_env_profile">%1$s játék</string> + <string name="game_env_add">Változó hozzáadása</string> + <string name="game_env_name">Változó neve</string> + <string name="game_env_value">Érték</string> + <string name="game_env_save">Mentés</string> + <string name="game_env_remove">Eltávolítás</string> + <string name="game_env_restore">Örökítés újra</string> + <string name="game_env_reset">Környezeti felülírások törlése</string> + <string name="game_env_unset">Törlés ebben a profilban</string> + <string name="game_env_empty">Nincs beállított változó.</string> + <string name="game_env_inherited">Örökölt</string> + <string name="game_env_custom">Egyéni változó</string> + <string name="game_env_error">Nem sikerült betölteni vagy menteni. Ellenőrizze a szabad tárhelyet, és próbálja újra.</string> + <string name="game_env_invalid">Használjon érvényes nevet és legfeljebb 8192 karakteres értéket.</string> + <string name="game_env_unused">A szabványos Proton futtatókörnyezet nem olvassa ezt a változót.</string> + <string name="game_env_loading">Betöltés…</string> + <string name="game_env_feature">D3D12 feature level felülírása. A 12_2 a DirectX 12 Ultimate képességeit jelzi; a nem támogatott funkciók hibát okozhatnak. Hagyja beállítás nélkül az automatikus felismeréshez.</string> + <string name="game_env_shader">D3D12 shader model felülírása. A 6_9 kellően friss VKD3D-t és megfelelő GPU-funkciókat igényel. Hagyja beállítás nélkül az automatikus felismeréshez.</string> + <string name="game_env_cache">Tartsa bekapcsolva a Mesa shader-gyorsítótárazást az ismételt fordítás csökkentéséhez. A false érték bekapcsolja a gyorsítótárazást.</string> + <string name="game_env_cache_size">A Mesa shader-gyorsítótár legnagyobb mérete, például 1G. A nagyobb gyorsítótár több tárhelyet használ.</string> + <string name="game_env_vkd3d_config">VKD3D-beállítások D3D12-höz. A nodxr kikapcsolja a sugárkövetést; csak olyan játéknál használja, amelynek szüksége van rá.</string> + <string name="game_env_proton_log">Állítsa 1-re a Proton diagnosztikai napló bekapcsolásához. A naplózás csökkentheti a teljesítményt, és tárhelyet használ.</string> + <string name="game_env_wined3d">Állítsa 1-re, hogy a DXVK helyett az OpenGL-alapú WineD3D-t használja D3D9–11 kompatibilitási teszteléshez.</string> + <string name="game_env_no_ntsync">Állítsa 1-re, hogy ez a játék ne használja az ntsyncet (droiddeck-ntsync vagy kernelillesztőprogram); ekkor a droiddeck-esynct használja, ha van csomag, egyébként a droiddeck-fsynct.</string> + <string name="game_env_no_esync">Állítsa 1-re a droiddeck-esync kikapcsolásához ennél a játéknál; helyette a droiddeck-fsynct használja, vagy a droiddeck-ntsynct, ha az fsync is ki van kapcsolva.</string> + <string name="game_env_no_fsync">Állítsa 1-re, hogy ez a játék ne használja a droiddeck-fsynct; ekkor a droiddeck-esynct használja, ha van csomag, egyébként a droiddeck-ntsynct.</string> + <string name="game_env_fsync_first">Állítsa 1-re, hogy ez a játék a droiddeck-fsyncen fusson, vagy 0-ra, hogy a droiddeck-esync kerüljön előre, ha van csomag a Protonjához.</string> + <string name="game_env_xalia">Állítsa 0-ra a Proton kontroller-navigációs segédjének kikapcsolásához ennél a játéknál.</string> + <string name="game_env_hud">DXVK overlay D3D9–11-hez. Az fps megjeleníti a képkockasebességet; beállítás nélkül elrejti.</string> + <string name="game_env_dxvk_config">Pontosvesszővel elválasztott DXVK-beállítások. A legújabb buildek a dxvk.maxFrameRate beállítást használják a képkocka-korlátozáshoz; szokásos játékhoz a munkamenet képkocka-korlátozóját érdemes használni.</string> + <string name="game_env_vkd3d_limit">D3D12 képkockasebesség-korlát. A 0 kikapcsolja a korlátot.</string> + <string name="game_env_glthread">Mesa OpenGL szálkezelés. A true érték segíthet a CPU-korlátozott OpenGL-játékoknál; tesztelje játékonként.</string> + <string name="sync_backend_title">Wine-szinkronizálás</string> + <string name="sync_backend_hint">Hogyan tartják szinkronban a játékok a szálaikat. A következő munkamenettől érvényes.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">FEX előbeállítás</string> + <string name="fex_next_launch">A következő játékindítástól érvényes.</string> + <string name="game_settings_title">Játékok</string> + <string name="fex_default_label">FEX alapértelmezés</string> + <string name="fex_default_detail">A FEX alapértelmezéseit használja.</string> + <string name="fex_stability_label">Stabilitás</string> + <string name="fex_stability_detail">A legbiztosabb, leglassabb beállítások.</string> + <string name="fex_compatibility_label">Kompatibilitás</string> + <string name="fex_compatibility_detail">Teljes TSO multiblokkal.</string> + <string name="fex_intermediate_label">Köztes</string> + <string name="fex_intermediate_detail">Kiegyensúlyozott TSO csökkentett x87-pontossággal.</string> + <string name="fex_performance_label">Teljesítmény</string> + <string name="fex_performance_detail">Minden TSO kikapcsolva; multiblokk.</string> + <string name="fex_performance_tso_label">Teljesítmény + TSO</string> + <string name="fex_performance_tso_detail">Teljesítmény-beállítások a fő TSO bekapcsolásával.</string> + <string name="fex_extreme_label">Extrém</string> + <string name="fex_extreme_detail">Kikapcsolja az önmódosító kód ellenőrzését. A JIT-et használó játékok és a DRM-ek hibásan működhetnek.</string> + <string name="fex_extreme_tso_label">Extrém + TSO</string> + <string name="fex_extreme_tso_detail">Extrém beállítások a fő TSO bekapcsolásával.</string> + <string name="fex_extreme_gn_label">Extrém-gn</string> + <string name="fex_extreme_gn_detail">Teljesítmény TSC- és metaadat-utalásokkal. Próbálja ki az Extrém előtt.</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">Teljesítmény teljes kódellenőrzéssel és elrejtett hypervisor-bittel.</string> + <string name="game_env_custom_value">Egyéni érték</string> + <string name="game_env_select_value">Érték kiválasztása</string> + <string name="user_apps_section">Emulátorok és alkalmazások</string> + <string name="user_apps_add">Hozzáadás</string> + <string name="user_apps_add_detail">Saját alkalmazás vagy játék</string> + <string name="user_apps_kind_script">Szkript</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d%%</string> + <string name="user_apps_starting">Indítás…</string> + <string name="user_apps_added">%1$s hozzáadva</string> + <string name="user_apps_removed">%1$s eltávolítva</string> + <string name="user_apps_back">Asztal</string> + <string name="user_apps_gone">Ez az alkalmazás már nincs meg. Válassza ki újra az Asztalról.</string> + <string name="user_apps_open">%1$s megnyitása</string> + <string name="user_apps_remove">Eltávolítás</string> + <string name="user_apps_remove_confirm">Nyomja meg újra az eltávolításhoz</string> + <string name="user_apps_stop_session">Állítsa le a munkamenetet az eltávolításhoz</string> + <string name="user_apps_script_linked">A %1$s a saját mappájából fut, amely a helyén csatolva van a munkamenetbe. Az alkalmazás eltávolítása nem érinti azt a mappát.</string> + <string name="user_apps_script_copied">A %1$s a mappájának egy, a DroidDecken belüli másolatából fut. Az alkalmazás eltávolítása törli a másolatot, az eredetit nem.</string> + <string name="user_apps_runtime_required">Előbb telepítse a Linux futtatókörnyezetet</string> + <string name="user_apps_file_gone">A fájl már nincs meg</string> + <string name="user_apps_no_space">Nincs elég szabad hely: a mappa másolásához %1$s szükséges</string> + <string name="user_apps_copying">%1$s másolása</string> + <string name="user_apps_failed">Az alkalmazás hozzáadása sikertelen</string> + <string name="user_apps_github_checking">%1$s kiadásának keresése</string> + <string name="user_apps_github_unreachable">Nem sikerült beolvasni a %1$s kiadásait</string> + <string name="user_apps_github_no_appimage">A %1$s semelyik kiadása nem tartalmaz ARM64 vagy x86_64 AppImage-et</string> + <string name="user_apps_downloading">%1$s letöltése</string> + <string name="user_apps_download_failed">A letöltés sikertelen</string> + <string name="user_apps_update_failed">Az alkalmazás frissítése sikertelen</string> + <string name="user_apps_icon_failed">Ezt az ikont nem sikerült beolvasni</string> + <string name="user_apps_saved">%1$s mentve</string> + <string name="user_apps_edit">Szerkesztés</string> + <string name="user_apps_check_updates">Frissítések keresése</string> + <string name="user_apps_checking_updates">Frissítések keresése…</string> + <string name="user_apps_update_to">Frissítés erre: %1$s</string> + <string name="user_apps_up_to_date">Naprakész · %1$s</string> + <string name="user_apps_updated">%1$s frissítve</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">Alkalmazás hozzáadása</string> + <string name="add_app_tab_script">Szkript</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">Egy .sh fájl. A saját mappájából fut, teljes képernyőn, gamescope alatt.</string> + <string name="add_app_appimage_hint">Egy ARM64 (aarch64) AppImage, vagy egy x86_64-es, amely a FEX-en keresztül fut. Egyszer kerül kicsomagolásra a Linux futtatókörnyezetbe.</string> + <string name="add_app_github_hint">Telepíti a tároló legújabb kiadásának ARM64 AppImage-ét, vagy annak x86_64-es változatát a FEX-en keresztül.</string> + <string name="app_fex_title">FEX használata</string> + <string name="app_fex_auto">Automatikus</string> + <string name="app_fex_on">Be</string> + <string name="app_fex_off">Ki</string> + <string name="app_fex_hint">A FEX x86-os Linux-programokat futtat ezen az ARM64-es eszközön. Az Automatikus akkor használja, ha a program x86-ra készült.</string> + <string name="app_fex_x86">%1$s a FEX-en keresztül</string> + <string name="app_fex_not_ready">Ez a program %1$s PC-khez készült, és a FEX-en keresztül fut, amely még nincs beállítva. Nyissa meg egyszer a Steamet bejelentkezve: a DroidDeck ekkor letölti a FEX-et és a szükséges Steam Linux Runtime-ot, majd ez megnyílik.</string> + <string name="session_end_program">Ez a program</string> + <string name="session_end_fex_missing">A %1$s x86 PC-khez készült, és a FEX-en keresztül fut, amely még nincs telepítve. Indítsa el egyszer a Steamet bejelentkezve, és hagyja letölteni a FEX-et és a Steam Linux Runtime-ot, majd indítsa el ezt újra.</string> + <string name="add_app_flatpak_hint">Egy Flathub-alkalmazásazonosító vagy a Flathub-oldala. A Flatpak előbb beállításra kerül, ha még nincs beállítva.</string> + <string name="add_app_file">Fájl</string> + <string name="add_app_no_file">Nincs kiválasztott fájl</string> + <string name="add_app_choose_file">Fájl kiválasztása</string> + <string name="add_app_change_file">Módosítás</string> + <string name="add_app_pick_script">Válasszon szkriptet</string> + <string name="add_app_pick_appimage">Válasszon AppImage-et</string> + <string name="add_app_pick_icon">Válasszon ikont</string> + <string name="add_app_repository">Tároló</string> + <string name="add_app_repository_placeholder">tulajdonos/tároló vagy egy github.com hivatkozás</string> + <string name="add_app_repository_invalid">Nem egy GitHub-tároló</string> + <string name="add_app_flatpak_id">Alkalmazásazonosító</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">Nem egy Flathub-alkalmazásazonosító</string> + <string name="add_app_flatpak_bundle_hint">Vagy egy .flatpak fájl, egy Flathubon nem elérhető alkalmazáshoz.</string> + <string name="add_app_pick_flatpak">Válasszon .flatpak fájlt</string> + <string name="add_app_source">Forrás</string> + <string name="add_app_appearance">Az Asztalon</string> + <string name="add_app_preview_name">Új alkalmazás</string> + <string name="add_app_name">Név</string> + <string name="add_app_name_placeholder">Az alkalmazásból</string> + <string name="add_app_icon">Ikon</string> + <string name="add_app_icon_default">Alapértelmezett ikon</string> + <string name="add_app_choose_icon">Ikon kiválasztása</string> + <string name="add_app_clear_icon">Törlés</string> + <string name="add_app_icon_suggestions">A tárolóból</string> + <string name="add_app_icon_searching">Ikonok keresése…</string> + <string name="add_app_icon_reset">Alapértelmezett</string> + <string name="edit_app_title">%1$s szerkesztése</string> + <string name="edit_app_save">Mentés</string> + <string name="add_app_checking_folder">A mappa ellenőrzése…</string> + <string name="add_app_folder_linked">A mappája a munkamenetbe van csatolva: %1$s</string> + <string name="add_app_folder_copied">A mappája Linux-programokat tartalmaz, amelyeket a megosztott tárhely nem tud futtatni, ezért átkerül a DroidDeckbe (%1$s).</string> + <string name="add_app_confirm">Hozzáadás</string> + <string name="add_app_cancel">Mégse</string> + <string name="add_app_runtime_required">Előbb telepítse a Linux futtatókörnyezetet a Beállításból.</string> + <string name="add_app_busy">Várja meg, amíg a(z) %1$s befejeződik.</string> + <string name="steam_compat_label">Kompatibilis</string> + <string name="proton_compatible_note">A Steam Kompatibilitás menüjében „%1$s” jelölésű buildek tartalmazzák a DroidDeck prefixjavításait, és ezeken itt elindulnak a játékok. Az alapértelmezett a Proton Experimental ARM64 (%1$s), vagy a Proton 11.0 ARM64 (%1$s), ha az az újabb build telepítve van. A Valve saját, jelölés nélküli Proton-bejegyzései nem tudnak játékot elindítani ezen az eszközön.</string> + <string name="proton_installed_compatible">%1$s telepítve · %2$s</string> + <string name="common_cancel">Mégse</string> + <string name="common_auto">Automatikus</string> + <string name="common_never">Soha</string> + <string name="common_always">Mindig</string> + <string name="common_uninstall">Eltávolítás</string> + <string name="common_check">Ellenőrzés</string> + <string name="common_update">Frissítés</string> + <string name="common_checking">Ellenőrzés…</string> + <string name="common_working">Feldolgozás…</string> + <string name="common_forget">Elfelejtés</string> + <string name="common_add_ellipsis">Hozzáadás…</string> + <string name="common_use">Használat</string> + <string name="common_applies_next_session">A következő munkamenettől érvényes.</string> + <string name="mode_steam_title">Steam-munkamenet</string> + <string name="mode_desktop_title">Asztali munkamenet</string> + <string name="mode_display">Kijelző</string> + <string name="mode_resolution">Felbontás</string> + <string name="mode_res_720">Legfeljebb 720p</string> + <string name="mode_res_900">Legfeljebb 900p</string> + <string name="mode_res_1080">Legfeljebb 1080p</string> + <string name="mode_res_native">Natív</string> + <string name="mode_res_default">%1$s (alapértelmezett)</string> + <string name="mode_res_custom_value">Egyéni · %1$d×%2$d</string> + <string name="mode_res_custom">Egyéni…</string> + <string name="mode_res_note">A 720p reszponzívabbá teheti a menüket.</string> + <string name="mode_ratio">Képernyőarány</string> + <string name="mode_ratio_custom">Az egyéni felbontás határozza meg.</string> + <string name="mode_ratio_auto">Az Automatikus legalább 16:9-et használ.</string> + <string name="mode_fps">Képkocka-korlát</string> + <string name="mode_fps_note">Korlátozza a teljes munkamenetet. A kijelző olyan frissítési frekvenciára vált, amelyet a korlát egyenlően oszt.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">HDR10-kimenet</string> + <string name="mode_hdr_unavailable">Nem elérhető: %1$s.</string> + <string name="mode_hdr_restart">Indítsa újra az alkalmazást az érvényesítéshez.</string> + <string name="mode_drivers">Illesztőprogramok</string> + <string name="mode_gpu_drivers">GPU-illesztőprogramok</string> + <string name="mode_gpu_drivers_hint">A Steam és az Asztal közösen használja. Az Összetevők oldalon állítható be.</string> + <string name="mode_touch_controls">Érintés és vezérlők</string> + <string name="mode_touch">Érintés</string> + <string name="mode_touch_touchpad">Érintőpad</string> + <string name="mode_touch_direct">Közvetlen</string> + <string name="mode_touch_note">Automatikus: érintőpad az Asztalon, közvetlen a Steamben.</string> + <string name="mode_osc">Képernyőn megjelenő vezérlők</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">Az Automatikus megjeleníti a vezérlőket, ha nincs csatlakoztatott kontroller.</string> + <string name="osc_steam_touch">Steam érintés (béta)</string> + <string name="osc_style_next_session">A Steam érintésvezérlői a következő Steam-munkamenettől lépnek életbe</string> + <string name="osc_steam_layout">Steam érintéskiosztás</string> + <string name="osc_steam_layout_hint">Mozgassa, méretezze vagy rejtse el a játék érintésvezérlőit; a Steamben mentve</string> + <string name="mode_controller">Kontroller</string> + <string name="mode_controller_hint">Hogyan látja a Steam a kontrollerét. A következő munkamenettől érvényes.</string> + <string name="mode_controller_deck">Steam Deck kontroller</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Xbox 360 kontroller</string> + <string name="mode_controller_note">Deck: Gyors hozzáférés gomb és gyroszkóp. Steam Controller: ugyanez, a hátsó gombok fogásérzékelésével. DualShock 4 és DualSense: gyroszkóp, és az érintőpadok mint touchpad. Xbox 360: a Gyors hozzáférés megnyitása Guide+A-val.</string> + <string name="mode_back">Vissza gomb</string> + <string name="back_menu_then_qam">Menü → QAM</string> + <string name="back_qam_then_menu">QAM → Menü</string> + <string name="mode_session">Munkamenet</string> + <string name="mode_suspend">Háttérbeli viselkedés</string> + <string name="mode_suspend_hint">Amikor az alkalmazás el van rejtve, vagy a kijelző kikapcsol.</string> + <string name="mode_suspend_manual">Manuális</string> + <string name="mode_suspend_native">Natív</string> + <string name="mode_suspend_steam_note">Az Automatikus szüneteltet, és megjelenéskor folytatja. A Natív előbb felkészíti a Steamet az altatásra. A Manuális megvárja a Folytatást. A Soha mindig futva tartja.</string> + <string name="mode_background_downloads">Letöltések futtatásának fenntartása</string> + <string name="mode_background_downloads_hint">Elhalasztja a háttérbeli felfüggesztést, amíg a Steam aktívan letölt.</string> + <string name="mode_suspend_note">Az Automatikus szüneteltet a háttérben, és megjelenéskor folytatja. A Manuális megvárja a Folytatást. A Soha mindig futva tartja.</string> + <string name="mode_startup">Indítás</string> + <string name="mode_steam_startup">Steam indítása a DroidDeckkel</string> + <string name="mode_steam_startup_hint">Megnyitja a Steam-munkamenetet a DroidDeck indulásakor.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">Kiadások keresése…</string> + <string name="mode_decky_ready">Készen áll a %1$s telepítésére.</string> + <string name="mode_decky_none">Nem található kompatibilis build. Próbálja újra később.</string> + <string name="mode_decky_installed">Telepítve · %1$s</string> + <string name="mode_decky_update">Elérhető frissítés · %1$s</string> + <string name="mode_decky_current">Naprakész · %1$s</string> + <string name="mode_decky_loader">Betöltő</string> + <string name="mode_decky_install_latest">Legújabb telepítése</string> + <string name="mode_decky_on">A Steammel együtt indul. Amíg be van kapcsolva, az eszközön lévő más alkalmazások is irányíthatják a Steamet.</string> + <string name="mode_decky_off">Ki. A Steam távoli hibakeresési portja zárva marad.</string> + <string name="mode_decky_remove_title">Eltávolítja a Decky Loadert?</string> + <string name="mode_decky_remove_text">Eltávolítja a betöltőt. A bővítményei és beállításai megmaradnak.</string> + <string name="mode_client">Kliens</string> + <string name="mode_mangoapp">Teljesítmény-overlay</string> + <string name="mode_mangoapp_hint">Kapcsolja ki, ha a játékok összeomlanak vagy elfeketednek Deck módban. A következő munkamenettől érvényes.</string> + <string name="mode_steam_repair">Steam-kliens javítása</string> + <string name="mode_steam_repair_hint">A Steam kliens újbóli letöltése a Steam következő indításakor. Használja, ha a Steam a betöltő képernyőn ragad. A játékok, a bejelentkezés és a beállítások megmaradnak.</string> + <string name="mode_steam_repair_queued">A Steam kliens újra letöltődik a Steam következő indításakor.</string> + <string name="mode_steam_repair_button">Javítás</string> + <string name="mode_added_art">Illusztrációk</string> + <string name="mode_sgdb_key">SteamGridDB API-kulcs</string> + <string name="mode_sgdb_key_yours">A kulcsa</string> + <string name="mode_sgdb_edit">Szerkesztés…</string> + <string name="mode_added_art_hint">Kitölti a hiányzó illusztrációkat a Steamről, majd a SteamGridDB-ről. A játék mappájában lévő saját cover.jpg, hero.jpg, logo.png vagy icon.png fájl elsőbbséget kap.</string> + <string name="summary_remove_game">%1$s eltávolítása</string> + <string name="mode_added_remove_confirm">Eltávolítja?</string> + <string name="games_add">Játék hozzáadása</string> + <string name="games_add_title">Játék hozzáadása:</string> + <string name="games_pick_exe_title">Válassza ki az exe fájlt:</string> + <string name="games_add_unreachable">Ez a hely nem érhető el a munkamenetben</string> + <string name="mode_added_exe_check">Ennek az exe fájlnak az ellenőrzése</string> + <string name="wincomp_title">Windows-összetevők</string> + <string name="wincomp_pad_hint">LB / RB a szakaszok közötti ugráshoz.</string> + <string name="wincomp_card_none">Nincs</string> + <string name="wincomp_applies">A Proton által nem tartalmazott Windows DLL-ek az összetevőlistából. Kapcsolja be azokat, amelyekre ennek a játéknak szüksége van: ezek a játék saját Windows-mappájába kerülnek, soha nem a fájljaiba, és megmaradnak, függetlenül attól, melyik Protonon fut.</string> + <string name="wincomp_offline">Nem sikerült elérni az összetevőlistát. A már letöltött összetevők továbbra is kapcsolhatók.</string> + <string name="wincomp_recommended">Ajánlott ehhez a játékhoz</string> + <string name="wincomp_checking">Annak ellenőrzése, mire van szüksége ennek a játéknak…</string> + <string name="wincomp_when_online">Online elérhető</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">%1$d összetevő bekapcsolása…</item> + <item quantity="other">%1$d összetevő bekapcsolása…</item> + </plurals> + <string name="wincomp_auto_section">Bekapcsolva ehhez a játékhoz</string> + <string name="wincomp_auto_steam">A Steam a %2$s segítségével telepíti a %1$s-t (alkalmazás: %3$s)</string> + <string name="wincomp_auto_gog">A GOG listájából ehhez a játékhoz</string> + <string name="wincomp_auto_epic">Az Epic előfeltételeiből ehhez a játékhoz</string> + <string name="wincomp_auto_amazon">Az Amazon telepítési lépéseiből ehhez a játékhoz</string> + <string name="wincomp_steam_by_name">A Steam név szerint egyezve telepíti a %1$s-t a %2$s alkalmazással</string> + <string name="wincomp_suggestions">Szintén szerepel a listán</string> + <string name="wincomp_suggestion_reason">A %1$s sorolja fel; itt nem összetevő</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">A játék a %1$s-t tartalmazza</string> + <string name="wincomp_found_shipped">A játékban megtalálható a %1$s (gyakran változatlanul is működik)</string> + <string name="wincomp_already_here">Már megvan ebben a játékban</string> + <string name="wincomp_no_recommendation">A játék mappájában semmi nem utal összetevőre.</string> + <string name="wincomp_active">Telepítve ehhez a játékhoz (%1$d)</string> + <string name="wincomp_all">Minden összetevő (%1$d)</string> + <string name="wincomp_installers">Windows-telepítőt igényel (%1$d)</string> + <string name="wincomp_installers_note">Ezek saját telepítőprogramok, amelyekben nincs szétszedhető csomag, és még nincs rögzítve, mit telepítenek. Többségük régi futtatókörnyezet, amelyet a Protonban lévő Mono már helyettesít; a rögzítések elkészülésük után kerülnek hozzáadásra.</string> + <string name="wincomp_show">Megjelenítés</string> + <string name="wincomp_hide">Elrejtés</string> + <string name="wincomp_needs_installer">Saját telepítőprogramot futtat (még nincs rögzítve)</string> + <string name="wincomp_unsupported">Itt még nem támogatott</string> + <string name="wincomp_loading">Összetevők betöltése…</string> + <string name="wincomp_failed">Nem sikerült telepíteni: %1$s</string> + <string name="wincomp_downloaded">Letöltve</string> + <string name="wincomp_downloading">%1$s letöltése</string> + <string name="wincomp_installing">Telepítés: %1$s</string> + <string name="wincomp_reverting">Kikapcsolás…</string> + <string name="wincomp_download_size">%1$s letöltés</string> + <string name="wincomp_next_launch">A játék következő indításakor lép érvénybe.</string> + <string name="mode_stretch169">16:9 játékok nyújtása</string> + <string name="mode_stretch169_hint">A 16:9-nél négyzetesebb képernyőkön kitölti a játékkal a képernyőt sávok helyett. A következő munkamenettől érvényes.</string> + <string name="mode_audio">Hang</string> + <string name="mode_directaudio">DirectAudio a játékokhoz</string> + <string name="mode_directaudio_hint">Megkerüli a PulseAudiót a kisebb késleltetés érdekében.</string> + <string name="mode_mic">Mikrofon</string> + <string name="mode_mic_hint">Hangos csevegéshez.</string> + <string name="mode_network">Hálózat</string> + <string name="mode_wifi_discovery">Wi-Fi felderítés</string> + <string name="mode_wifi_discovery_hint">Wi-Fi nevek és közeli hálózatok megjelenítése a Steamben.</string> + <string name="mode_wifi_location_off">A helymeghatározási szolgáltatások ki vannak kapcsolva.</string> + <string name="mode_wifi_permission_denied">A helymeghatározási engedély nincs megadva.</string> + <string name="mode_wifi_open_settings">Beállítások megnyitása</string> + <string name="mode_wifi_continue">Folytatás</string> + <string name="mode_wifi_explain_title">Bekapcsolja a Wi-Fi felderítést?</string> + <string name="mode_wifi_explain_text">Az Android helymeghatározási engedélyt igényel a Wi-Fi nevek beolvasásához és a közeli hálózatok felderítéséhez. A DroidDeck ezt a hozzáférést arra használja, hogy megjelenítse ezeket a hálózatokat a Steamben. Nem követjük a helyadatait. Az internet-hozzáférés ezen engedély nélkül is működik.</string> + <string name="mode_storage">Játéktárhely</string> + <string name="mode_storage_auto">Automatikus – az SD-kártya, ha van, és a Játékok mappái</string> + <string name="mode_storage_auto_none">Automatikus – a Játékok mappái, és az SD-kártya, ha van (jelenleg nincs)</string> + <string name="mode_storage_internal">Csak belső</string> + <string name="mode_storage_folder">Mappa: %1$s</string> + <string name="mode_second_library">Második könyvtár</string> + <string name="mode_storage_note">Az SD-kártyáról vagy megosztott tárhelyről streamelő játékok akadhatnak.</string> + <string name="mode_storage_diagnostics">Tárhely-diagnosztika</string> + <string name="mode_storage_diagnostics_hint">Rögzíti a foglalási eredményeket és a mintavételezett tárhelyidőzítéseket a megosztott naplókban. Bekapcsolva lassíthatja a telepítéseket. A következő munkamenettől érvényes.</string> + <string name="mode_choose_folder">Válasszon mappát…</string> + <string name="mode_custom_res_title">Egyéni felbontás</string> + <string name="mode_custom_res_text">Felülírja a korlátot és az arányt. A képernyőhöz nem illő méret sávokat eredményez.</string> + <string name="mode_width">Szélesség</string> + <string name="mode_height">Magasság</string> + <string name="mode_custom_res_range">320×240 és 3840×2160 között.</string> + <plurals name="mode_added_count"> + <item quantity="one">%1$d játék</item> + <item quantity="other">%1$d játék</item> + </plurals> + <string name="common_ok">OK</string> + <string name="common_delete">Törlés</string> + <string name="common_restore">Visszaállítás</string> + <string name="common_download">Letöltés</string> + <string name="common_delete_named">%1$s törlése</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Futtatókörnyezet-illesztőprogram</string> + <string name="comp_runtime_driver_hint">A Steam, a játékok és az asztal használja. A következő munkamenettől érvényes.</string> + <string name="comp_import_turnip">Turnip zip importálása…</string> + <string name="comp_display_driver">Megjelenítő-illesztőprogram</string> + <string name="comp_display_driver_hint">Mindkét módban ez rajzolja a képkockákat a képernyőre. Indítsa újra az alkalmazást az érvényesítéshez.</string> + <string name="comp_import_adrenotools">AdrenoTools zip importálása…</string> + <string name="comp_never">soha</string> + <string name="comp_nightlies_checked">Nightlies ellenőrizve: %1$s</string> + <string name="comp_title">Összetevők</string> + <string name="comp_about">Az Összetevőkről</string> + <string name="comp_check_drivers">Új illesztőprogramok keresése</string> + <string name="comp_check_nightlies">Új csomagok keresése a Nightliesben</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">Az itt kiválasztott Proton futtat minden olyan játékot, amely a Steamben nem választott saját Protont, és a Steamben végzett módosítás is megjelenik itt. A Steam frissítéskor lecseréli a Valve Protonját; az eredetik buildenként megmaradnak.</string> + <string name="comp_scope">Konfiguráció</string> + <string name="comp_profile_configure">Profil beállítása</string> + <string name="comp_profile">Profil</string> + <string name="comp_scope_general">Alapértelmezett profil</string> + <string name="comp_scope_general_detail">A felülírás nélküli játékok automatikusan ezt használják.</string> + <string name="comp_profile_game_detail">A mentett beállítások automatikusan érvénybe lépnek a %1$s indításakor.</string> + <string name="comp_game_hint">Csak azokat a beállításokat válassza ki, amelyekre ennek a játéknak szüksége van. Minden más továbbra is az Alapértelmezett profilt használja.</string> + <string name="comp_game_fex">FEX előbeállítás</string> + <string name="comp_game_fex_inherited">Az Alapértelmezett profil előbeállítását használja.</string> + <string name="comp_game_fex_override">Egyéni előbeállítás mentve ehhez a játékhoz.</string> + <string name="comp_game_fex_custom">Egyéni FEX-változók</string> + <string name="comp_game_use_general">Alapértelmezett használata</string> + <string name="comp_game_compatibility">Játékkompatibilitás</string> + <string name="comp_game_environment_hint">Proton-, DXVK-, VKD3D- és szinkronizációs környezeti beállítások ehhez a játékhoz.</string> + <string name="comp_game_proton_inherited">Az Alapértelmezett profil Protonját használja.</string> + <string name="comp_game_proton_override">Egyéni Proton mentve ehhez a játékhoz a Steam kompatibilitási hozzárendelésén keresztül.</string> + <string name="comp_no_proton_short">Nincs Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Az Alapértelmezett profil %1$s csomagját használja, hacsak ez a játék másik csomagot nem választ.</string> + <string name="comp_game_component_general">Az Alapértelmezett profil összetevőcsomagját használja.</string> + <string name="comp_game_component_override">Csak ehhez a játékhoz a %1$s-t használja.</string> + <string name="comp_game_runtime_options">Futtatókörnyezet-beállítások</string> + <string name="comp_game_environment_other_hint">Egyéb, játékonkénti Proton-, DXVK-, VKD3D-Proton- és szinkronizációs környezeti beállítások.</string> + <string name="comp_game_saved_next">A %1$s profilja mentve a játék következő indítására.</string> + <string name="game_no_steam_title">Indítás Steam nélkül</string> + <string name="game_no_steam_detail">Olyan játékhoz, amely azonnal bezáródik: elindítja a Proton Steam-beállítása nélkül. A Steam funkciói, mint az overlay, ki vannak kapcsolva hozzá.</string> + <string name="game_no_steam_on">Be</string> + <string name="comp_game_proton_live">A %1$s Proton-választása mentve, és már elérhető a Steamben.</string> + <string name="comp_game_proton_next">A %1$s Proton-választása mentve. Indítsa újra a Steamet a játék elindítása előtt.</string> + <string name="comp_game_proton_not_runnable">A %1$s nem tudja használni a kiválasztott Protont ezen az eszközön.</string> + <string name="comp_game_load_failed">Nem sikerült betölteni a %1$s profilját: %2$s</string> + <string name="comp_game_save_failed">Nem sikerült menteni a játékprofilt: %1$s</string> + <string name="comp_game_reset">Játékprofil visszaállítása</string> + <string name="comp_game_reset_title">Visszaállítja a %1$s profilját?</string> + <string name="comp_game_reset_body">Eltávolítja ennek a játéknak a Proton-, FEX-, összetevőcsomag-, környezeti, textúraszűrési és Windows-összetevő-felülírásait? Ezután az Alapértelmezett profilt fogja használni.</string> + <string name="comp_game_reset_done">A %1$s most az Alapértelmezett profilt használja.</string> + <string name="comp_game_reset_failed">Nem sikerült teljesen visszaállítani a játékprofilt: %1$s</string> + <string name="comp_default_live">A %1$s most minden olyan játékot futtat, amely a Steamben nem választott saját Protont.</string> + <string name="comp_default_next">A %1$s a Steam következő indításától minden olyan játékot futtat, amely nem választott saját Protont.</string> + <string name="comp_default_not_runnable">A %1$s itt nem tud elindítani játékokat, ezért nem lehet alapértelmezett.</string> + <string name="comp_default_failed">Nem sikerült menteni az alapértelmezett Protont: %1$s</string> + <string name="comp_swapped">%1$s · %2$d kicserélve</string> + <string name="comp_all_original">%1$s · mind eredeti</string> + <string name="comp_game_running_suffix">%1$s · a játék fut</string> + <string name="comp_esync_pack">%1$s · droiddeck-esync csomag</string> + <string name="comp_esync_wanted">%1$s · még nincs droiddeck-esync csomag</string> + <string name="comp_esync_builtin">%1$s · az esync be van építve, nincs szükség csomagra</string> + <string name="comp_gpu_drivers">GPU-illesztőprogramok</string> + <string name="comp_reapplied">%1$s · újraalkalmazva indításkor: %2$s</string> + <string name="comp_next_queued">Következő: %1$s · a játék bezárása után</string> + <string name="comp_in_use">%1$s használatban: %2$s</string> + <string name="comp_waits">Megvárja, amíg a játék bezárul</string> + <string name="comp_applies_next_game">A következő játékindítástól érvényes</string> + <string name="comp_import_zip">.zip importálása</string> + <string name="comp_reading">Protonok beolvasása…</string> + <string name="comp_no_proton">Még nincs telepítve Proton. Indítsa el egyszer a Steamet az ARM64 Protonjának letöltéséhez, vagy adjon hozzá GE-Protont, CachyOS-t vagy DroidDeck-Protont a Beállításból.</string> + <string name="comp_original">Eredeti · %1$s</string> + <string name="comp_this_build">%1$s · ez a build</string> + <string name="comp_earlier_build">%1$s · korábbi build</string> + <string name="comp_installed_count">Telepítve · %1$d</string> + <string name="comp_nothing_stored">Még nincs tárolva semmi a %1$s számára. Ennek a Protonnak a saját fájljai eredetiként lesznek mentve.</string> + <string name="comp_waits_long">Egy játék fut ezen a Protonon, ezért ez megvárja, amíg bezárul.</string> + <string name="comp_applies_long">A következő játékindítástól érvényes.</string> + <string name="comp_swap_title">Kicseréli a %1$s-t?</string> + <string name="comp_swap_body">Beilleszti a %1$s-t a %2$s-be? Az eredeti fájljai mentve maradnak. %3$s</string> + <string name="comp_restore_title">Visszaállítja a %1$s-t?</string> + <string name="comp_restore_body">Visszateszi a %1$s eredetijét a %2$s-be? %3$s</string> + <string name="comp_delete_package_body">Eltávolítja a csomagot az alkalmazásból. Újra letöltheti.</string> + <string name="comp_delete_original_title">Törli ezt az eredetit?</string> + <string name="comp_delete_original_body">Ez a %1$s eredeti a %2$s egy korábbi buildjéből származik. Az aktuális build eredetije mindig megmarad.</string> + <string name="comp_nightlies_count">A Nightliesen · %1$d</string> + <string name="comp_refresh_to_list">Frissítsen a Nightliesen lévő csomagok listázásához.</string> + <string name="comp_nothing_new">Nincs semmi új az utolsó ellenőrzés óta.</string> + <string name="comp_import_wcp">.wcp importálása</string> + <string name="comp_restore_original">Ennek a Protonnak az eredetijének visszaállítása</string> + <string name="comp_restore_original_body">Visszateszi a %1$s eredeti %2$s-ét?</string> + <string name="comp_swap">Csere</string> + <string name="comp_about_text">FEX, DXVK és VKD3D-Proton minden olyan Protonhoz, amellyel a Steam játékokat futtat.\n\nVálasszon ki egy telepített sort a becseréléséhez. Ez a következő játékindításkor, vagy a futó játék bezárása után lép érvénybe.\n\nA választásai minden indítás előtt újraellenőrződnek, és visszaállnak, ha valami megváltoztatta őket. Minden Proton eredeti fájljai megmaradnak, így a Steam-frissítések sosem veszítik el őket.</string> + <string name="comp_tag_original">Eredeti</string> + <string name="comp_tag_in_use">Használatban</string> + <string name="comp_tag_new">Új</string> + <string name="comp_tag_stored">Tárolva</string> + <string name="perf_title">Teljesítmény</string> + <string name="perf_lede">CPU-magok, munkamenet-javítások és a hosztnév. A következő munkamenettől érvényes.</string> + <string name="perf_client_cores_group">Steam-kliens magok</string> + <string name="perf_override">A Steam magválasztásának felülírása</string> + <string name="perf_override_hint">A Steamet, felhasználói felület segítőjét és a gamescope-ot az alábbi magokhoz rögzíti.</string> + <string name="perf_client_cores">Kliensmagok</string> + <string name="perf_client_cores_on">Magok a Steamhez.</string> + <string name="perf_client_cores_off">Kapcsolja be a felülírást a kiválasztáshoz.</string> + <string name="perf_game_cores_group">Játékmagok</string> + <string name="perf_game_cores">Játékmagok</string> + <string name="perf_game_cores_hint">Magok a játékokhoz. Ha mind kiválasztva van, az Android szabadon ütemez.</string> + <string name="perf_client_ui">Kliens felhasználói felület</string> + <string name="perf_glthread">Szálalt GL</string> + <string name="perf_glthread_hint">Reszponzívabbá teheti a Steam menüit.</string> + <string name="perf_zink">Zink: lusta leírók</string> + <string name="perf_zink_hint">Leírópuffer nélküli illesztőprogramokhoz ajánlott.</string> + <string name="perf_noglerror">GL-hibaellenőrzések kihagyása</string> + <string name="perf_noglerror_hint">Kikapcsolja a hívásonkénti GL-ellenőrzést.</string> + <string name="perf_gsrealtime">gamescope: valós idejű GPU-sor</string> + <string name="perf_gsrealtime_hint">Előnyben részesíti a kompozitor GPU-munkáját. Egyenletesebbé teheti a képkocka-ütemezést, kisebb költséggel a játékoknak.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">A GPU legmagasabb órajelen tartása</string> + <string name="perf_gpuclock_hint">Csak Adreno. Kevesebb akadás, nagyobb akkumulátorhasználat és hőtermelés.</string> + <string name="perf_fixes">Munkamenet-javítások</string> + <string name="perf_sysmem">Turnip: sysmem renderelés</string> + <string name="perf_sysmem_hint">Gyorsabb a Steamhez és a legtöbb játékhoz. Adreno 710/720/722-n szükséges.</string> + <string name="perf_xalia">A Steam xalia segítőjének kihagyása</string> + <string name="perf_fastsync">droiddeck-ntsync (kísérleti)</string> + <string name="perf_fastsync_hint">A Linux ntsync illesztőprogram felhasználói szintű helyettesítője, teszteléshez. Amíg be van kapcsolva, a Proton ezt használja a droiddeck-esync és a droiddeck-fsync helyett. A Proton Experimental ARM64-gyel, a GE-Protonnal és a proton-cachyosszal működik. A következő munkamenettől érvényes.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync előre</string> + <string name="perf_fsync_hint">A Proton saját fsyncjét futtatja az eredeti Proton-fájlokon, felhasználói szinten biztosított futex_waitv-vel, így nincs szükség csomagra, és azonnal működik, amint a Steam frissíti a Protont. Amíg be van kapcsolva, a játékok ezt használják a droiddeck-esync előtt, kivéve azt a néhányat, amelyet a Proton kizár az fsyncből. A következő munkamenettől érvényes.</string> + <string name="perf_esync_hint">A játékok Windows-szálai eventfd-n és megosztott memórián keresztül várnak egymásra, a wineserveren át vezető körök helyett. Egy az adott Proton buildhez készült droiddeck-esync csomagot használ; a csomagok automatikusan letöltődnek. Amíg egy új Proton buildhez nincs csomag, a droiddeck-fsync helyettesíti. A következő munkamenettől érvényes.</string> + <string name="perf_xalia_hint">Kikapcsolja a Proton kontroller-navigációs segédjét, amely minden játékban CPU-t használ.</string> + <string name="perf_seccomp">A proot futtatása seccomp nélkül</string> + <string name="perf_seccomp_hint">Javíthatja a hiányzó szisztémahívás-hibákat egyes kerneleken. Csökkentheti a teljesítményt.</string> + <string name="perf_fastpath">proot gyors útvonal</string> + <string name="perf_fastpath_needs_seccomp">A proot seccomp-szűrőjét igényli.</string> + <string name="perf_fastpath_hint">Gyorsabb fájlkeresés és kevesebb akadás. Kapcsolja ki, ha egy program nem találja a fájljait.</string> + <string name="perf_identity">Munkamenet-azonosítás</string> + <string name="perf_hostname">Hosztnév</string> + <string name="perf_hostname_hint">A név, amelyet a Steam ehhez az eszközhöz lát. Üresen hagyva visszaállítja: %1$s.</string> + <string name="perf_hostname_invalid">Csak betűk, számok és belső kötőjelek, legfeljebb 63 karakter.</string> + <string name="perf_phantom_warning">Az Android be van állítva, hogy kilője ezt a munkamenetet</string> + <string name="driver_installed">Telepítve</string> + <string name="driver_available">Letölthető</string> + <string name="driver_nothing_new">Nincs semmi új az utolsó ellenőrzés óta.</string> + <string name="driver_restore_builtin">Beépített illesztőprogramok visszaállítása</string> + <string name="driver_delete_builtin">Beépítve az alkalmazásba: ez eltávolítja a kicsomagolt fájljait, és elrejti. A Beépített illesztőprogramok visszaállítása visszahozza.</string> + <string name="driver_delete_bundle">Fele egy Android + Linux csomagnak: mindkét fele eltávolításra kerül. A használatban lévő illesztőprogram visszaáll az alapértelmezettre.</string> + <string name="driver_delete_plain">A fájljai eltávolításra kerülnek az alkalmazásból.</string> + <string name="driver_delete_in_use">A fájljai eltávolításra kerülnek. Használatban van, ezért az alapértelmezett veszi át a szerepét.</string> + <string name="drawer_page_display">Kijelző</string> + <string name="drawer_page_effects">Effektek</string> + <string name="drawer_page_controls">Vezérlők</string> + <string name="drawer_page_components">Összetevők</string> + <string name="drawer_page_controller">Kontroller</string> + <string name="drawer_page_settings">Beállítások</string> + <string name="drawer_page_desc">%1$s oldal</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Asztal</string> + <string name="drawer_steam_menu">Steam-menü</string> + <string name="drawer_quick_access">Gyors hozzáférés</string> + <string name="drawer_open_qam">Gyors hozzáférés menü megnyitása</string> + <string name="drawer_prev_page">Előző oldal</string> + <string name="drawer_next_page">Következő oldal</string> + <string name="drawer_hud">Teljesítmény HUD</string> + <string name="drawer_scaling">Méretezési mód</string> + <string name="drawer_scaling_sharpness">Méretezési élesség</string> + <string name="drawer_effects">Képernyőeffektek</string> + <string name="drawer_effects_hint">Élőben. Kis GPU-időt igényel, amíg bármelyik effekt be van kapcsolva.</string> + <string name="drawer_look">Előbeállítások</string> + <string name="drawer_look_custom">Egyéni</string> + <string name="drawer_effects_advanced">Speciális</string> + <string name="widgets_expanded">Kibontva</string> + <string name="widgets_collapsed">Összecsukva</string> + <string name="drawer_cas">Élesítés (CAS)</string> + <string name="drawer_cas_level">Élesítés szintje</string> + <string name="drawer_fake_hdr">Hamis HDR</string> + <string name="drawer_deband">Csíkmentesítés</string> + <string name="drawer_deband_strength">Csíkmentesítés erőssége</string> + <string name="drawer_brightness">Fényerő</string> + <string name="drawer_contrast">Kontraszt</string> + <string name="drawer_gamma">Gamma</string> + <string name="drawer_saturation">Telítettség</string> + <string name="drawer_fxaa">FXAA (élsimítás)</string> + <string name="drawer_toon">Rajzfilmes körvonalak</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Textúraszűrés</string> + <string name="drawer_texture_hint">DirectX 9–11 játékok, a következő indításuktól.</string> + <string name="drawer_anisotropy">Anizotróp szűrés</string> + <string name="drawer_texture_sharpness">Textúraélesség</string> + <string name="drawer_texture_sharpness_note">Negatív mip-eltolás: élesebb textúrák, némi csillogással. Az Automatikus illeszkedik ahhoz, mennyire van felnagyítva a munkamenet a képernyőhöz.</string> + <string name="drawer_touch_auto">Automatikus (%1$s)</string> + <string name="drawer_keyboard">Billentyűzet</string> + <string name="drawer_pc_keyboard">PC-billentyűzet</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">Második képernyő</string> + <string name="drawer_second_screen_mode">Mód</string> + <string name="drawer_second_screen_shows">Megjelenít</string> + <string name="drawer_android_apps">Android-alkalmazások</string> + <string name="drawer_launch_app">Alkalmazás indítása</string> + <string name="common_hide">Elrejtés</string> + <string name="common_show">Megjelenítés</string> + <string name="drawer_no_apps">Nincs indítható alkalmazás</string> + <string name="drawer_session_behavior">Munkamenet-viselkedés</string> + <string name="drawer_suspend_hint">Most és a jövőbeli, ebben a módban futó munkamenetekre is érvényes.</string> + <string name="drawer_next_session">Következő munkamenet</string> + <string name="drawer_support">Támogatás</string> + <string name="drawer_logs">Munkamenet-naplók</string> + <string name="drawer_logs_hint">Csatolja egy hibajelentéshez</string> + <string name="drawer_share_logs">Naplók megosztása</string> + <string name="drawer_background">Háttér</string> + <string name="drawer_stop">Leállítás</string> + <string name="drawer_stop_title">Leállítja a munkamenetet?</string> + <string name="drawer_running_now">Jelenleg fut</string> + <string name="drawer_no_game">Nem fut játék Protonon</string> + <string name="drawer_selected_profile">Játékprofil</string> + <string name="drawer_selected_profile_none">A profil részletei még nem állnak rendelkezésre.</string> + <string name="drawer_profile_refresh_failed_initial">Nem sikerült betölteni a játékprofilokat. Automatikus újrapróbálkozás.</string> + <string name="drawer_profile_refresh_failed_stale">Nem sikerült frissíteni a játékprofilokat. Az utolsó elérhető adatok jelennek meg; automatikus újrapróbálkozás.</string> + <string name="drawer_profile_inspect_hint">A következő indításkor használt mentett beállítások megtekintése és szerkesztése.</string> + <string name="drawer_profile_default">Az Alapértelmezett profil használatban</string> + <string name="drawer_profile_custom">Egyéni profil</string> + <string name="drawer_component_inherited">A %1$s az Alapértelmezett profilból használatban.</string> + <string name="drawer_game_components_note">A módosítások a %1$s profiljába kerülnek mentésre, és a játék következő indításakor lépnek érvénybe.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d játékfelülírás</item> + <item quantity="other">%1$d játékfelülírás</item> + </plurals> + <string name="drawer_no_proton">Még nincs telepítve Proton.</string> + <string name="drawer_game_running">játék fut</string> + <string name="drawer_reapplied">újraalkalmazva indításkor: %1$s</string> + <string name="drawer_original">Eredeti</string> + <string name="drawer_components_note">Importálás és törlés az Összetevők oldalon.</string> + <string name="setup_check_done">Kész</string> + <string name="setup_check_warn">Figyelmet igényel</string> + <string name="setup_check_busy">Feldolgozás</string> + <string name="setup_limit_adb_failed">Ellenőrizze a vezeték nélküli hibakeresés IP-címét és portját, vagy párosítsa újra, ha az Android eltávolította ezt az eszközt.</string> + <string name="setup_install">Telepítés</string> + <string name="setup_manage">Kezelés</string> + <string name="setup_tab_overview">Áttekintés</string> + <string name="setup_tab_controller">Kontroller</string> + <string name="setup_tab_session">Munkamenet</string> + <string name="setup_tab_launcher">Indítóalkalmazás</string> + <string name="setup_title">Beállítás</string> + <string name="setup_all_set">● Minden rendben</string> + <string name="setup_n_ready">%1$d/%2$d kész</string> + <string name="setup_system_check">Rendszerellenőrzés</string> + <string name="setup_gpu_supported">Az eszköz támogatott</string> + <string name="setup_gpu_untested">Nem tesztelt GPU</string> + <string name="setup_gpu_unsupported">A GPU nem támogatott</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">A Steamnek Adreno-illesztőprogram szükséges; a %1$s fekete képernyőt jeleníthet meg</string> + <string name="setup_runtime">Linux futtatókörnyezet</string> + <string name="setup_runtime_progress">%1$s · %2$d%%</string> + <string name="setup_runtime_missing">Nincs telepítve · kb. 3 GB, az első Játék gombnyomáskor települ</string> + <string name="setup_installed">Telepítve</string> + <string name="setup_runtime_update">%1$s · elérhető frissítés</string> + <string name="setup_runtime_current">%1$s · naprakész</string> + <string name="setup_limit">Gyermekfolyamat-korlát</string> + <string name="setup_limit_on">Be · az Android bezárhatja a Steamet. A kikapcsolása egy percet vesz igénybe.</string> + <string name="setup_limit_unset">Nincs beállítva · az Android bezárhatja a Steamet. A kikapcsolása egy percet vesz igénybe.</string> + <string name="setup_limit_unknown">Nem sikerült ellenőrizni · a kikapcsolása egy percet vesz igénybe.</string> + <string name="setup_fix_it">Javítás</string> + <string name="setup_details">Részletek</string> + <string name="setup_dev_options">Fejlesztői beállítások</string> + <string name="setup_use_wireless">Vezeték nélküli hibakeresés használata</string> + <string name="setup_check_again">Ellenőrzés újra</string> + <string name="setup_limit_turn_on">Korlát bekapcsolása</string> + <string name="setup_account">Steam-fiók</string> + <string name="setup_signed_in_offline">Bejelentkezve mint %1$s · offline mód</string> + <string name="setup_signed_in">Bejelentkezve mint %1$s</string> + <string name="setup_sign_in">Nyomja meg a Játék gombot, és jelentkezzen be a Steambe</string> + <string name="setup_tools">Eszközök</string> + <string name="setup_controller">Kontroller</string> + <string name="setup_controller_unavailable">A kontrollerbeállítások nem érhetők el.</string> + <string name="setup_session">Munkamenet</string> + <string name="setup_logs">Munkamenet-naplók</string> + <string name="setup_logs_hint">Minden munkamenet után mentve</string> + <string name="setup_agent_commands">Hibakeresési eszközök</string> + <string name="setup_latest_logs">Legutóbbi munkamenet-naplók</string> + <string name="setup_saved_logs">Mentett munkamenet-naplók</string> + <string name="setup_saved_logs_hint">A legújabb %1$d-t tartja meg</string> + <string name="setup_clear_logs">Naplók törlése</string> + <string name="setup_offline">Offline mód</string> + <string name="setup_sign_in_first">Előbb jelentkezzen be a Steambe</string> + <string name="setup_launcher">Indítóalkalmazás</string> + <string name="setup_language">Nyelv</string> + <string name="setup_language_hint">A Steam is ezt használja, a következő indításától.</string> + <string name="setup_language_system">Rendszer alapértelmezett (%1$s)</string> + <string name="setup_language_system_short">Rendszer alapértelmezett</string> + <string name="setup_app_scale">Alkalmazásméret</string> + <string name="setup_app_scale_hint">A DroidDeck szövege és vezérlői</string> + <string name="setup_app_scale_default">%1$d%% (Alapértelmezett)</string> + <string name="setup_theme">Téma</string> + <string name="setup_theme_hint">Az indítóalkalmazás megjelenése</string> + <string name="setup_home">Használat kezdőképernyőként</string> + <string name="setup_home_on">A DroidDeck lehet a kezdőképernyő alkalmazás</string> + <string name="setup_home_off">A DroidDeck soha nem jelenik meg kezdőképernyő alkalmazásként</string> + <string name="setup_orientation">Tájolás</string> + <string name="setup_orientation_auto">Automatikus fekvő</string> + <string name="setup_orientation_landscape">Fekvő</string> + <string name="setup_orientation_reverse">Fordított fekvő</string> + <string name="setup_fullscreen">Teljes képernyő</string> + <string name="setup_fullscreen_on">Elrejti az Android állapot- és navigációs sávokat</string> + <string name="setup_fullscreen_off">Megjeleníti az Android állapot- és navigációs sávokat</string> + <string name="setup_default_home">Alapértelmezett kezdőképernyő alkalmazás</string> + <string name="setup_choose_home">Válasszon kezdőképernyő alkalmazást</string> + <string name="setup_choose">Kiválasztás</string> + <string name="setup_linux_apps">Linux-alkalmazások (béta)</string> + <string name="setup_tool_files">Fájlok</string> + <string name="setup_tool_files_hint">Fájlok tallózása és kezelése</string> + <string name="setup_tool_protons">Proton-verziók</string> + <string name="setup_tool_protons_hint">ARM64 Proton-buildek telepítése</string> + <string name="setup_tool_performance">Teljesítmény</string> + <string name="setup_tool_performance_hint">CPU-mag hozzárendelés</string> + <string name="setup_tool_roms">ROMs mappa</string> + <string name="setup_tool_roms_hint">Hol vannak tárolva az emulátoros játékok</string> + <string name="setup_card_components">Összetevők</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Vezérlők</string> + <string name="setup_card_controls_hint">Gombkiosztás</string> + <string name="store_tab_discover">Felfedezés</string> + <string name="store_tab_search">Keresés</string> + <string name="store_tab_installed">Telepítve</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Áruház</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● A Flatpak kész</string> + <string name="store_needs_runtime">Előbb telepítse a Linux futtatókörnyezetet a Beállításból.</string> + <string name="store_setup_title">Flatpak beállítása</string> + <string name="store_setup_text">Hozzáadja a Flatpakot, és csatlakoztatja a Flathubhoz (kb. 20 MB). Az első alkalmazás letölti a saját futtatókörnyezetét is.</string> + <string name="store_setting_up">Beállítás…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Frissítések</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d%%</string> + <string name="store_starting">Indítás…</string> + <string name="store_categories">Kategóriák</string> + <string name="store_unreachable">Nem sikerült elérni a Flathubot. Ellenőrizze a kapcsolatot, és próbálja újra.</string> + <string name="store_try_again">Próbálja újra</string> + <string name="store_search_label">Keresés a Flathubon</string> + <string name="store_search_placeholder">Firefox, játékok, emulátorok…</string> + <string name="store_search">Keresés</string> + <string name="store_all">Összes</string> + <string name="store_searching">Keresés…</string> + <string name="store_search_hint">Keressen név szerint, vagy válasszon kategóriát. Csak ARM64-es alkalmazások jelennek meg.</string> + <string name="store_no_results">Nincs ennek megfelelő ARM64-es alkalmazás a Flathubon.</string> + <string name="store_results">Eredmények</string> + <string name="store_check_updates">Frissítések keresése</string> + <string name="store_update_all">Összes frissítése (%1$d)</string> + <string name="store_all_current">● Minden naprakész</string> + <string name="store_no_apps">Még nincsenek alkalmazások. Keressen a Felfedezés vagy a Keresés alatt.</string> + <string name="store_setup_first_note">Állítsa be a Flatpakot az alkalmazások telepítéséhez.</string> + <string name="store_update_available_prefix">Elérhető frissítés · %1$s</string> + <string name="store_open">Megnyitás</string> + <string name="store_details">Részletek</string> + <string name="store_remove">Eltávolítás</string> + <string name="store_press_again">Nyomja meg újra az eltávolításhoz</string> + <string name="store_installing">Telepítés…</string> + <string name="store_install">Telepítés</string> + <string name="store_setup_first">Előbb állítsa be a Flatpakot</string> + <string name="store_download_size">%1$s letöltés</string> + <string name="store_installed_chip">● Telepítve</string> + <string name="store_details_failed">Nem sikerült betölteni ennek az alkalmazásnak a Flathub-oldalát.</string> + <string name="store_loading">Betöltés a Flathubról…</string> + <string name="store_no_arm">Nincs ARM64-es build a Flathubon, így itt nem telepíthető.</string> + <string name="store_screenshots">Képernyőképek</string> + <string name="store_about">Névjegy</string> + <string name="store_app_id">Alkalmazásazonosító</string> + <string name="store_license">Licenc</string> + <string name="store_runtime">Futtatókörnyezet</string> + <string name="store_installed_size">Telepített méret</string> + <string name="store_installed_size_value">%1$s (plusz a megosztott futtatókörnyezete)</string> + <string name="store_website">Weboldal</string> + <string name="store_verified">Ellenőrzött</string> + <string name="upd_title">Frissítések</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Ellenőrizve: %1$s</string> + <string name="upd_not_checked">Még nincs ellenőrizve</string> + <string name="upd_perm_title">Engedélyezze, hogy a DroidDeck magát frissítse</string> + <string name="upd_perm_text">A következő képernyőn kapcsolja be az „Engedélyezés ebből a forrásból” lehetőséget, majd térjen vissza. A frissítés önműködően folytatódik.</string> + <string name="upd_open_settings">Beállítások megnyitása</string> + <string name="upd_not_now">Most nem</string> + <string name="upd_signed_status">Másként aláírva</string> + <string name="upd_signed_headline">Nem frissíthető a helyén</string> + <string name="upd_signed_detail">Ez a másolat nincs a DroidDeck kiadási kulcsával aláírva, így a frissítések nem telepíthetők rá. Távolítsa el az átváltáshoz.</string> + <string name="upd_available">Elérhető frissítés</string> + <string name="upd_blocked_status">Nem telepíthető</string> + <string name="upd_switch">Kész az átváltásra</string> + <string name="upd_ahead_status">A Stable előtt</string> + <string name="upd_ahead_headline">A Stable előtt áll</string> + <string name="upd_ahead_detail">Ez a build újabb, mint a legújabb Stable. A Stable következő kiadásával arra fog átváltani.</string> + <string name="upd_gone_status">A teszt lezárult</string> + <string name="upd_gone_headline">Ez a teszt lezárult</string> + <string name="upd_gone_detail">A javítása beolvasztva vagy elvetve lett. Kövesse a Previewt a legújabb javításokért.</string> + <string name="upd_current_status">Naprakész</string> + <string name="upd_current_headline">A legújabb %1$s van telepítve</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Frissítés%1$s</string> + <string name="upd_install_button">Telepítés%1$s</string> + <string name="upd_install_anyway">A Stable %1$s telepítése mindenképpen</string> + <string name="upd_follow_preview">Preview követése</string> + <string name="upd_stop_session">Állítsa le a futó munkamenetet a frissítéshez.</string> + <string name="upd_less">Kevesebb</string> + <string name="upd_more">Több</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Új Preview build</string> + <string name="upd_new_test">PR #%1$d teszt build</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">FRISSÍTÉSI CSATORNA</string> + <string name="upd_stable">Stable</string> + <string name="upd_stable_hint">Tesztelt kiadások, a legtöbb felhasználónak</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Preview</string> + <string name="upd_preview_hint">A legújabb javítások, a Stable előtt</string> + <string name="upd_preview_current">Naprakész</string> + <string name="upd_tests">Teszt buildek</string> + <string name="upd_tests_hint">Próbáljon ki egy javítást a kiadása előtt</string> + <string name="upd_tests_none">Jelenleg nincs</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Nincs buildelve ehhez a DroidDeck-másolathoz</string> + <string name="upd_updated">Frissítve: %1$s</string> + <string name="upd_test_name">a PR #%1$d teszt</string> + <string name="upd_while_ago">egy ideje</string> + <string name="upd_just_now">épp most</string> + <plurals name="upd_tests_count"> + <item quantity="one">%1$d kipróbálható</item> + <item quantity="other">%1$d kipróbálható</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="one">Változások a buildje óta · %1$d build lemaradás</item> + <item quantity="other">Változások a buildje óta · %1$d build lemaradás</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="one">Legutóbbi változások · %1$d build</item> + <item quantity="other">Legutóbbi változások · %1$d build</item> + </plurals> + <string name="gate_title">Egy Android-beállítást kell módosítani</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Az Android leállítja azokat az alkalmazásokat, amelyek sok háttérfolyamatot indítanak, a Steam pedig tucatnyit indít. Amíg ez nem változik, a játékok hibaüzenet nélkül bezáródnak.</string> + <string name="gate_fix_for_me">Javítsa ezt helyettem</string> + <string name="gate_toggle_steps">Kapcsolja be a „Gyermekfolyamat-korlátozások letiltása” kapcsolót, majd térjen vissza. Hagyja bekapcsolva a Fejlesztői beállításokat, különben a kapcsoló visszakapcsol.</string> + <string name="gate_open_dev">Fejlesztői beállítások megnyitása</string> + <string name="gate_no_toggle">Ehhez az Android-verzióhoz nincs kapcsoló, ezért a DroidDeck a vezeték nélküli hibakeresésen keresztül módosítja. Körülbelül egy percet vesz igénybe, számítógép nélkül.</string> + <string name="gate_needs_wifi">A vezeték nélküli hibakereséshez Wi-Fi szükséges. Bármelyik hálózat megfelel; internetkapcsolat nem szükséges.</string> + <string name="gate_needs_notifications">A párosítási kód egy értesítésben jelenik meg. Engedélyezze az értesítéseket a DroidDecknek, vagy használja az Egyéb módokat.</string> + <string name="gate_allow_notifications">Értesítések engedélyezése</string> + <string name="gate_step1">Nyissa meg: Beállítások → Fejlesztői beállítások → Vezeték nélküli hibakeresés, és kapcsolja be</string> + <string name="gate_step2">Koppintson az „Eszköz párosítása párosítási kóddal” lehetőségre</string> + <string name="gate_step3">Írja be a kódot a DroidDeck értesítésébe</string> + <string name="gate_waiting">Várakozás a párosítási felugró ablakra…</string> + <string name="gate_code_needed">Felugró ablak megtalálva. Írja be a kódját az értesítésbe.</string> + <string name="gate_status">Állapot</string> + <string name="gate_polling">Ellenőrzés 2 másodpercenként. Ez az oldal bezárul, amikor kikapcsol.</string> + <string name="gate_other_ways">Egyéb módok</string> + <string name="gate_other_toggle">Vezeték nélküli hibakeresés, manuális cím, számítógépes ADB</string> + <string name="gate_other_no_toggle">Manuális cím, számítógépes ADB, Fejlesztői beállítások</string> + <string name="gate_use_wireless_instead">Vezeték nélküli hibakeresés használata helyette</string> + <string name="gate_some_roms">Néhány ROM mindenképpen hozzáadja a kapcsolót. Keresse a „gyermekfolyamat” kifejezést a Fejlesztői beállításokban.</string> + <string name="gate_manual_hint">Ha az értesítés nem működik, írja be saját kezűleg a felugró ablak címét és kódját.</string> + <string name="gate_enter_manually">Cím kézi megadása</string> + <string name="gate_from_computer">Egy ADB-vel rendelkező számítógépről:</string> + <string name="gate_copy_command">Parancs másolása</string> + <string name="gate_choose_display">Válassza ki, melyik kijelzőn nyíljon meg az Android Beállítások.</string> + <string name="gate_main_screen">Fő képernyő</string> + <string name="wadb_finding_port">Párosítva. A vezeték nélküli hibakeresés portjának keresése…</string> + <string name="wadb_enter_port">Párosítva. Adja meg a portot a Beállításokból.</string> + <string name="wadb_applying">Eszköz megtalálva. A beállítás alkalmazása…</string> + <string name="wadb_apply_failed">Nem sikerült automatikusan alkalmazni. Ellenőrizze az alábbi címet, és próbálja újra. %1$s</string> + <string name="wadb_title_wireless">Vezeték nélküli hibakeresés</string> + <string name="wadb_title_done">A gyermekfolyamat-korlát frissítve</string> + <string name="wadb_title">A gyermekfolyamat-korlát módosítása</string> + <string name="wadb_lede_compact0">Ha a beállítás hiányzik, párosítson a vezeték nélküli hibakeresésen keresztül.</string> + <string name="wadb_lede_compact1">Párosítva. Ellenőrizze az IP-címet és a portot a vezeték nélküli hibakeresésben.</string> + <string name="wadb_lede_done">A gyermekfolyamat-korlát frissítve.</string> + <string name="wadb_lede0">Használja a 2. lépést, ha a Fejlesztői beállításokban nincs gyermekfolyamat-beállítás. Nyissa meg: Fejlesztői beállítások → Vezeték nélküli hibakeresés → Eszköz párosítása párosítási kóddal.</string> + <string name="wadb_lede1">Párosítva. Használja az alábbi címet, vagy frissítse a Vezeték nélküli hibakeresés → IP-cím és port menüből.</string> + <string name="wadb_lede_done_long">A gyermekfolyamat-korlát frissítve a vezeték nélküli hibakeresésen keresztül.</string> + <string name="wadb_confirmed_on">Az Android megerősítette, hogy a korlát be van kapcsolva.</string> + <string name="wadb_confirmed_off">Az Android megerősítette, hogy a korlát ki van kapcsolva.</string> + <string name="wadb_pair_group">2 · Vezeték nélküli hibakeresés párosítása</string> + <string name="wadb_pair_address">A párosítási felugró ablak IP-címe és portja</string> + <string name="wadb_pin">Párosítási kód (PIN)</string> + <string name="wadb_pin_placeholder">6 számjegy</string> + <string name="wadb_pair">Párosítás</string> + <string name="wadb_apply_group">2 · Alkalmazás vezeték nélküli hibakeresésen keresztül</string> + <string name="wadb_connect_address">Kapcsolat IP-címe és portja</string> + <string name="wadb_pair_again">Párosítás újra</string> + <string name="wadb_turn_off">Korlát kikapcsolása</string> + <string name="wadb_other_ways">Egyéb módok a módosítására</string> + <string name="wadb_dev_try_first">1 · Fejlesztői beállítások (próbálja ezt először)</string> + <string name="wadb_restrict_on">Kapcsolja be a „Gyermekfolyamatok korlátozása” lehetőséget.</string> + <string name="wadb_restrict_off">Kapcsolja ki a „Gyermekfolyamatok korlátozása” lehetőséget.</string> + <string name="wadb_computer_last">3 · Számítógépes ADB (végső megoldás)</string> + <string name="wadb_only_if">Csak ha a vezeték nélküli hibakeresés nem elérhető.</string> + <string name="wadb_computer_group">3 · Számítógépes ADB (végső megoldás)</string> + <string name="ctrl_osc_hint">Mikor jelennek meg az érintésvezérlők egy munkamenetben</string> + <string name="ctrl_color">Szín</string> + <string name="ctrl_color_hint">A képernyőn megjelenő gombok árnyalata</string> + <string name="ctrl_custom">Egyéni</string> + <string name="ctrl_opacity">Átlátszatlanság</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">Gombméret</string> + <string name="ctrl_size_hint" formatted="false">A 100%% az alapértelmezett méret</string> + <string name="ctrl_stick_click">Kar kattintása</string> + <string name="ctrl_stick_click_hint">Koppintson duplán, és tartsa lenyomva a kart az L3 vagy R3 érdekében</string> + <string name="ctrl_adaptive">Adaptív karok</string> + <string name="ctrl_rumble">Játékvibráció</string> + <string name="ctrl_steam_button">Steam gomb</string> + <string name="ctrl_qam_button">QAM gomb</string> + <string name="ctrl_keyboard_button">Billentyűzet gomb</string> + <string name="ctrl_select_steam">Nyomja meg a Select gombot a Steam-menü megnyitásához</string> + <string name="ctrl_select_steam_hint">A View/Select helyett.</string> + <string name="setup_animations">Animációk</string> + <string name="ctrl_adaptive_hint">A karok ott jelennek meg, ahol megérinti őket, és elengedéskor elrejtőznek</string> + <string name="ctrl_layout">Elrendezés</string> + <string name="ctrl_layout_custom">Egyéni pozíciók mentve</string> + <string name="ctrl_layout_default">Ehhez a képernyőhöz illesztve</string> + <string name="ctrl_edit">Szerkesztés</string> + <string name="ctrl_reset">Visszaállítás</string> + <string name="ctrl_mapping">Gombkiosztás</string> + <string name="ctrl_mapping_default">Minden gomb a saját bemenetét küldi</string> + <string name="ctrl_mapping_count">%1$d/%2$d gomb átkiosztva</string> + <string name="ctrl_configure">Beállítás</string> + <string name="ctrl_reset_all">Kontroller visszaállítása</string> + <string name="ctrl_reset_all_hint">Visszaállítja az összes kontrollerbeállítást és az elrendezést</string> + <string name="ctrl_mapping_lede">Mit küld a játéknak az egyes képernyőgombok. A Rejtett eltávolítja a gombot.</string> + <string name="ctrl_onscreen_buttons">Képernyőgombok</string> + <string name="ctrl_defaults">Alapértelmezések</string> + <string name="ctrl_reset_mapping">Kiosztás visszaállítása</string> + <string name="content_android_apps">Android-alkalmazások</string> + <string name="content_apps_count">%1$d alkalmazás</string> + <string name="content_no_android_apps">Nem található indítható Android-alkalmazás.</string> + <string name="content_desktop_installed">● Az Asztal telepítve</string> + <string name="content_desktop_first_open">Az első megnyitáskor települ</string> + <string name="content_available">Telepíthető</string> + <string name="content_not_installed">Nincs telepítve.</string> + <string name="content_not_installed_suffix">%1$s · nincs telepítve</string> + <string name="content_open_named">%1$s megnyitása</string> + <string name="content_games">Játékok</string> + <string name="content_choose_roms">Válasszon ROMs mappát.</string> + <string name="content_retroarch_roms">Tallózzon a /root/ROMs mappához a RetroArchban.</string> + <string name="content_add_roms">Adjon hozzá %1$s játékokat ide: ROMs/%2$s.</string> + <string name="content_install_named">%1$s telepítése</string> + <string name="content_stop_to_install">Állítsa le a munkamenetet a telepítéshez</string> + <string name="content_runtime_required">Futtatókörnyezet szükséges</string> + <string name="content_loading_details">Telepítési részletek betöltése…</string> + <string name="content_details_unavailable">A telepítési részletek nem érhetők el. Próbálja újra később.</string> + <string name="content_needs_runtime">Előbb telepítse a Linux futtatókörnyezetet a Beállításból.</string> + <string name="content_stop_first">Állítsa le a futó munkamenetet az asztali alkalmazások telepítéséhez.</string> + <string name="content_install_into">A %1$s-t telepíti a Linux-asztalba.</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">Az elrejtés eltávolítja az Asztalról. A fájljai megmaradnak a Linux futtatókörnyezetben.</string> + <string name="content_rom_gone">Ez a játék már nincs a ROMs mappában.</string> + <string name="content_desktop_system">Asztal · %1$s</string> + <string name="content_launch_in">Indítás ebben: %1$s</string> + <string name="content_also_in">Ebben is: %1$s</string> + <string name="content_page_gone">Ez az oldal már nincs meg. Válasszon egy szakaszt a bal oldalon.</string> + <string name="content_open_desktop">Asztal megnyitása</string> + <string name="content_install_open_desktop">Asztal telepítése és megnyitása</string> + <string name="desktop_remove_title">Asztal eltávolítása</string> + <string name="desktop_remove_text">Eltávolítja az asztalt és a hozzá tartozó alkalmazásokat. A játékok, a hozzáadott alkalmazások, a beállítások és a megosztott könyvtárak megmaradnak.</string> + <string name="desktop_removing">Asztal eltávolítása</string> + <string name="desktop_removed">Asztal eltávolítva</string> + <string name="desktop_cleaning_legacy">Régi LXQt-fájlok eltávolítása</string> + <string name="desktop_removal_unknown">Frissítse a DroidDecket, mielőtt eltávolítja ezt az asztali verziót.</string> + <string name="content_linux_desktop">Linux-asztal</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox és az emulátorai</string> + <string name="games_empty">A Steamben telepített játékok a munkamenet után itt jelennek meg.</string> + <string name="games_play_steam">Steam elindítása</string> + <string name="games_launch_settings">Indítási beállítások</string> + <string name="games_launch">Indítás</string> + <string name="games_files">Játékfájlok</string> + <string name="games_prefix">Proton prefix</string> + <string name="games_saves_looking">Mentések keresése…</string> + <string name="games_saves_none">Nem található mentésmappa név szerint. Az exportálás a teljes felhasználói mappát veszi.</string> + <string name="games_saves_folders">%1$d mentésmappa</string> + <string name="games_saves_summary">%1$s · %2$d fájl · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">Mentések kezelése</string> + <string name="games_saves">Játékmentések</string> + <string name="games_import_saves">Mentések importálása…</string> + <string name="games_export_gamehub">Exportálás GameHub zipként…</string> + <string name="games_export_winlator">Exportálás Winlator zipként…</string> + <string name="games_open_save_folder">Mentésmappa megnyitása</string> + <string name="games_never_played">Még nem játszott</string> + <string name="games_runtime_busy_percent">Futtatókörnyezet dolgozik · %1$d%%</string> + <string name="games_runtime_busy">Futtatókörnyezet dolgozik</string> + <string name="games_runtime_first_play">A futtatókörnyezet az első Játék gombnyomáskor települ</string> + <string name="games_runtime_update">Elérhető futtatókörnyezet-frissítés</string> + <string name="games_runtime_ready">● A futtatókörnyezet kész</string> + <string name="games_last_played">Utoljára játszva: %1$s</string> + <string name="games_internal">Belső tárhely</string> + <string name="games_added">Hozzáadott játék</string> + <string name="games_custom">Egyéni játék</string> + <string name="games_store_storage">Játéktárhely</string> + <string name="gpu_auto_hint">A legjobb páros ehhez a GPU-hoz, naprakészen tartva</string> + <string name="gpu_manual_hint">Válasszon párost saját kezűleg</string> + <string name="gpu_checking">Illesztőprogramok keresése…</string> + <string name="gpu_none_listed">Még nincs felsorolt illesztőprogram. Frissítsen a kereséshez.</string> + <string name="gpu_hide_other">Más GPU-k illesztőprogramjainak elrejtése</string> + <string name="gpu_show_other">Más GPU-k illesztőprogramjainak megjelenítése</string> + <string name="gpu_advanced">Speciális</string> + <string name="gpu_set_with_display">A megjelenítő-illesztőprogrammal együtt beállítva: a %1$s mindkettőt tartalmazza.</string> + <string name="gpu_set_with_runtime">A futtatókörnyezet-illesztőprogrammal együtt beállítva: a %1$s mindkettőt tartalmazza.</string> + <string name="gpu_runtime_hint">A játékok és az asztal használja.</string> + <string name="gpu_runtime_default">Futtatókörnyezet alapértelmezése</string> + <string name="gpu_display_hint">A képkockákat rajzolja a képernyőre. A DroidDeck újraindítása után lép érvénybe.</string> + <string name="gpu_auto_by_gpu">Automatikus – GPU szerint kiválasztva</string> + <string name="gpu_manage">Letöltés, importálás vagy törlés…</string> + <string name="gpu_in_use_caps">HASZNÁLATBAN</string> + <string name="gpu_builtin">A DroidDeckbe épített illesztőprogramok</string> + <string name="gpu_matched_pair">A futtatókörnyezet- és megjelenítő-illesztőprogramok együtt, párként beállítva.</string> + <string name="gpu_recommended_for">Ehhez a GPU-hoz ajánlott: %1$s.</string> + <string name="gpu_refresh_hint">Frissítsen az ehhez a GPU-hoz tartozó illesztőprogramok kereséséhez.</string> + <string name="gpu_recommended_caps">AJÁNLOTT</string> + <string name="gpu_unavailable">Nem elérhető</string> + <string name="load_runtime">Linux futtatókörnyezet</string> + <string name="load_runtime_detail">A Linux futtatókörnyezet előkészítése</string> + <string name="load_session">Munkamenet</string> + <string name="load_session_detail">Kijelző és hang indítása</string> + <string name="load_steam">Steam-kliens</string> + <string name="load_steam_detail">A Steam és kompatibilitási eszközeinek ellenőrzése</string> + <string name="load_opening_steam">Steam megnyitása</string> + <string name="load_opening_steam_detail">Várakozás a Steam első képkockájára</string> + <string name="load_desktop">Asztal</string> + <string name="load_desktop_detail">Az asztal előkészítése</string> + <string name="load_opening">Megnyitás</string> + <string name="load_opening_detail">Várakozás az első képkockára</string> + <string name="load_starting_steam">Steam indítása</string> + <string name="load_ended">A munkamenet véget ért</string> + <string name="load_step">%1$d. lépés a %2$d-ből</string> + <string name="load_steam_paused">A Steam szünetel</string> + <string name="deck_tab_grips">Fogók</string> + <string name="deck_tab_left">Bal panel</string> + <string name="deck_tab_right">Jobb panel</string> + <string name="deck_tab_both">Mindkettő</string> + <string name="deck_tab_gamepad">Kontroller</string> + <string name="deck_surface_stick">Kar</string> + <string name="deck_surface_trackpad">Érintőpad</string> + <string name="deck_surface_switch">Váltás a kar és az érintőpad között</string> + <string name="deck_more_controls">További Deck-vezérlők</string> + <string name="deck_panels">PANELEK</string> + <string name="deck_close">Második képernyős vezérlők bezárása</string> + <string name="deck_open_steam_menu">Steam-menü megnyitása</string> + <string name="deck_left_upper">Bal · felső</string> + <string name="deck_left_lower">Bal · alsó</string> + <string name="deck_right_upper">Jobb · felső</string> + <string name="deck_right_lower">Jobb · alsó</string> + <string name="deck_click">Kattintás</string> + <string name="deck_right_trackpad">Jobb érintőpad</string> + <string name="deck_left_trackpad">Bal érintőpad</string> + <string name="second_trackpad">Érintőpad</string> + <string name="second_show_keyboard">Billentyűzet megjelenítése</string> + <string name="second_hide_keyboard">Billentyűzet elrejtése</string> + <string name="second_terminal">Linux terminál</string> + <string name="second_terminal_not_ready">A Linux-munkamenet még nem áll készen a terminálra.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Játékok</string> + <string name="rail_desktop">Asztal</string> + <string name="rail_store">Áruház</string> + <string name="rail_components">Összetevők</string> + <string name="rail_apps">Alkalmazások</string> + <string name="rail_setup">Beállítás</string> + <string name="rail_updates">Frissítések</string> + <string name="rail_percent">%1$d%%</string> + <string name="rail_resume_named">%1$s folytatása</string> + <string name="roms_text">Minden munkamenetben /root/ROMs néven jelenik meg. A belső tárhely itt található: /root/Storage.</string> + <string name="roms_none">Nincs mappa kiválasztva</string> + <string name="roms_applies">A következő munkamenettől érvényes. SD-kártyák is működnek.</string> + <string name="roms_choose">Mappa kiválasztása</string> + <string name="roms_change">Mappa módosítása</string> + <string name="proton_lede">Telepítsen egy ARM64-es Proton-buildet, majd válassza ki játékonként itt: Steam → Tulajdonságok → Kompatibilitás.</string> + <string name="proton_stop_first">Állítsa le a futó munkamenetet egy kompatibilitási eszköz telepítéséhez vagy eltávolításához.</string> + <string name="proton_available">Elérhető buildek</string> + <string name="proton_installed">%1$s telepítve</string> + <string name="proton_queued">Egy korábbi kérésből függőben</string> + <string name="proton_not_installed">Nincs telepítve</string> + <string name="proton_update_available">Elérhető frissítés → %1$s</string> + <string name="proton_remove_older">Régi %1$s verziók eltávolítása</string> + <string name="proton_remove_older_body">Eltávolítja: %1$s. Az ezekre a verziókra beállított játékokat másik Protonra kell állítani.</string> + <string name="proton_remove_body">Az erre a verzióra beállított játékokat másik Protonra kell állítani.</string> + <string name="proton_install_now">Telepítés most</string> + <string name="proton_cancel_queued">Várakozó megszakítása</string> + <string name="pckb_android">Android billentyűzet</string> + <string name="pckb_hide">Elrejtés ✕</string> + <string name="main_launch_named">%1$s indítása</string> + <string name="main_choose_screen">Válasszon képernyőt. Másodlagos: %1$s.</string> + <string name="main_secondary_gone">A másodlagos kijelző már nem elérhető.</string> + <string name="main_primary">Elsődleges képernyő</string> + <string name="main_secondary">Másodlagos képernyő</string> + <string name="widgets_prev_tab">Előző lap</string> + <string name="widgets_next_tab">Következő lap</string> + <string name="widgets_on">Be</string> + <string name="widgets_all_n">Összes (%1$d)</string> + <string name="widgets_none">Nincs</string> + <string name="widgets_n_of_m">%1$d/%2$d</string> + <string name="widgets_off">Ki</string> + <string name="screen_bottom">Alsó képernyő</string> + <string name="game_env_applies">A következő játékindítástól érvényes. A játék profilja felülírja a közöset.</string> + <string name="game_env_other">Másik játék alkalmazásazonosító szerint…</string> + <string name="game_env_done">Kész</string> + <string name="game_env_default">Alapértelmezett</string> + <string name="game_env_off">Ki</string> + <string name="game_env_change">Módosítás…</string> + <string name="game_env_change_title">Változó módosítása</string> + <string name="game_env_restore_default">Visszaállítás alapértelmezettre</string> + <string name="game_env_eyebrow">Proton-játékok</string> + <string name="pip_title">Kép a képben</string> + <string name="pip_auto">Automatikus belépés a kép a képben módba</string> + <string name="unfolded_controls">Vezérlők a kinyitott képernyőn</string> + <string name="unfolded_controls_hint">Kinyitva a játék a Steam Deck érintőpadjai és hátsó fogói fölött jelenik meg. Összecsukva a játék teljes képernyős.</string> + <string name="pip_suspended">Felfüggesztve</string> + <string name="pip_not_ready">A kép a képben mód csak a munkamenet elkészülése után érhető el.</string> + <string name="pip_permission">Engedélyezze a kép a képben módot a DroidDeck számára az Android beállításaiban.</string> + <string name="pip_unavailable">Az Android nem tudta megnyitni a kép a képben módot.</string> + <string name="game_shortcut">Parancsikon</string> + <string name="game_shortcut_add">Hozzáadás a kezdőképernyőhöz</string> + <string name="game_shortcut_choose">Játék kiválasztása</string> + <string name="game_shortcut_scanning">Telepített játékok keresése…</string> + <string name="game_link_copy">Indítási link másolása</string> + <string name="game_link_copied">Indítási link másolva</string> + <string name="game_shortcut_unsupported">Ez az indítóalkalmazás nem tud parancsikont hozzáadni. Használja helyette az „Indítási link másolása” lehetőséget.</string> + <string name="game_link_invalid">Érvénytelen játékindítási link.</string> + <string name="game_link_missing">Ez a játék már nincs a DroidDeck könyvtárában.</string> + <string name="game_link_session_busy">Állítsa le a jelenlegi munkamenetet, mielőtt elindít egy Steam-játékot.</string> + <string name="game_link_failed">Nem sikerült elküldeni a játékot a Steamnek. Próbálja újra elindítani.</string> + <string name="setup_retry_removal">Eltávolítás újra</string> + <string name="runtime_removing">Eltávolítás</string> + <string name="runtime_removal_incomplete">Az eltávolítás nem teljes</string> + <string name="game_file_export">Játékfájl exportálása</string> + <string name="game_file_sync">Minden játékfájl szinkronizálása mappába</string> + <string name="game_file_stop_sync">Játékfájlok szinkronizálásának leállítása</string> + <string name="game_file_folder">Mappa kiválasztása a frontend játékfájlokhoz</string> + <string name="game_file_exported">A játékfájlok mentve: %1$s</string> + <string name="game_frontend_files">Frontend-fájlok</string> + + <string name="main_wait_runtime">Várja meg a futtatókörnyezet-művelet befejezését</string> + <string name="main_wait_proton_install">Várja meg a kompatibilitási eszköz telepítésének befejezését</string> + <string name="main_wait_desktop_install">Várja meg az asztali alkalmazás telepítésének befejezését</string> + <string name="main_wait_install_game">Várja meg a telepítés befejezését, mielőtt elindít egy játékot</string> + <string name="main_saves_close_session">Előbb zárja be a Steam-munkamenetet, hogy a játék ne írhassa felül az importált mentést</string> + <string name="main_saves_importing">Importálás ide: %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="one">%1$d fájl importálva innen: %2$s, ide: %3$s</item> + <item quantity="other">%1$d fájl importálva innen: %2$s, ide: %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">%1$d fájl importálva innen: %2$s, ide: %3$s. A régi mentések mentve itt: Download/DroidDeck/Saves/backups</item> + <item quantity="other">%1$d fájl importálva innen: %2$s, ide: %3$s. A régi mentések mentve itt: Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Válasszon mentés zip fájlt ehhez: %1$s</string> + <string name="main_saves_exporting">%1$s exportálása</string> + <plurals name="main_saves_exported"> + <item quantity="one">%1$d fájl exportálva %2$s formátumban: %3$s</item> + <item quantity="other">%1$d fájl exportálva %2$s formátumban: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Válassza ki, hová mentse: %1$s (%2$s)</string> + <string name="main_action_failed">%1$s sikertelen: %2$s</string> + <string name="main_no_logs_yet">Még nincs munkamenet-napló: indítson el előbb egy munkamenetet.</string> + <string name="main_no_logs_to_clear">Nincs törölhető munkamenet-napló.</string> + <plurals name="main_logs_cleared"> + <item quantity="one">%1$d munkamenet-naplómappa törölve.</item> + <item quantity="other">%1$d munkamenet-naplómappa törölve.</item> + </plurals> + <string name="main_wadb_pair_failed">A vezeték nélküli hibakeresés párosítása sikertelen</string> + <string name="main_wadb_command_failed">A vezeték nélküli hibakeresés parancsa sikertelen</string> + <string name="main_wadb_unavailable">A vezeték nélküli hibakeresés nem áll rendelkezésre</string> + <string name="main_adb_copied">ADB-parancs másolva</string> + <string name="main_pick_roms">ROMs mappa kiválasztása</string> + <string name="main_non_adreno_title">Nem Adreno GPU</string> + <string name="main_non_adreno_text">A Turnip Adreno GPU-kat támogat. %1$s esetén a Steam fekete képernyőt jeleníthet meg. Letöltés: %2$.0f MB.</string> + <string name="main_install_anyway">Telepítés mindenképpen</string> + <string name="main_remove_runtime_title">Linux futtatókörnyezet eltávolítása</string> + <string name="main_remove_runtime_text">Ez törli a futtatókörnyezetet, a benne lévő Steam-klienst és az összes ott telepített játékot.</string> + <string name="main_applied">Alkalmazva: %1$s</string> + <string name="android_app_open_failed_primary">Nem sikerült megnyitni a %1$s alkalmazást az elsődleges képernyőn</string> + <string name="android_app_open_failed_on">Nem sikerült megnyitni a %1$s alkalmazást itt: %2$s</string> + <string name="android_app_open_failed_display">Nem sikerült megnyitni a %1$s alkalmazást a %2$d. kijelzőn</string> + <string name="comp_busy_importing">Importálás</string> + <string name="comp_busy_swapping">Csere</string> + <string name="comp_busy_restoring">Visszaállítás</string> + <string name="comp_busy_cancelling">Megszakítás</string> + <string name="comp_busy_deleting">Törlés</string> + <string name="comp_cannot_read">nem lehet beolvasni a fájlt</string> + <string name="comp_imported">%1$s importálva</string> + <string name="comp_swap_cancelled">A várakozó csere megszakítva.</string> + <string name="comp_pick_wcp">Válasszon összetevőcsomagot (-linux .wcp)</string> + <string name="comp_nightlies_unreachable">A Nightlies nem volt elérhető</string> + <string name="comp_stored">%1$s tárolva</string> + <string name="comp_download_failed">Letöltés sikertelen: %1$s</string> + <string name="drivers_pick_linux">Válasszon Linux futtatókörnyezet-illesztőprogramot (-Linux zip vagy Android + Linux csomag)</string> + <string name="drivers_pick_android">Válasszon megjelenítő-illesztőprogramot (AdrenoTools zip vagy Android + Linux csomag)</string> + <string name="drivers_pick_any">Válasszon illesztőprogram zip fájlt vagy Android + Linux csomagot</string> + <string name="main_pkg_problem">%1$s: %2$s</string> + <string name="main_removing_named">%1$s eltávolítása…</string> + <string name="main_forgetting_named">%1$s elfelejtése…</string> + <string name="main_pick_game_storage">Játéktárhely-mappa kiválasztása</string> + <string name="main_pick_decky_zip">Válasszon Decky-bővítmény ZIP fájlt</string> + <string name="main_storage_not_usable">Nem használható: %1$s</string> + <string name="main_running_program">Program</string> + <string name="main_removing_runtime">Linux futtatókörnyezet eltávolítása</string> + <string name="main_settings_main_screen">Nem sikerült megnyitni a Beállításokat az alsó képernyőn; a fő képernyőn nyílt meg.</string> + <string name="main_open_dev_options">Fejlesztői beállítások megnyitása a Beállításokban</string> + + <string name="upd_unknown_apps_failed">Nem sikerült megnyitni az Android „Ismeretlen alkalmazások telepítése” beállítását</string> + <string name="upd_couldnt_check">Nem sikerült ellenőrizni: %1$s</string> + <string name="upd_no_download">Ez a build nem rendelkezik letöltéssel a DroidDeck ezen másolatához</string> + <string name="upd_stop_first">Előbb állítsa le a futó munkamenetet, majd frissítsen</string> + <string name="upd_stage_downloading">Letöltés</string> + <string name="upd_stage_installing">Telepítés</string> + <string name="upd_stage_confirm">Megerősítésre várakozás</string> + <string name="upd_failed">A frissítés sikertelen</string> + <string name="upd_not_offered">Az Android nem ajánlotta fel a frissítés telepítését</string> + + <string name="decky_install_failed">A Decky telepítése sikertelen: %1$s</string> + <string name="decky_plugin_starting">Bővítmény importálásának indítása…</string> + <string name="decky_plugin_failed">A bővítmény importálása sikertelen: %1$s</string> + <string name="decky_plugin_installed">A bővítmény telepítve. Indítsa újra a Steamet a betöltéséhez.</string> + + <string name="drivers_not_checked">Még nincs ellenőrizve – koppintson a frissítésre az új illesztőprogramok kereséséhez</string> + <string name="drivers_bundle_sets_display">Android + Linux: ezzel együtt beállítja a megjelenítő-illesztőprogramot is</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: ezzel együtt beállítja a futtatókörnyezet illesztőprogramját is</string> + <string name="drivers_runtime_builtin">a futtatókörnyezetbe épített Turnip</string> + <string name="drivers_system_vulkan">rendszer Vulkan: nincs csomagolt build ehhez a GPU-hoz</string> + <string name="drivers_bundled">%1$s (csomagolt)</string> + <string name="drivers_manual">Manuális</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">Jelenleg csak az egyik fele van kiadva</string> + <string name="drivers_pair_size">%1$.0f MB mindkettőhöz</string> + <string name="drivers_none_to_set">Nincs beállítható illesztőprogram: %1$s</string> + <string name="drivers_checking_latest">A legújabb illesztőprogramok keresése…</string> + <string name="drivers_up_to_date_check_failed">Naprakész (nem sikerült ellenőrizni: %1$s)</string> + <string name="drivers_recommended_unpublished">Az ajánlott illesztőprogramok jelenleg nincsenek kiadva</string> + <string name="drivers_downloading_pair">%1$s %2$s letöltése…</string> + <string name="drivers_updated_to">Frissítve erre: %1$s %2$s.</string> + <string name="drivers_updated_to_restart">Frissítve erre: %1$s %2$s. A megjelenítő-illesztőprogram a DroidDeck újraindítása után lép érvénybe.</string> + <string name="drivers_switched_to">Átváltva erre: %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Átváltva erre: %1$s %2$s. A megjelenítő-illesztőprogram a DroidDeck újraindítása után lép érvénybe.</string> + <string name="drivers_using">%1$s %2$s használatban.</string> + <string name="drivers_using_restart">%1$s %2$s használatban. A megjelenítő-illesztőprogram a DroidDeck újraindítása után lép érvénybe.</string> + <string name="drivers_not_a_driver">Nem illesztőprogram zip: nem AdrenoTools-illesztőprogram, nem -Linux Turnip és nem Android + Linux csomag</string> + <string name="drivers_import_failed">Importálás sikertelen: %1$s</string> + <string name="drivers_imported_unnamed">Illesztőprogram importálva</string> + <string name="drivers_using_bundle">%1$s használatban mind futtatókörnyezet-, mind megjelenítő-illesztőprogramként.</string> + <string name="drivers_using_bundle_restart">%1$s használatban mind futtatókörnyezet-, mind megjelenítő-illesztőprogramként. A megjelenítő-illesztőprogram a DroidDeck újraindítása után lép érvénybe.</string> + <string name="drivers_deleted_bundle">%1$s törölve (mindkét illesztőprogram)</string> + <string name="drivers_deleted">%1$s törölve</string> + <string name="drivers_release_status">Legújabb: %1$s · ellenőrizve: %2$s</string> + <string name="drivers_release_status_failed">Legújabb: %1$s · %2$s nem elérhető · ellenőrizve: %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">%1$d perce</item> + <item quantity="other">%1$d perce</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">%1$d órája</item> + <item quantity="other">%1$d órája</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">%1$d napja</item> + <item quantity="other">%1$d napja</item> + </plurals> + <string name="drivers_checking_sources">A Banners-Turnip, a WinNative és a DroidDeck ellenőrzése…</string> + <string name="drivers_check_failed">ellenőrzés sikertelen</string> + <string name="drivers_installed_pick_either">%1$s telepítve – válassza ki bármelyik listában mindkét illesztőprogram beállításához</string> + <string name="drivers_installed_pick">%1$s telepítve – válassza ki a menüben</string> + + <string name="session_wait_removal">Várja meg a futtatókörnyezet eltávolításának befejezését</string> + <string name="session_title_game">Játék</string> + <string name="session_touch_auto_touchpad">érintőpad</string> + <string name="session_touch_auto_direct">közvetlen</string> + <string name="session_swap_failed">A csere sikertelen: %1$s</string> + <string name="session_profile_save_failed">A profil mentése sikertelen: %1$s</string> + <string name="session_desktop_paused">Az asztal szünetel</string> + <string name="session_named_paused">%1$s szünetel</string> + <string name="session_program_paused">A program szünetel</string> + <string name="session_starting_desktop">Az asztal indítása</string> + <string name="session_starting_named">%1$s indítása</string> + <string name="session_starting_program">A program indítása</string> + <string name="session_no_logs">Nincs napló ehhez a munkamenethez.</string> + <string name="session_logs_archive_failed">Nem sikerült létrehozni a munkamenet-napló archívumot.</string> + <string name="session_logs_share_failed">Nem sikerült megosztani a munkamenet-naplókat.</string> + <string name="session_starting_session">A munkamenet indítása…</string> + <string name="session_runtime_downloading">a Linux futtatókörnyezet letöltése</string> + <string name="session_runtime_downloading_mb">a Linux futtatókörnyezet letöltése · %1$d/%2$d MB</string> + <string name="session_runtime_checking">a Linux futtatókörnyezet ellenőrzése</string> + <string name="session_runtime_unpacking">a Linux futtatókörnyezet kicsomagolása</string> + <string name="session_desktop_downloading">az asztal letöltése</string> + <string name="session_desktop_downloading_mb">az asztal letöltése · %1$d/%2$d MB</string> + <string name="session_desktop_checking">az asztal ellenőrzése</string> + <string name="session_desktop_unpacking">az asztal kicsomagolása</string> + <string name="session_proton_downloading">a Proton Experimental (ARM64) letöltése</string> + <string name="session_proton_downloading_mb">a Proton Experimental (ARM64) letöltése · %1$d/%2$d MB</string> + <string name="session_proton_checking">a Proton Experimental (ARM64) ellenőrzése</string> + <string name="session_proton_unpacking">a Proton Experimental (ARM64) kicsomagolása</string> + <string name="session_runtime_catalog_unreachable">Nem sikerült elérni a futtatókörnyezet-katalógust. Ellenőrizze a kapcsolatot, és nyomja meg újra a Játék gombot.</string> + <string name="session_runtime_install_failed">A Linux futtatókörnyezet telepítése sikertelen. Ellenőrizze a futtatókörnyezetet a Beállításban, és nyomja meg újra a Játék gombot.</string> + <string name="session_desktop_install_failed">Az asztal telepítése sikertelen (%1$s). Ellenőrizze a kapcsolatot, és nyomja meg újra az Asztal gombot.</string> + <string name="session_ended_named">%1$s váratlanul leállt. Ossza meg a naplókat egy hibajelentésben, vagy próbálja újra.</string> + <string name="session_ended_program">A program váratlanul leállt. Ossza meg a naplókat egy hibajelentésben, vagy próbálja újra.</string> + <string name="session_ended_desktop">Az asztal váratlanul leállt. Ossza meg a naplókat egy hibajelentésben, vagy próbálja újra.</string> + <string name="session_ended_detail">Kilépési állapot: %1$d · %2$s</string> + <string name="session_ended_no_log">nincs napló</string> + <plurals name="session_hint_enosys"> + <item quantity="one">A napló %1$d „Function not implemented” hibát tartalmaz: a proot seccomp-gyorsítása elutasít egy segédfolyamatot ezen az eszközön. Próbálja ki ezt: %2$s → „%3$s” (vagy „%4$s”), majd indítsa el újra.</item> + <item quantity="other">A napló %1$d „Function not implemented” hibát tartalmaz: a proot seccomp-gyorsítása elutasít egy segédfolyamatot ezen az eszközön. Próbálja ki ezt: %2$s → „%3$s” (vagy „%4$s”), majd indítsa el újra.</item> + </plurals> + + <string name="mode_decky_plugins">Bővítmények</string> + <string name="mode_decky_plugins_hint">Telepítsen egy bővítmény ZIP-et a következő Steam-munkamenethez.</string> + <string name="mode_decky_plugin_downloading">Letöltés…</string> + <string name="mode_decky_plugin_install_zip">Telepítés ZIP-ből</string> + <string name="store_cat_games">Játékok</string> + <string name="store_cat_audio_video">Hang és videó</string> + <string name="store_cat_graphics">Grafika</string> + <string name="store_cat_internet">Internet</string> + <string name="store_cat_office">Iroda</string> + <string name="store_cat_developer">Fejlesztés</string> + <string name="store_cat_education">Oktatás</string> + <string name="store_cat_science">Tudomány</string> + <string name="store_cat_system">Rendszer</string> + <string name="store_cat_utilities">Segédprogramok</string> + <string name="store_section_popular">Népszerű</string> + <string name="store_section_trending">Felkapott</string> + <string name="store_section_new">Új</string> + <string name="store_section_updated">Frissített</string> + <string name="store_section_loading">betöltés…</string> + <string name="store_flatpak_setup">Flatpak beállítása</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Nem sikerült ellenőrizni a frissítéseket a Flathubon</string> + <string name="content_rom_installed">telepítve</string> + <string name="content_rom_folder">mappa</string> + <string name="theme_graphite">Grafit</string> + <string name="theme_graphite_detail">Grafitszínű felületek, a logó saját gömbkékjével minden kijelölt, fókuszált vagy lenyomott elemhez.</string> + <string name="theme_paper">Papír feketén</string> + <string name="theme_paper_detail">A fehér az elsődleges, az ikon kékje jelzi a vonalakat, köröket és pontokat.</string> + <string name="theme_icon_blue">Ikonkék</string> + <string name="theme_icon_blue_detail">Fekete alap, fehér betűk, a kék minden kijelölt, fókuszált vagy lenyomott elemhez.</string> + <string name="theme_electric">Elektromos tengerészkék</string> + <string name="theme_electric_detail">Fényesebb kék, szürke helyett tengerészkék felületeken.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Space</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <string name="game_sync_err_runtime">Előbb telepítse a futtatókörnyezetet</string> + <string name="game_sync_err_steam_library">A Steam-könyvtár nem elérhető</string> + <string name="game_sync_err_previous_sd">A korábbi SD-könyvtár nem elérhető</string> + <string name="game_sync_err_game_storage">A játéktárhely nem elérhető</string> + <string name="game_sync_err_added_games">A hozzáadott játékok mappája nem elérhető</string> + <string name="game_sync_err_manifests">A Steam-manifesztek nem elérhetők</string> + <string name="content_system_many">Sok rendszer</string> + <string name="fm_drive_internal">Belső</string> + <string name="fm_drive_linux">Linux-rendszer</string> + <string name="fm_drive_sd">SD-kártya</string> + <string name="fm_drive_storage">Tárhely</string> + <string name="fm_select">Kiválasztás</string> + <string name="fm_properties">Tulajdonságok</string> + <string name="fm_favorite_add">Hozzáadás a Kedvencekhez</string> + <string name="fm_favorite_remove">Eltávolítás a Kedvencekből</string> + <string name="fm_rename">Átnevezés</string> + <string name="fm_copy">Másolás</string> + <string name="fm_cut">Kivágás</string> + <string name="fm_actions">Műveletek</string> + <string name="fm_favorites">Kedvencek</string> + <string name="fm_pin_current">Jelenlegi mappa rögzítése</string> + <string name="fm_no_favorites">Még nincs kedvenc – rögzítsen egy mappát a ⋮ menüjével, hogy gyorsan visszaugorhasson ide.</string> + <string name="fm_unpin_favorite">Eltávolítás a kedvencekből</string> + <string name="fm_size_zero">0 bájt</string> + <string name="fm_unit_bytes">bájt</string> + <string name="fm_prop_location">Hely</string> + <string name="fm_prop_type">Típus</string> + <string name="fm_prop_size">Méret</string> + <string name="fm_prop_modified">Módosítva</string> + <string name="fm_type_folder">Mappa</string> + <string name="fm_type_file">Fájl</string> + <string name="fm_type_ext_file">%1$s fájl</string> + <string name="fm_read_only">Csak olvasható</string> + <string name="fm_read_only_hint">Megakadályozza, hogy a játékok (és a Wine) felülírják vagy töröljék ezt a fájlt.</string> + <string name="fm_read_only_failed">Nem sikerült módosítani a Csak olvasható állapotot</string> + <string name="fm_hidden">Rejtett</string> + <string name="fm_hidden_unsupported">Ez a tárhely nem tudja tárolni a rejtett jelzőt.</string> + <string name="fm_hidden_hint">Rejtettként jelöli meg a fájlt Windows alatt (a Wine rejtett attribútuma).</string> + <string name="fm_hidden_failed">Nem sikerült módosítani a Rejtett állapotot ezen a tárhelyen</string> + <string name="fm_done">Kész</string> + <string name="fm_op_deleting">Törlés…</string> + <string name="fm_delete_failed">A törlés sikertelen</string> + <string name="fm_op_moving">Áthelyezés…</string> + <string name="fm_op_copying">Másolás…</string> + <string name="fm_op_moving_item">Áthelyezés – %1$s</string> + <string name="fm_op_copying_item">Másolás – %1$s</string> + <string name="fm_op_moving_batch">Áthelyezés %1$d/%2$d – %3$s</string> + <string name="fm_op_copying_batch">Másolás %1$d/%2$d – %3$s</string> + <string name="fm_paste_failed">%1$d kész, %2$d sikertelen</string> + <string name="fm_paste_skipped">%1$d kész, %2$d kihagyva</string> + <plurals name="fm_paste_moved"> + <item quantity="one">%1$d elem áthelyezve</item> + <item quantity="other">%1$d elem áthelyezve</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">%1$d elem másolva</item> + <item quantity="other">%1$d elem másolva</item> + </plurals> + <string name="fm_already_exists">„%1$s” már létezik</string> + <string name="fm_op_renaming">Átnevezés…</string> + <string name="fm_rename_failed">Az átnevezés sikertelen</string> + <string name="fm_op_creating_folder">Mappa létrehozása…</string> + <string name="fm_create_folder_failed">Nem sikerült létrehozni a mappát</string> + <string name="fm_new_folder">Új mappa</string> + <string name="fm_folder_name">Mappa neve</string> + <string name="fm_create">Létrehozás</string> + <string name="fm_new_name">Új név</string> + <string name="fm_delete_title">Törli?</string> + <string name="fm_delete_text">Véglegesen törli: „%1$s”?</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">Törli %1$d elemet?</item> + <item quantity="other">Törli %1$d elemet?</item> + </plurals> + <string name="fm_cannot_undo">Ez nem vonható vissza.</string> + <plurals name="fm_and_more"> + <item quantity="one">…és még %1$d</item> + <item quantity="other">…és még %1$d</item> + </plurals> + <string name="fm_op_deleting_batch">Törlés %1$d/%2$d – %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">%1$d elemet nem sikerült törölni</item> + <item quantity="other">%1$d elemet nem sikerült törölni</item> + </plurals> + <string name="fm_conflict_merge_hint">Az Összefésülés hozzáadja és lecseréli a fájlokat a meglévő mappában.</string> + <string name="fm_conflict_overwrite_hint">A Felülírás lecseréli a meglévő fájlt.</string> + <string name="fm_conflict_apply_all">Alkalmazás minden ütközésre</string> + <string name="fm_conflict_merge">Összefésülés</string> + <string name="fm_conflict_overwrite">Felülírás</string> + <string name="fm_conflict_keep_both">Mindkettő megtartása</string> + <string name="fm_conflict_skip">Kihagyás</string> + <string name="fm_select_this_folder">Ennek a mappának a kiválasztása</string> + <string name="fm_back">Vissza</string> + <string name="fm_drive_not_readable">A %1$s csatolva van, de jelenleg nem olvasható</string> + <string name="fm_list_view">Listanézet</string> + <string name="fm_grid_view">Rácsnézet</string> + <string name="fm_search">Keresés</string> + <string name="fm_sort">Rendezés</string> + <string name="fm_sort_name">Név</string> + <string name="fm_sort_date">Módosítás dátuma</string> + <string name="fm_rows_comfortable">Kényelmes sorok</string> + <string name="fm_rows_compact">Kompakt sorok</string> + <string name="fm_hide_hidden">Rejtett fájlok elrejtése</string> + <string name="fm_show_hidden">Rejtett fájlok megjelenítése</string> + <string name="fm_hide_favorites">Kedvencek elrejtése</string> + <string name="fm_show_favorites">Kedvencek megjelenítése</string> + <string name="fm_filter_folder">Ennek a mappának a szűrése</string> + <string name="fm_free_space">%1$s szabad</string> + <string name="fm_selected_count">%1$d kiválasztva</string> + <string name="fm_select_none">Nincs</string> + <string name="fm_select_all">Összes</string> + <string name="fm_paste">Beillesztés</string> + <plurals name="fm_items"> + <item quantity="one">%1$d elem</item> + <item quantity="other">%1$d elem</item> + </plurals> + <string name="fm_paste_here">%1$s beillesztése ide</string> + <string name="fm_paste_move_here">%1$s beillesztése (áthelyezés) ide</string> + <string name="fm_rail_title">Fájlok</string> + <string name="fm_rail_storage">TÁRHELY</string> + <string name="fm_rail_quick">GYORS</string> + <string name="fm_rail_favorites">KEDVENCEK</string> + <string name="fm_rail_downloads">Letöltések</string> + <string name="fm_rail_downloads_short">Letöltés</string> + <string name="fm_rail_roms">ROMs</string> + <string name="fm_rail_session_logs">Munkamenet-naplók</string> + <string name="fm_rail_pictures">Képek</string> + <string name="fm_favorite_added">„%1$s” hozzáadva a Kedvencekhez</string> + <string name="fm_favorite_removed">„%1$s” eltávolítva a Kedvencekből</string> + <string name="fm_empty_directory">Üres mappa</string> + <string name="fm_up_one_level">Egy szinttel feljebb</string> + <string name="fm_choose_folder">Válasszon mappát</string> + <string name="fm_choose_file">Válasszon fájlt</string> + <string name="ctrl_tint_steam_blue">Steam kék</string> + <string name="ctrl_tint_sky">Égszínkék</string> + <string name="ctrl_tint_white">Fehér</string> + <string name="ctrl_tint_violet">Lila</string> + <string name="ctrl_tint_green">Zöld</string> + <string name="ctrl_tint_amber">Borostyán</string> + <string name="ctrl_tint_red">Piros</string> + <string name="ctrl_tint_pink">Rózsaszín</string> + <string name="ctrl_button_a">A gomb</string> + <string name="ctrl_button_b">B gomb</string> + <string name="ctrl_button_x">X gomb</string> + <string name="ctrl_button_y">Y gomb</string> + <string name="ctrl_button_lb">Bal bumper</string> + <string name="ctrl_button_rb">Jobb bumper</string> + <string name="ctrl_button_lt">Bal ravasz</string> + <string name="ctrl_button_rt">Jobb ravasz</string> + <string name="ctrl_button_view">View gomb</string> + <string name="ctrl_button_menu">Menu gomb</string> + <string name="ctrl_target_view">View</string> + <string name="ctrl_target_menu">Menu</string> + <string name="ctrl_target_up">D-pad fel</string> + <string name="ctrl_target_down">D-pad le</string> + <string name="ctrl_target_left">D-pad balra</string> + <string name="ctrl_target_right">D-pad jobbra</string> + <string name="ctrl_target_hidden">Rejtett</string> + <string name="ctrl_editor_drag">Húzza a mozgatáshoz</string> + <string name="ctrl_editor_snap">Rögzítés</string> + <string name="ctrl_editor_save">Mentés</string> + <string name="second_mode_none">Nincs</string> + <string name="second_mode_keyboard_trackpad">Billentyűzet + érintőpad</string> + <string name="second_mode_terminal">Terminál</string> + <string name="second_mode_deck_controls">Deck fogók + érintőpadok</string> + + <string name="rtinst_removing">Linux futtatókörnyezet eltávolítása</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">Linux futtatókörnyezet eltávolítása · %1$d bejegyzés</item> + <item quantity="other">Linux futtatókörnyezet eltávolítása · %1$d bejegyzés</item> + </plurals> + <string name="rtinst_removed">Linux futtatókörnyezet eltávolítva</string> + <string name="rtinst_removal_failed">Nem sikerült befejezni a Linux futtatókörnyezet eltávolítását. Próbálja újra a Beállításban.</string> + <string name="rtinst_removing_leftovers">Futtatókörnyezet-maradványok eltávolítása</string> + <string name="rtinst_downloading">Letöltés</string> + <string name="rtinst_verifying">Ellenőrzés</string> + <string name="rtinst_extracting">Kicsomagolás</string> + <string name="rtinst_channel_description">Megjelenik, amíg a Linux futtatókörnyezet telepítése vagy eltávolítása zajlik</string> + <string name="rtinst_title_installing">A Linux futtatókörnyezet telepítése</string> + <string name="rtinst_title_removing">A Linux futtatókörnyezet eltávolítása</string> + + <string name="deskpkg_runtime_missing">A Linux futtatókörnyezet nincs telepítve</string> + <string name="deskpkg_no_checksum">A katalógusban nincs ellenőrzőösszeg a %1$s-hez</string> + <string name="deskpkg_checksum_mismatch">Az ellenőrzőösszeg nem egyezik – semmi nem változott</string> + <string name="deskpkg_installing">%1$s telepítése</string> + <string name="deskpkg_place_failed">Nem sikerült elhelyezni az AppImage-et</string> + <string name="deskpkg_extract_failed">A kicsomagolás sikertelen</string> + <string name="deskpkg_install_failed">A telepítés sikertelen</string> + + <string name="flatpakmgr_setup_failed">A Flatpak beállítása sikertelen (kilépési kód: %1$d)</string> + <string name="flatpakmgr_setup_unfinished">A Flatpak beállítása nem fejeződött be</string> + <string name="flatpakmgr_busy">Egy másik áruházi feladat fut</string> + <string name="flatpakmgr_not_ready">Előbb állítsa be a Flatpakot</string> + <string name="flatpakmgr_starting">Indítás</string> + <string name="flatpakmgr_exit_status">A Flatpak %1$d állapotkóddal lépett ki</string> + <string name="flatpakmgr_installing">%1$s telepítése</string> + <string name="flatpakmgr_updating">%1$s frissítése</string> + <string name="flatpakmgr_removing">%1$s eltávolítása</string> + <string name="flatpakmgr_installing_step">%1$s telepítése (%2$d/%3$d)</string> + <string name="flatpakmgr_updating_step">%1$s frissítése (%2$d/%3$d)</string> + <string name="flatpakmgr_removing_step">%1$s eltávolítása (%2$d/%3$d)</string> + <string name="flatpakmgr_ref_translations">fordítások</string> + <string name="flatpakmgr_ref_graphics_drivers">grafikai illesztőprogramok</string> + <string name="flatpakmgr_ref_media_codecs">médiakodekek</string> + <string name="flatpakmgr_ref_runtime">%1$s futtatókörnyezet</string> + <string name="flatpakmgr_ref_runtime_branch">%1$s futtatókörnyezet %2$s</string> + + <string name="appimg_too_small">A fájl túl kicsi ahhoz, hogy AppImage legyen</string> + <string name="appimg_unreadable">A fájl nem olvasható: %1$s</string> + <string name="appimg_not_linux">Ez nem egy AppImage (nem Linux-program)</string> + <string name="appimg_wrong_arch">Ez az AppImage sem ARM64-hez (aarch64), sem x86 PC-khez nincs készítve</string> + <string name="appimg_not_appimage">Ez egy Linux-program, de nem AppImage</string> + <string name="appimg_wrong_type">Csak 2-es típusú AppImage-ek importálhatók (ez %1$d típusú)</string> + <string name="appimg_no_space_import">Nincs elég szabad hely: az importáláshoz körülbelül %1$s szükséges</string> + <string name="appimg_no_space_update">Nincs elég szabad hely: a frissítéshez körülbelül %1$s szükséges</string> + <string name="appimg_import_failed">Az importálás sikertelen</string> + <string name="appimg_gone">Ez az AppImage már nincs meg</string> + <string name="appimg_update_failed">A frissítés sikertelen</string> + <string name="appimg_extracting">%1$s kicsomagolása</string> + <string name="appimg_extract_failed">Az AppImage-et nem sikerült kicsomagolni</string> + <string name="appimg_extract_failed_detail">Az AppImage-et nem sikerült kicsomagolni: %1$s</string> + + <string name="deckymgr_stop_session_loader">Állítsa le az aktív munkamenetet a Decky Loader telepítése előtt</string> + <string name="deckymgr_download_failed">A Decky Loader letöltése sikertelen</string> + <string name="deckymgr_size_mismatch">A Decky Loader mérete nem egyezett a kiadás metaadataival</string> + <string name="deckymgr_checksum_download_failed">Nem sikerült letölteni a kiadás ellenőrzőösszegét</string> + <string name="deckymgr_checksum_missing">A kiadás ellenőrzőösszege hiányzik</string> + <string name="deckymgr_checksum_invalid">A kiadás ellenőrzőösszege érvénytelen</string> + <string name="deckymgr_checksum_mismatch">A Decky Loader ellenőrzőösszege nem egyezik</string> + <string name="deckymgr_not_elf">A kiadás eszköze nem kompatibilis 64 bites Linux-futtatható</string> + <string name="deckymgr_session_started_loader">Egy munkamenet elindult a letöltés közben; állítsa le a Decky Loader telepítése előtt</string> + <string name="deckymgr_loader_not_executable">Nem sikerült futtathatóvá tenni a PluginLoadert</string> + <string name="deckymgr_cef_failed">Nem sikerült bekapcsolni a Steam CEF távoli hibakeresését</string> + <string name="deckymgr_stage_failed">Nem sikerült előkészíteni a PluginLoadert</string> + <string name="deckymgr_loader_preserve_failed">Nem sikerült megőrizni a meglévő PluginLoadert</string> + <string name="deckymgr_loader_activate_failed">Nem sikerült aktiválni az új PluginLoadert</string> + <string name="deckymgr_stop_session_plugin">Állítsa le az aktív munkamenetet egy bővítmény telepítése előtt</string> + <string name="deckymgr_loader_required">Telepítse a Decky Loadert egy bővítmény importálása előtt</string> + <string name="deckymgr_zip_unreadable">Nem sikerült beolvasni a kiválasztott ZIP fájlt</string> + <string name="deckymgr_plugins_dir_failed">Nem sikerült létrehozni a Decky bővítménymappát</string> + <string name="deckymgr_extracting_zip">Bővítmény ZIP kicsomagolása</string> + <string name="deckymgr_zip_one_folder">A bővítmény ZIP-nek egy legfelső szintű bővítménymappát kell tartalmaznia</string> + <string name="deckymgr_zip_missing_files">A bővítmény ZIP-ből hiányzik a plugin.json, a package.json vagy a dist/index.js</string> + <string name="deckymgr_plugin_name_missing">A bővítmény neve hiányzik</string> + <string name="deckymgr_session_started_plugin">Egy munkamenet elindult a telepítés közben; állítsa le egy bővítmény telepítése előtt</string> + <string name="deckymgr_plugin_installed">A bővítmény telepítve · indítsa újra a Steamet a betöltéséhez</string> + <string name="deckymgr_plugin_install_failed">A bővítmény ZIP telepítése sikertelen</string> + <string name="deckymgr_zip_too_many">A bővítmény ZIP túl sok bejegyzést tartalmaz</string> + <string name="deckymgr_zip_invalid_path">A bővítmény ZIP érvénytelen elérési utat tartalmaz</string> + <string name="deckymgr_zip_duplicate_paths">A bővítmény ZIP duplikált elérési utakat tartalmaz</string> + <string name="deckymgr_zip_unsafe_path">A bővítmény ZIP nem biztonságos elérési utat tartalmaz</string> + <string name="deckymgr_folder_failed">Nem sikerült létrehozni a bővítménymappát</string> + <string name="deckymgr_zip_too_large">A bővítmény ZIP kicsomagolva túllépi az 1 GiB-os korlátot</string> + <string name="deckymgr_bundled_not_executable">Nem sikerült futtathatóvá tenni a csomagolt bővítménybinárist</string> + <string name="deckymgr_remote_entry_invalid">Érvénytelen remote_binary bejegyzés</string> + <string name="deckymgr_remote_name_invalid">Érvénytelen távoli bináris neve</string> + <string name="deckymgr_remote_url_https">A távoli bináris URL-jének HTTPS-t kell használnia</string> + <string name="deckymgr_remote_hash_invalid">A %1$s távoli bináris SHA-256 hash-e érvénytelen</string> + <string name="deckymgr_bin_dir_failed">Nem sikerült létrehozni a bővítmény bin mappáját</string> + <string name="deckymgr_downloading_binary">Bővítménybináris letöltése · %1$s</string> + <string name="deckymgr_binary_download_failed">Nem sikerült letölteni a %1$s bővítménybinárist</string> + <string name="deckymgr_binary_checksum_mismatch">A bővítménybináris ellenőrzőösszege nem egyezik · %1$s</string> + <string name="deckymgr_binary_not_executable">Nem sikerült futtathatóvá tenni a bővítménybinárist · %1$s</string> + <string name="deckymgr_binary_replace_failed">Nem sikerült lecserélni a bővítménybinárist · %1$s</string> + <string name="deckymgr_binary_install_failed">Nem sikerült telepíteni a bővítménybinárist · %1$s</string> + <string name="deckymgr_folder_taken">A bővítménymappa egy másik bővítményhez tartozik</string> + <string name="deckymgr_preserve_failed">Nem sikerült megőrizni a meglévő bővítményt</string> + <string name="deckymgr_activate_failed">Nem sikerült aktiválni az importált bővítményt</string> + <string name="deckymgr_settings_failed">Nem sikerült frissíteni a Decky bővítménybeállításokat</string> + <string name="deckymgr_restore_failed">Nem sikerült visszaállítani a meglévő bővítményt</string> + + <string name="adbfix_save_failed">Nem sikerült menteni a vezeték nélküli hibakeresés azonosítóját</string> + <string name="adbfix_command_failed">Az ADB-parancs sikertelen (%1$d)</string> + <string name="adbfix_not_confirmed">Az Android nem erősítette meg a gyermekfolyamat-korlát módosítását: %1$s</string> + <string name="adbfix_no_pairing">Nincs mentett vezeték nélküli hibakeresés-párosítás</string> + <string name="adbfix_enter_address">Adja meg a vezeték nélküli hibakeresés jelenlegi IP-címét és portját</string> + + <string name="adbpair_timed_out">Időtúllépés a párosítási felugró ablakra várva. Próbálja újra a DroidDeckből.</string> + <string name="adbpair_discovery_failed_code">Nem sikerült megkeresni a párosítási felugró ablakot (%1$d). Ellenőrizze, hogy a Wi-Fi be van kapcsolva.</string> + <string name="adbpair_discovery_failed">Nem sikerült megkeresni a párosítási felugró ablakot: %1$s</string> + <string name="adbpair_six_digits">Írja be a kód mind a 6 számjegyét</string> + <string name="adbpair_pairing">Párosítás…</string> + <string name="adbpair_code_rejected">Ez a kód nem működött. Nyissa meg újra a párosítási felugró ablakot, és írja be az új kódját.</string> + <string name="adbpair_connecting">Párosítva. Csatlakozás…</string> + <string name="adbpair_port_not_found">Párosítva, de a vezeték nélküli hibakeresés portja nem található. Hagyja bekapcsolva a vezeték nélküli hibakeresést, és próbálja újra.</string> + <string name="adbpair_applying">A beállítás alkalmazása…</string> + <string name="adbpair_command_failed">A vezeték nélküli hibakeresés parancsa sikertelen</string> + <string name="adbpair_channel_name">Vezeték nélküli hibakeresés beállítása</string> + <string name="adbpair_channel_description">A vezeték nélküli hibakeresés párosítási kódját kéri, amíg a Beállítások meg van nyitva</string> + <string name="adbpair_title_pair">Vezeték nélküli hibakeresés párosítása</string> + <string name="adbpair_waiting_text">Kapcsolja be a vezeték nélküli hibakeresést, majd koppintson az „Eszköz párosítása párosítási kóddal” lehetőségre.</string> + <string name="adbpair_waiting_long">Kapcsolja be a vezeték nélküli hibakeresést, majd koppintson az „Eszköz párosítása párosítási kóddal” lehetőségre. Maradjon a Beállításokban: a kód ebbe az értesítésbe kerül.</string> + <string name="adbpair_code_label">6 számjegyű párosítási kód</string> + <string name="adbpair_code_hint">Koppintson a Kód megadására, és írja be a Beállításokban megjelenő 6 számjegyet.</string> + <string name="adbpair_title_code">Adja meg a Wi-Fi párosítási kódot</string> + <string name="adbpair_title_retry">Próbálja meg újra a kódot</string> + <string name="adbpair_enter_code">Kód megadása</string> + <string name="adbpair_title_working">A Steam beállítása</string> + <string name="adbpair_title_done">A Steam készen áll</string> + <string name="adbpair_done_text">Koppintson a DroidDeckhez való visszatéréshez. A vezeték nélküli hibakeresést most kikapcsolhatja.</string> + <string name="adbpair_title_failed">A vezeték nélküli hibakeresés beállítása megállt</string> + + <string name="selfinst_http">A letöltés HTTP %1$d választ adott</string> + <string name="selfinst_size_mismatch">A letöltés mérete nem egyezett a közzétett APK-val, és elvetésre került – próbálja újra</string> + <string name="selfinst_checksum_mismatch">A letöltés nem egyezett az ellenőrzőösszegével, és elvetésre került – próbálja újra</string> + <string name="selfinst_wrong_package">A frissítés a %1$s Android-csomaghoz tartozik, nem a %2$s-hez</string> + <string name="selfinst_metadata_signer">A frissítés metaadatai nem nevezik meg a DroidDeck kiadási aláírókulcsát</string> + <string name="selfinst_unreadable">Az Android nem tudta beolvasni a letöltött APK-t</string> + <string name="selfinst_apk_wrong_package">A letöltött APK a %1$s-hez tartozik, nem a %2$s-hez</string> + <string name="selfinst_version_mismatch">A letöltött APK %1$d versionCode-ot ad meg, nem a közzétett %2$d-t</string> + <string name="selfinst_downgrade">Az Android nem telepíti a %1$d versionCode-ot a telepített %2$d fölé</string> + <string name="selfinst_no_signing">A letöltött APK nem tartalmaz aláírási információt</string> + <string name="selfinst_wrong_signer">A letöltött APK nincs aláírva a DroidDeck kiadási kulcsával</string> + <string name="appupd_http">A frissítési katalógus HTTP %1$d választ adott</string> + + <string name="loadstate_starting">A munkamenet indítása…</string> + <string name="loadstate_elapsed">%1$d:%2$02d eltelt · még dolgozik</string> + <string name="loadstate_steam_update">A Steam-kliens frissítésének letöltése · %1$d%%</string> + <string name="loadstate_steam_client">A Steam-kliens letöltése · %1$s</string> + + <string name="pextra_busy">Már fut egy kompatibilitási eszköz telepítése</string> + <string name="pextra_stop_session">Állítsa le az aktív munkamenetet a kompatibilitási eszközök telepítése előtt</string> + <string name="pextra_no_release">Nem található ARM64-es kiadás a %1$s-hez</string> + <string name="pextra_no_space">Legalább 4 GB szabad hely, plusz a letöltés mérete szükséges a %1$s telepítéséhez</string> + <string name="pextra_downloading">%1$s %2$s letöltése</string> + <string name="pextra_retrying">A letöltés újrapróbálása · %1$d. próbálkozás a %2$d-ből</string> + <string name="pextra_interrupted">A letöltés megszakadt</string> + <string name="pextra_download_failed">A letöltés sikertelen; a részleges fájl megmarad, hogy egy újrapróbálkozás folytathassa</string> + <string name="pextra_too_big">A letöltés túllépte a kiadás méretét, és törlésre került</string> + <string name="pextra_incomplete">A letöltés befejezetlen volt; a részleges fájl megmarad egy folytatható újrapróbálkozáshoz</string> + <string name="pextra_verifying">A letöltés ellenőrzése</string> + <string name="pextra_checksum_unreadable">Nem sikerült beolvasni a kiadás ellenőrzőösszegét; próbálja újra</string> + <string name="pextra_no_checksum">A %1$s %2$s nem közöl ellenőrzőösszeget; semmi nem lett telepítve</string> + <string name="pextra_checksum_mismatch">Az ellenőrzőösszeg nem egyezik; a letöltés törlésre került</string> + <string name="pextra_session_started">Egy munkamenet elindult a letöltés közben; állítsa le a kompatibilitási eszközök telepítése előtt</string> + <string name="pextra_installing">%1$s telepítése</string> + <string name="pextra_install_failed">A %1$s telepítése sikertelen (kilépési kód: %2$d)</string> + <string name="pextra_could_not_install">Nem sikerült telepíteni a %1$s-t</string> + <string name="esync_fetching">droiddeck-esync csomag letöltése</string> + + <string name="cmgr_fex_linux_helpers">Linux segédprogramokkal</string> + <string name="cmgr_fex_dlls_only">Csak DLL-ek</string> + <string name="cmgr_original_proton_updated">Eredeti (a Proton frissült az utolsó csere óta)</string> + <string name="cmgr_changed_outside">Az Összetevőkön kívül módosítva</string> + <string name="cmgr_not_present">nincs jelen</string> + <string name="cmgr_original_version">Eredeti %1$s</string> + <string name="cmgr_original_from">Eredeti innen: %1$s</string> + <string name="cmgr_swap_queued">A %1$s %2$s a %3$s-be kerül, amint a rajta futó játék bezárul.</string> + <string name="cmgr_swap_done">A %1$s %2$s most a %3$s-ben van. A következő játékindítástól érvényes.</string> + <string name="cmgr_original_missing">Az az eredeti csomag nincs tárolva</string> + <string name="cmgr_restore_queued">A %2$s %1$s-e visszaáll az eredetire, amint a rajta futó játék bezárul.</string> + <string name="cmgr_restore_done">A %2$s %1$s-e visszaállítva a %3$s eredetijéből.</string> + <string name="cmgr_package_in_use">A %1$s használatban van itt: %2$s. Előbb cserélje ki azt a Protont valami másra.</string> + <string name="cmgr_package_not_stored">A %1$s nincs tárolva</string> + <string name="cmgr_deleted">%1$s törölve.</string> + <string name="cmgr_original_kept">A telepített build eredetije megmarad: ez a visszaút.</string> + <string name="cmgr_not_stored">Nincs tárolva</string> + <string name="cmgr_deleted_original">A %2$s %1$s eredetije törölve.</string> + <string name="cmgr_nightlies_only">A letöltések csak a Nightlies kiadásokból érkeznek</string> + <string name="cmgr_list_predates_checksums">Ez a csomaglista régebbi, mint az ellenőrzőösszegek – frissítse, és próbálja újra</string> + <string name="cmgr_checksum_mismatch">Az ellenőrzőösszeg nem egyezik</string> + + <string name="sprefs_upscaler_linear">Lineáris</string> + <string name="sprefs_upscaler_nearest">Legközelebbi</string> + <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (minőség)</string> + <string name="sprefs_upscaler_sharpen">Csak élesítés</string> + <string name="sprefs_lod_bias_auto">Automatikus (illeszkedik a méretezéshez)</string> + + <string name="look_off_desc">Semmi nincs alkalmazva.</string> + <string name="look_game_clarity">Játéktisztaság</string> + <string name="look_game_clarity_desc">Élesítés egy csipetnyi kontraszttal – a mindennapi.</string> + <string name="look_vivid">Élénk</string> + <string name="look_vivid_desc">Erősen fokozott szín. Csíkmentesítés bekapcsolva, hogy az égbolt ne csíkozódjon.</string> + <string name="look_cinematic">Filmszerű</string> + <string name="look_cinematic_desc">Mélyebb kontraszt, hidegebb szín, simított élek.</string> + <string name="look_competitive">Versenyzős</string> + <string name="look_competitive_desc">Felemelt árnyékok és erős élesítés – vegye észre először az embereket.</string> + <string name="look_adaptive_sharpen">Adaptív élesítés</string> + <string name="look_adaptive_sharpen_desc">Csak élesítés, a színek érintetlenek.</string> + <string name="look_filmic">Filmes</string> + <string name="look_filmic_desc">Filmszerű görbe, enyhén tompított.</string> + <string name="look_arcade">Arcade</string> + <string name="look_arcade_desc">Hangos és ütős.</string> + <string name="look_retro_crt">Retro CRT</string> + <string name="look_retro_crt_desc">A CRT-képernyő és az analóg jel átmenetei együtt.</string> + <string name="look_upscale_sharp">Felskálázó élesítés</string> + <string name="look_upscale_sharp_desc">A panel felbontása alatti futtatáshoz.</string> + <string name="look_pixel_clean">Pixeltiszta</string> + <string name="look_pixel_clean_desc">Éles pixelek 2D-s és régi játékokhoz.</string> + <string name="look_anime_edge">Anime élek</string> + <string name="look_anime_edge_desc">Rajzfilmes körvonalak plusz élesítés.</string> + + <string name="gpuinfo_not_adreno">Nem Adreno GPU</string> + <string name="gpuinfo_supported">Támogatott</string> + <string name="gpuinfo_experimental">Kísérleti: az illesztőprogramjai teszt buildek</string> + <string name="gpuinfo_below_tested">A tesztelt hardver alatt (Adreno 725 és újabb): lehet, hogy nem fut</string> + <string name="gpuinfo_unsupported">Nem támogatott: a DroidDecknek Adreno (Snapdragon) GPU szükséges</string> + + <string name="turnip_rate_limited">A GitHub korlátja elérve – próbálja újra később</string> + <string name="turnip_github_http">A GitHub HTTP %1$d választ adott</string> + <string name="turnip_download_http">a letöltés HTTP %1$d választ adott</string> + <string name="turnip_list_predates_checksums">Ez az illesztőprogram-lista régebbi, mint az ellenőrzőösszegek – frissítse, és próbálja újra</string> + <string name="turnip_checksum_mismatch">Az ellenőrzőösszeg nem egyezik – a letöltés elvetve</string> + <string name="turnip_not_adrenotools">Nem egy AdrenoTools illesztőprogram: %1$s</string> + <string name="turnip_reject_no_meta">nincs meta.json</string> + <string name="turnip_reject_meta_unreadable">a meta.json nem olvasható (%1$s)</string> + <string name="turnip_reject_linux_driver">ez egy Linux futtatókörnyezet-illesztőprogram (importálja a „Linux futtatókörnyezet-illesztőprogram” alatt)</string> + <string name="turnip_reject_wayland">ez egy Wayland játékillesztőprogram, amelyet ez az alkalmazás nem tud használni</string> + <string name="turnip_reject_no_library_name">a meta.json nem nevez meg libraryName-et, így semmit nem lehetett átadni az AdrenoToolsnak</string> + <string name="turnip_reject_library_missing">a meta.json a %1$s-t nevezi meg, amely nincs a zipben</string> + <string name="turnip_reject_not_aarch64">a %1$s nem 64 bites AArch64 ELF megosztott könyvtár</string> + <string name="turnip_reject_glibc">a %1$s a glibc-hez linkel – egy „-Linux” Turnip, amely a „Linux futtatókörnyezet-illesztőprogram” alá tartozik</string> + <string name="lvd_no_library">Nincs libvulkan_freedreno*.so ebben a zipben. Egy Android (AdrenoTools) vagy -Wayland Turnip zip nem Linux futtatókörnyezet-illesztőprogram.</string> + <string name="lvd_not_aarch64">A %1$s nem 64 bites AArch64 ELF megosztott könyvtár.</string> + <string name="lvd_not_glibc">A %1$s nem glibc illesztőprogram – az Android libc-jéhez linkel. A Linux futtatókörnyezethez „-Linux” zip szükséges; egy egyszerű vagy „-Wayland” Turnipet a Steam-kliens egyáltalán nem tudja betölteni.</string> + <string name="bundle_no_manifest">Nem illesztőprogram-csomag: nincs manifest.json, amely android és linux illesztőprogramot nevez meg</string> + <string name="bundle_entry_outside">Nem illesztőprogram-csomag: a %1$s rajta kívülre mutat</string> + <string name="bundle_too_large">Nem illesztőprogram-csomag: kicsomagolva több mint %1$d MB</string> + + <string name="gstore_sd_card">SD-kártya</string> + <string name="gstore_folder">Mappa</string> + + <string name="logshare_chooser">Munkamenet-naplók megosztása</string> + + <string name="hdr_needs_android_10">Android 10 vagy újabb szükséges (színtérrel rendelkező megjelenítési rétegek)</string> + <string name="hdr_none">ez a kijelző nem jelez HDR-támogatást</string> + <string name="hdr_not_hdr10">ez a kijelző a %1$s-t támogatja, nem a HDR10-et</string> + + <string name="gpu_this_gpu">ez a GPU</string> + <string name="phantom_title_on">A gyermekfolyamat-korlát be van kapcsolva</string> + <string name="phantom_title_unset">A gyermekfolyamat-korlát nincs beállítva</string> + <string name="phantom_title_unreadable">A gyermekfolyamat-korlátot nem sikerült ellenőrizni</string> + <string name="phantom_title_off">A gyermekfolyamat-korlát ki van kapcsolva</string> + <string name="phantom_title_not_required">A gyermekfolyamat-korlát nem szükséges</string> + <string name="phantom_instructions_fix">%1$s Ha ez nem lehetséges, csatlakoztassa ezt az eszközt egy ADB-vel rendelkező számítógéphez, és futtassa az alábbi parancsot. Térjen vissza ide, és ellenőrizze újra.</string> + <string name="phantom_instructions_off">A Steam-munkamenetek elindulhatnak.</string> + <string name="phantom_instructions_not_applicable">Ez az Android-verzió nem használja ezt a korlátot.</string> + <string name="phantom_fix_toggle">Kapcsolja be a „Gyermekfolyamat-korlátozások letiltása” lehetőséget a Fejlesztői beállításokban, és utána hagyja bekapcsolva a Fejlesztői beállításokat.</string> + <string name="phantom_fix_no_toggle">Ehhez az Android-verzióhoz nincs kapcsoló. Használja a vezeték nélküli hibakeresést a módosításához erről az eszközről.</string> + <string name="upd_block_no_apk">Ehhez a buildhez nincs letöltés a DroidDeck ezen másolatához.</string> + <string name="upd_block_version_code">Az Android nem telepíti ezt a buildet fölé, mert a %1$d versionCode-ja a telepített %2$d alatt van.</string> + <string name="upd_fail_downgrade">Az Android nem telepít régebbi verziót egy újabb fölé. Maradjon ezen a buildon, amíg a csatorna felzárkózik.</string> + <string name="upd_fail_signature">Ez a DroidDeck-másolat másként van aláírva (talán egy helyi build?), így az Android nem frissíti a helyén.</string> + <string name="upd_fail_storage">Nincs elég szabad hely a frissítéshez.</string> + <string name="upd_fail_aborted">A frissítés megszakítva.</string> + <string name="upd_fail_with_message">A frissítés nem települt: %1$s</string> + <string name="upd_fail_generic">A frissítés nem települt.</string> + <string name="saves_layout_zip">%1$s zip</string> + <string name="saves_zip">zip</string> + <string name="saves_not_launched">A %1$s még nem lett elindítva</string> + <string name="saves_none_found">Nem található mentésfájl a %1$s-hez</string> + <string name="saves_not_save_zip">A %1$s-ben nincs drive_c mappa, így nem egy GameHub vagy Winlator mentés zip</string> + <string name="gstore_cannot_create">nem lehet mappákat létrehozni itt: %1$s</string> + <string name="gstore_cannot_write">nem lehet írni ide: %1$s</string> + <string name="gstore_cannot_write_detail">nem lehet írni ide: %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">A mappa nem elérhető</string> + <string name="gfiles_name_taken">Egy másik fájl már használja a %1$s nevet</string> + <string name="gfiles_art_unavailable">Az illusztrációk mappája nem elérhető</string> + <string name="gfiles_unrelated_inventory">A mappában egy nem kapcsolódó szinkronizációs leltár van</string> + <string name="gfiles_remove_failed">Nem sikerült eltávolítani a %1$s-t</string> + <string name="gfiles_outside_folder">A fájl kilép az exportmappából</string> + <string name="cmgr_fex_unnamed">FEX (verziónév nélküli build)</string> + <string name="cmgr_no_profile">nincs profile.json a %1$s-ben</string> + <string name="cmgr_not_component_package">A %1$s nem Linux-összetevőcsomag (típus: %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s szabad</string> + <string name="mode_storage_auto_short">Automatikus</string> + <string name="appupd_catalog_rejected">A frissítési katalógus nem ment át az ellenőrzéseken (%1$s)</string> + <string name="main_adb_clip_label">DroidDeck gyermekfolyamat-beállítás</string> + <string name="setup_signed_in_offline_unnamed">Bejelentkezve · offline mód</string> + <string name="setup_signed_in_unnamed">Bejelentkezve</string> + <string name="driver_tag_bundled">CSOMAGOLT</string> + <string name="driver_tag_downloaded">LETÖLTÖTT</string> + <string name="driver_tag_imported">IMPORTÁLT</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Munkamenet kijelzője</string> + <string name="display_resolution">Felbontás</string> + <string name="display_game_fit">16:9 játék illesztése</string> + <string name="display_game_fit_hint">Hogyan illeszkednek a 16:9 játékok ehhez a kijelzőhöz. A következő munkamenettől.</string> + <string name="display_game_fit_unavailable">Válassza a Képernyőhöz illesztést, vagy egy 16:9-nél szűkebb egyéni méretet.</string> + <string name="display_preserve">Arányok megtartása</string> + <string name="display_stretch">Nyújtás a kijelzőhöz</string> + <string name="display_image_scaling">Képméretezés</string> + <string name="display_filter">Méretezési szűrő</string> + <string name="display_filter_hint">A Steam és az Asztal közösen használja.</string> + <string name="display_filter_live">Élőben érvénybe lép. A Steam és az Asztal közösen használja.</string> + <string name="display_filter_note">Az FSR, a GSR és a NIS a munkamenet képének felnagyításakor lép érvénybe. A Csak élesítés nagyítás nélkül is működik.</string> + <string name="display_sharpness">Szűrőélesség</string> + <string name="display_frame_rate">Képkockasebesség</string> + <string name="display_look_filter">%1$s A méretezést erre állítja: %2$s.</string> + <string name="display_match_screen">Képernyőhöz illesztés (%1$d×%2$d)</string> + <string name="display_follow_screen">Képernyő követése (átméreteződik össze- vagy kinyitáskor)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Eszközrészletek megjelenítése</string> + <string name="device_info_hide">Eszközrészletek elrejtése</string> + <string name="device_info_loading">Az eszköz beolvasása…</string> + <string name="device_info_copy">Másolás</string> + <string name="device_info_copy_hint">Másolja ezeket a részleteket egy hibajelentésbe.</string> + <string name="device_info_copied">Másolva.</string> + <string name="force_ssbs_title">SSBS kényszerítése</string> + <string name="force_ssbs_hint">Bekapcsolva tartja azt a CPU-sebesség-beállítást, amelyet a Wine folyton kikapcsol. Gyorsíthatja a Windows-os játékokat Snapdragon 8 Elite-n; régebbi chipeken alig van hatása. A következő játékindítástól érvényes.</string> + + <string name="rail_stores">Áruházak</string> + <string name="source_steam">Steam</string> + <string name="source_added">Egyéni</string> + <string name="setup_stores">Áruházak</string> + <string name="setup_stores_open_on">Áruház megnyitása itt</string> + <string name="setup_stores_speed">Letöltési sebességi szint</string> + <string name="setup_stores_engine">Áruházmotor</string> + <string name="setup_stores_engine_chip_ready">● A motor kész</string> + <string name="setup_stores_engine_chip_missing">Nincs buildelve</string> + <string name="stores_tier_balanced">Kiegyensúlyozott</string> + <string name="stores_tier_fast">Gyors</string> + <string name="stores_tier_max">Max</string> + <string name="stores_title">Áruházak</string> + <string name="stores_settings">Áruházbeállítások</string> + <string name="stores_settings_eyebrow">Beállítások</string> + <string name="stores_chip_signed_out">(nincs bejelentkezve)</string> + <string name="stores_tab_store">Áruház</string> + <string name="stores_tab_installed">Telepítve</string> + <string name="stores_tab_library">Könyvtár</string> + <string name="stores_tab_all">Összes</string> + <string name="stores_tab_downloads">Letöltések</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Bejelentkezés</string> + <string name="stores_signout_named">%1$s · kijelentkezés</string> + <string name="stores_signout_title">Kijelentkezik a %1$s-ből?</string> + <string name="stores_signout_note">A telepített játékok megmaradnak. A könyvtár újra lekérésre kerül, amikor újra bejelentkezik.</string> + <string name="stores_signout_stay">Bejelentkezve marad</string> + <string name="stores_signout_go">Kijelentkezés</string> + <string name="stores_search_catalog">Keresés a %1$s katalógusban…</string> + <string name="stores_search_installed">Keresés a telepített játékok között…</string> + <string name="stores_search_library">Keresés a könyvtárában…</string> + <string name="stores_search_open">Keresés</string> + <string name="stores_installed_empty">Nincs telepítve semmi</string> + <string name="stores_library_loading">Betöltés</string> + <string name="stores_nothing_matches">Nincs játék</string> + <string name="stores_shelves_loading">Az áruház betöltése…</string> + <string name="stores_amazon_no_catalog">Nincs katalógus</string> + <string name="stores_shelf_new">Újdonságok</string> + <string name="stores_shelf_deals">Akciók</string> + <string name="stores_shelf_free">Ingyenes játékok</string> + <string name="stores_shelf_trending">Felkapott</string> + <string name="stores_shelf_library">A könyvtára</string> + <string name="stores_play">Játék</string> + <string name="stores_install">Telepítés</string> + <string name="stores_resume_install">Telepítés folytatása</string> + <string name="stores_dl_interrupted">Megszakítva</string> + <string name="stores_install_size">Telepítés · %1$s</string> + <string name="stores_get_free">Ingyenes beszerzés</string> + <string name="stores_view_on">Megtekintés itt: %1$s</string> + <string name="stores_buy_price">Megvétel · %1$s</string> + <string name="stores_installed_chip">Telepítve</string> + <string name="stores_in_library">A könyvtárban</string> + <string name="stores_free">Ingyenes</string> + <string name="stores_resume">Folytatás</string> + <string name="stores_refresh">Frissítés</string> + <string name="stores_show_mature">Felnőtt tartalom megjelenítése</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Offline</string> + <string name="epic_card_none">Ki</string> + <string name="epic_eos">Epic bejelentkezés (EOS)</string> + <string name="epic_offline">Indítás offline</string> + <string name="epic_resolve">Epic bejelentkezés megoldása</string> + <string name="store_cloud_saves">Felhőmentések</string> + <string name="cloud_upload">Feltöltés a felhőbe</string> + <string name="cloud_download">Letöltés a felhőből</string> + <string name="cloud_none">Nincs felhőmentés</string> + <string name="cloud_conflict">Felhőütközés</string> + <string name="cloud_conflict_files">Felhőütközés · %1$d fájl</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Felhő megtartása</string> + <string name="cloud_keep_local">Helyi megtartása</string> + <string name="cloud_checking">Felhő ellenőrzése…</string> + <string name="cloud_last_sync">Utolsó szinkronizálás: %1$s</string> + <string name="cloud_never">Még nincs szinkronizálva</string> + <string name="cloud_done">%1$d fájl</string> + <string name="stores_eyebrow_installed">telepítve</string> + <string name="stores_eyebrow_library">a könyvtárában</string> + <string name="stores_eyebrow_store">áruház</string> + <string name="achievements_title">Eredmények</string> + <string name="achievements_count">%1$d / %2$d feloldva</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Feloldva · %1$d</string> + <string name="achievements_group_locked">Zárolva · %1$d</string> + <string name="achievements_group_hidden">Rejtett · %1$d</string> + <string name="achievements_hidden">Rejtett eredmény</string> + <string name="achievements_hidden_detail">A részletek a feloldás után jelennek meg.</string> + <string name="achievements_all">Minden eredmény feloldva</string> + <string name="achievements_reading">Betöltés…</string> + <string name="achievements_signed_out">Jelentkezz be a %1$s fiókodba az eredményeid megtekintéséhez.</string> + <string name="achievements_unreachable">Nem sikerült elérni a %1$s szolgáltatást. Nézd meg újra, ha online vagy.</string> + <string name="achievements_none">A %1$s nem sorol fel eredményt ehhez a játékhoz.</string> + <string name="achievements_show_all">Mind a %1$d megjelenítése</string> + <string name="achievements_dialog_title">Eredmények · %1$s</string> + <string name="stores_in_steam">✓ A Steamben</string> + <string name="stores_uninstall">Eltávolítás</string> + <string name="stores_uninstalling">Eltávolítás…</string> + <string name="stores_uninstall_title">Eltávolítja a %1$s-t?</string> + <string name="stores_uninstall_body_size">%1$s szabadít fel, és eltávolítja a Steamből.</string> + <string name="stores_uninstall_body">Eltávolítja a fájljait és a Steam-parancsikonját.</string> + <string name="stores_launch_not_registered">A Steam a következő indításakor felveszi a játékot; indítsa el onnan, vagy indítsa el most a Steamet.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">A Steam választása játékonként</string> + <string name="stores_card_audio">Hang</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Összetevők</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Motor</string> + <string name="stores_card_engine_value">%1$s · natív</string> + <string name="stores_card_engine_java">%1$s · beépített</string> + <string name="stores_how_it_runs">Hogyan fut</string> + <string name="stores_kv_fetched">Lekérve innen</string> + <string name="stores_kv_fetched_value">%1$s, az Ön fiókjával</string> + <string name="stores_kv_installed_to">Telepítve ide</string> + <string name="stores_kv_installed_to_value">Játéktárhely (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Indítja</string> + <string name="stores_kv_launched_by_value">A Steam és a Proton, ugyanúgy, mint a Steam-játékoknál</string> + <string name="stores_kv_launched_by_epic">A Steam és a Proton · Epic bejelentkezés minden indításnál, EOS overlay kikapcsolva</string> + <string name="stores_kv_launched_by_amazon">A Steam és a Proton · Amazon Games Services változók beállítva</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">a Steamhez adva telepítéskor (Deck felhasználói felület + Steam Input)</string> + <string name="stores_downloads_empty">Nincs letöltés</string> + <string name="stores_dl_paused">Szüneteltetve</string> + <string name="stores_dl_queued">Sorban</string> + <string name="stores_dl_queued_at">Sorban · #%1$d</string> + <string name="stores_dl_cancelled">Megszakítva</string> + <string name="stores_dl_failed">Sikertelen</string> + <string name="stores_dl_failed_reason">Sikertelen · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Szüneteltetés</string> + <string name="stores_dl_resume">Folytatás</string> + <string name="stores_dl_cancel">Megszakítás</string> + <string name="stores_dl_retry">Újra</string> + <string name="stores_dl_clear">Törlés</string> + <string name="stores_dl_clear_all">Befejezettek törlése</string> + <string name="stores_stage_manifest">Manifeszt</string> + <string name="stores_stage_download">Letöltés</string> + <string name="stores_stage_verify">Ellenőrzés</string> + <string name="stores_stage_install">Telepítés</string> + <string name="stores_stage_done">Kész</string> + <string name="stores_stage_checking">Ellenőrzés</string> + <string name="stores_stage_downloading">Letöltés</string> + <string name="stores_dl_cancel_confirm">Törli a letöltést?</string> + <string name="stores_dl_failed_kept">Sikertelen · %1$s megtartva</string> + <string name="stores_stage_verifying">Ellenőrzés</string> + <string name="stores_stage_installing">Telepítés</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d fájl</string> + <string name="stores_download_manager">Letöltéskezelő</string> + <string name="stores_parallel">Egyszerre futó letöltések</string> + <string name="stores_install_where">Telepítés ide</string> + <string name="stores_install_title">%1$s telepítése:</string> + <string name="stores_target_internal">Belső</string> + <string name="stores_target_free">%1$s szabad</string> + <string name="stores_notification_channel">Áruházletöltések</string> + <string name="stores_notification_channel_hint">Megjeleníti a játékletöltés folyamatát, és futva tartja a letöltéseket a háttérben</string> + <string name="stores_notification_one">Letöltés</string> + <string name="stores_notification_many">%1$d letöltés</string> + <string name="stores_notification_waiting">Előkészítés…</string> + <string name="stores_notification_done_channel">Befejezett áruházletöltések</string> + <string name="stores_notification_installed">%1$s telepítve</string> + <string name="stores_notification_failed">A %1$s letöltése sikertelen</string> + <string name="stores_notification_delete">Törlés</string> + <string name="stores_notification_keep">Megtartás</string> + <string name="stores_login_title">Bejelentkezés ide: %1$s</string> + <string name="stores_login_provider_title">Bejelentkezés a szolgáltatójával</string> + <string name="stores_login_reload">Újratöltés</string> + <string name="stores_login_close">Bezárás</string> + <string name="stores_login_retry">Próbálja újra</string> + <string name="stores_login_finishing">A bejelentkezés befejezése…</string> + <string name="stores_login_error_network">Nem sikerült betölteni a bejelentkezési oldalt: %1$s</string> + <string name="stores_login_error_idp_blocked">Ez a szolgáltató nem engedi a bejelentkezést egy beágyazott oldalról. Jelentkezzen be helyette az áruház jelszavával.</string> + <string name="stores_login_error_http">A bejelentkezési oldal hibával válaszolt (HTTP %1$d).</string> + <string name="stores_login_error_ssl">A kapcsolat a bejelentkezési oldalhoz nem biztonságos.</string> + <string name="stores_login_error_verification">A bejelentkezést nem sikerült ellenőrizni. Próbálja újra.</string> + <string name="stores_login_error_generic">A bejelentkezés sikertelen. Próbálja újra.</string> + <string name="common_close">Bezárás</string> + <string name="common_back">Vissza</string> + <string name="common_edit">Szerkesztés</string> + <string name="games_add_all">Minden játék hozzáadása ebből a mappából</string> + <string name="games_added_none">Nincs semmi új ebben a mappában</string> + <string name="edit_name">Név</string> + <string name="edit_target">Cél</string> + <string name="edit_start_in">Indítás innen</string> + <string name="edit_launch_options">Indítási beállítások</string> + <string name="edit_artwork">Illusztráció</string> + <string name="edit_other_exe">Másik exe…</string> + <string name="edit_remove">Eltávolítás</string> + <string name="art_cover">Borító</string> + <string name="art_background">Háttér</string> + <string name="art_logo">Logó</string> + <string name="art_icon">Ikon</string> + <string name="art_src_folder">Játékmappa</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">Saját fájl</string> + <string name="art_src_exe">Az exe-ből</string> + <string name="art_src_auto">Automatikus</string> + <string name="art_pick_image">Kép kiválasztása…</string> + <string name="art_pick_title">Kép kiválasztása: %1$s</string> + <string name="art_reset">Visszaállítás automatikusra</string> + <plurals name="games_added_n"> + <item quantity="one">%1$d játék hozzáadva</item> + <item quantity="other">%1$d játék hozzáadva</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="one">%1$d már a Játékokban van</item> + <item quantity="other">%1$d már a Játékokban van</item> + </plurals> + <string name="hop_back">B · Vissza: %1$s</string> + <string name="hop_from_games">a játék</string> + <string name="hop_from_play">Játék beállítások</string> + <string name="sheet_graphics_drivers">Grafikai illesztőprogramok</string> + <string name="comp_this_proton">Ez a Proton</string> + <string name="comp_every_proton">Minden Proton</string> + <string name="comp_for_every_proton">Minden Protonhoz</string> + <string name="comp_available_count">Elérhető · %1$d</string> + <string name="comp_checked">Ellenőrizve: %1$s</string> + <string name="comp_check_all">Új összetevők és illesztőprogramok keresése</string> + <string name="comp_get">Beszerzés</string> + <string name="comp_proton_bar">%1$s · a lenti DXVK, VKD3D-Proton és FEX ehhez a Protonhoz tartozik</string> + <string name="comp_device_tested">%1$s · tesztelt</string> + <string name="comp_device_untested">%1$s · nem tesztelt</string> + <string name="comp_device_unsupported">%1$s · nem támogatott</string> + <string name="gpu_auto_keeps">Naprakészen tartja az ajánlott párost</string> + <string name="gpu_bar_auto">Automatikus · %1$s</string> + <string name="gpu_separately">Futtatókörnyezet- és megjelenítő-illesztőprogramok külön</string> + <string name="gpu_use_title">Használja a %1$s-t?</string> + <string name="proton_remove_title">Eltávolítja a %1$s-t?</string> + <string name="setup_tab_diagnostics">Diagnosztika</string> + <string name="setup_ready">Készen áll a játékra</string> + <string name="setup_not_ready">Még nem áll készen a játékra</string> + <string name="setup_ready_runtime">futtatókörnyezet: %1$s</string> + <string name="setup_ready_limit_off">folyamatkorlát kikapcsolva</string> + <string name="setup_ready_signed_in">bejelentkezve mint %1$s</string> + <string name="setup_needs_you">Amikor szükség van rá</string> + <string name="setup_group_look">Megjelenés</string> + <string name="setup_group_device">Ez az eszköz</string> + <string name="setup_group_art">Könyvtár illusztrációi</string> + <string name="flathub_eyebrow">LINUX-ALKALMAZÁSOK</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Asztali alkalmazások a Linux-asztalhoz, az Asztalról megnyitva.</string> + <string name="flathub_setup">Flathub beállítása</string> + <string name="flathub_remove_title">Eltávolítja a %1$s-t?</string> + <string name="flathub_remove_note">A beállításai és fájljai megmaradnak.</string> + <string name="flathub_keep">Megtartás</string> + <string name="desktop_size">Méret a tárhelyen</string> + <string name="desktop_uninstall_title">Eltávolítja az asztalt?</string> + <string name="desktop_all_systems">Összes</string> + <string name="gog_achievements">GOG-eredmények</string> + <string name="gog_achievements_hint">Szinkronizálja az eredményeket a támogatott GOG-játékokhoz. A következő indítástól lép életbe.</string> + <string name="gog_achievements_off">Eredmények kikapcsolva</string> + <string name="gog_achievements_on">Eredmények bekapcsolva</string> + <string name="gog_achievements_signin">Jelentkezz be újra a GOG-ba</string> + <string name="gog_achievements_uploaded">Eredmények feltöltve</string> + <string name="gog_achievements_pending">Az eredmény feltöltése függőben</string> + <string name="gog_achievements_enable_store">Kapcsold be a GOG-eredményeket a Store-beállításokban</string> + <string name="gog_achievements_unavailable">Eredmények nem elérhetők; nézd meg a munkamenet-naplókat</string> + <string name="achievement_unlocked">Eredmény feloldva</string> + <string name="touch_paste">Beillesztés</string> + <string name="touch_dpad">Iránykereszt</string> + <string name="touch_left_stick">Bal kar</string> + <string name="touch_right_stick">Jobb kar</string> + <string name="touch_menu">Menü</string> + <string name="touch_keyboard">Billentyűzet</string> + <string name="touch_dpad_up">Iránykereszt fel</string> + <string name="touch_dpad_down">Iránykereszt le</string> + <string name="touch_dpad_left">Iránykereszt balra</string> + <string name="touch_dpad_right">Iránykereszt jobbra</string> + <string name="touch_title">Érintővezérlők</string> + <string name="touch_controller">Érintőkontroller</string> + <string name="touch_mouse">Egér</string> + <string name="touch_on">Be</string> + <string name="touch_off">Ki</string> + <string name="touch_speed_slow">Lassú</string> + <string name="touch_speed_normal">Normál</string> + <string name="touch_speed_fast">Gyors</string> + <string name="touch_edit_layout">Elrendezés szerkesztése</string> + <string name="touch_droiddeck_menu">DroidDeck menü</string> + <string name="touch_drag_hint">Húzd a vezérlőket a képernyőre</string> + <string name="touch_arrange">Elrendezés</string> + <string name="touch_discard">Elvetés</string> + <string name="touch_saving">Mentés…</string> + <string name="touch_icon">Ikon</string> + <string name="touch_narrower">Sz−</string> + <string name="touch_wider">Sz+</string> + <string name="touch_shorter">M−</string> + <string name="touch_taller">M+</string> + <string name="touch_hide">Elrejtés</string> + <string name="touch_color_opacity">Szín és átlátszatlanság…</string> + <string name="touch_copy_layout">Elrendezés másolása</string> + <string name="touch_layout_copied">Elrendezés másolva; illeszd be bármelyik játék érintőelrendezésébe</string> + <string name="touch_paste_layout">Másolt elrendezés beillesztése</string> + <string name="touch_reset_layout">Visszaállítás a Steam alapértelmezésére</string> + <string name="touch_no_binding">Ehhez a vezérlőhöz nincs hozzárendelés a Steamben, amihez ikont lehetne adni</string> + <string name="touch_icon_for">Ikon ehhez: %1$s</string> + <string name="touch_layout_color">Érintőelrendezés színe</string> + <string name="touch_done">Kész</string> + <string name="touch_save_failed">Az érintőelrendezést nem sikerült elmenteni</string> + <string name="touch_saved">Az érintőelrendezés elmentve a Steambe</string> + <string name="touch_app_id">%1$d. alkalmazás</string> + <string name="touch_label_hint">Felirat (nem kötelező)</string> + <string name="touch_icons_missing">A Steam ikonjai nem találhatók</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d ikon a Steamről</item> + <item quantity="other">%1$d ikon a Steamről</item> + </plurals> + <string name="touch_icon_color">Ikon színe</string> + <string name="touch_button_color">Gomb színe</string> + <string name="touch_no_icon">Nincs ikon</string> + + <string name="stores_sync_sign_in">Bejelentkezés ellenőrzése…</string> + <string name="stores_sync_library">Könyvtárad lekérése…</string> + <string name="stores_sync_game_list">Játéklista lekérése…</string> + <string name="stores_sync_details">Játékadatok betöltése… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">%1$d játék szinkronizálása…</item> + <item quantity="other">%1$d játék szinkronizálása…</item> + </plurals> + <string name="stores_sync_progress">Szinkronizálás… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Borítóképek lekérése…</string> + <string name="stores_sync_failed">A szinkronizálás sikertelen</string> + <string name="stores_sync_no_games">Nem található játék a %1$s könyvtáradban</string> + <string name="stores_sync_amazon_no_answer">Az Amazon Games nem válaszolt a könyvtáradra vonatkozóan: frissítsd, vagy jelentkezz be újra</string> + <string name="stores_sync_gog_unreachable">Nem sikerült csatlakozni a GOG-hoz</string> + <string name="stores_sync_gog_unreadable">A GOG-könyvtáradat nem sikerült beolvasni</string> + <string name="stores_registering">Regisztráció a Steamnél…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Cardboard mód</string> + <string name="cardboard_mode_hint">Bal és jobb szem képe telefonos VR-szemüveghez.</string> + <string name="cardboard_source">Kép</string> + <string name="cardboard_source_screen">Virtuális képernyő</string> + <string name="cardboard_source_sbs">Egymás melletti 3D</string> + <string name="cardboard_viewer">Szemüveg</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Lencse nélkül</string> + <string name="cardboard_ipd">Szemtávolság</string> + <string name="cardboard_screen_width">Képernyő szélessége</string> + <string name="cardboard_screen_distance">Képernyő távolsága</string> + <string name="cardboard_head_tracking">Fejkövetés</string> + <string name="cardboard_no_sensor">Ezen az eszközön nincs forgásérzékelő.</string> + <string name="cardboard_recenter">Nézet középre igazítása</string> + <string name="cardboard_recenter_button">Középre</string> + <string name="cardboard_steamvr">SteamVR (kísérleti)</string> + <string name="cardboard_steamvr_install">Telepítés</string> + <string name="cardboard_steamvr_install_hint">Letölti a Valve ARM64-es SteamVR-jét (571 MB).</string> + <string name="cardboard_steamvr_installing">SteamVR letöltése és kicsomagolása…</string> + <string name="cardboard_steamvr_start">Indítás</string> + <string name="cardboard_steamvr_ready_hint">A SteamVR-t a fejeddel mint headsettel futtatja; a kép egymás melletti 3D-re vált.</string> + <string name="cardboard_steamvr_stop">Leállítás</string> + <string name="cardboard_steamvr_running_hint">A SteamVR fut.</string> + <string name="cardboard_steamvr_unavailable">Ebben a munkamenetben nem érhető el.</string> + <string name="cardboard_steamvr_failed">A SteamVR nem válaszolt; nézd meg a munkamenet naplóját.</string> + <string name="drawer_component_downloads">Összetevők letöltése</string> + <string name="drawer_component_downloads_hint">A letöltések közösek az Összetevők oldallal. Fent válassz egy letöltött verziót, hogy a következő játékindítástól azt használd.</string> + <string name="drawer_component_choose_download">Válassz letöltendő verziót</string> + <string name="stores_fact_developer">Fejlesztő</string> + <string name="stores_fact_publisher">Kiadó</string> + <string name="stores_fact_released">Megjelenés</string> + <string name="stores_hltb_main">Fő történet</string> + <string name="stores_hltb_extras">+ Extrák</string> + <string name="stores_hltb_completionist">Teljes</string> + <string name="stores_hltb_hours">%1$s óra</string> + <string name="stores_hltb_minutes">%1$d perc</string> +</resources> diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml new file mode 100644 index 000000000..7e4049f3c --- /dev/null +++ b/app/src/main/res/values-in/strings.xml @@ -0,0 +1,2210 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>Proses pertama mengunduh Steam. Ini mungkin memerlukan beberapa menit.</item> + <item>Secara default, keluar dari aplikasi akan menjeda Steam; sesi dilanjutkan dari posisi terakhir saat Anda kembali.</item> + <item>Pengontrol Anda akan terlihat sebagai gamepad Xbox 360. Jika tidak, gunakan kontrol di layar.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>Proses pertama menyiapkan desktop. Ini mungkin memerlukan beberapa menit.</item> + <item>Secara default, keluar dari aplikasi akan menjeda sesi; sesi dilanjutkan dari posisi terakhir saat Anda kembali.</item> + <item>Sentuhan berfungsi seperti touchpad: geser untuk menggerakkan penunjuk, ketuk untuk mengeklik.</item> + <item>Pengontrol Anda akan terlihat sebagai gamepad Xbox 360. Jika tidak, gunakan kontrol di layar.</item> + </string-array> + <string name="session_channel">Sesi</string> + <string name="session_channel_description">Ditampilkan saat sesi Steam atau desktop berjalan</string> + <string name="session_notification">Steam berjalan</string> + <string name="session_notification_downloads">Unduhan Steam aktif</string> + <string name="session_notification_paused">Sesi dijeda - ketuk Lanjutkan saat siap</string> + <string name="resume_session">Lanjutkan</string> + <string name="suspend_now">Jeda sekarang</string> + <string name="stop_session">Hentikan sesi</string> + <string name="frame_gen_title">Pembuatan frame</string> + <string name="frame_gen_hint">Pilih mode pembuatan frame</string> + <string name="frame_gen_off">Nonaktif</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG Adaptif %1$d</string> + <string name="lsfg_needs_steam">Memerlukan Lossless Scaling di Steam</string> + <string name="lsfg_needs_install">Instal Lossless Scaling di Steam atau impor Lossless.dll</string> + <string name="lsfg_import">Impor Lossless.dll…</string> + <string name="lsfg_using_steam">Menggunakan salinan dari Steam</string> + <string name="lsfg_using_import">Menggunakan salinan yang diimpor</string> + <string name="lsfg_pick_title">Pilih Lossless.dll</string> + <string name="lsfg_imported">Lossless.dll diimpor</string> + <string name="lsfg_import_unchanged">Lossless.dll ini sudah digunakan</string> + <string name="lsfg_import_not_owned">Lossless Scaling tidak dimiliki di akun Steam ini</string> + <string name="lsfg_import_invalid">File ini bukan Lossless.dll milik Lossless Scaling</string> + <string name="lsfg_import_failed">Tidak dapat menyiapkan LSFG dari Lossless.dll ini</string> + <string name="lsfg_problem_missing">LSFG memerlukan Lossless Scaling dari Steam atau Lossless.dll yang diimpor</string> + <string name="lsfg_problem_failed">LSFG tidak dapat menyiapkan shader-nya dari Lossless.dll</string> + <string name="hud_fg_unsupported">%1$s: tidak didukung</string> + <string name="hud_fg_failed">%1$s: mesin gagal</string> + <string name="hud_fg_needs_lossless">%1$s: memerlukan Lossless Scaling</string> + <string name="hud_fg_starting">%1$s %2$s · memulai</string> + <string name="second_library_import_hint">Menambahkan pustaka Steam dan memindainya untuk gim Windows. Gim yang sudah dimiliki akan menggunakan entri Steam-nya; sisanya menjadi pintasan non-Steam.</string> + <string name="game_env_title">Lingkungan gim</string> + <string name="game_env_hint">Variabel lingkungan untuk gim Proton.</string> + <string name="game_env_edit">Edit</string> + <string name="game_env_shared">Semua gim Proton</string> + <string name="game_env_scope">Profil</string> + <string name="game_env_app_id">ID aplikasi Steam</string> + <string name="game_env_open">Buka</string> + <string name="game_env_profile">Gim %1$s</string> + <string name="game_env_add">Tambah variabel</string> + <string name="game_env_name">Nama variabel</string> + <string name="game_env_value">Nilai</string> + <string name="game_env_save">Simpan</string> + <string name="game_env_remove">Hapus</string> + <string name="game_env_restore">Warisi kembali</string> + <string name="game_env_reset">Hapus semua penggantian lingkungan</string> + <string name="game_env_unset">Nonaktifkan untuk profil ini</string> + <string name="game_env_empty">Belum ada variabel yang diatur.</string> + <string name="game_env_inherited">Diwariskan</string> + <string name="game_env_custom">Variabel khusus</string> + <string name="game_env_error">Tidak dapat memuat atau menyimpan. Periksa penyimpanan kosong dan coba lagi.</string> + <string name="game_env_invalid">Gunakan nama yang valid dan nilai hingga 8192 karakter.</string> + <string name="game_env_unused">Runtime Proton standar tidak membaca variabel ini.</string> + <string name="game_env_loading">Memuat…</string> + <string name="game_env_feature">Override level fitur D3D12. 12_2 melaporkan kemampuan DirectX 12 Ultimate; fitur yang tidak didukung dapat gagal. Biarkan kosong untuk deteksi otomatis.</string> + <string name="game_env_shader">Override model shader D3D12. 6_9 memerlukan VKD3D yang cukup baru dan fitur GPU yang sesuai. Biarkan kosong untuk deteksi otomatis.</string> + <string name="game_env_cache">Pertahankan cache shader Mesa tetap aktif untuk mengurangi kompilasi berulang. false mengaktifkan cache.</string> + <string name="game_env_cache_size">Ukuran maksimum cache shader Mesa, misalnya 1G. Cache yang lebih besar menggunakan lebih banyak penyimpanan.</string> + <string name="game_env_vkd3d_config">Opsi VKD3D untuk D3D12. nodxr menonaktifkan ray tracing; gunakan hanya untuk gim yang membutuhkannya.</string> + <string name="game_env_proton_log">Atur ke 1 untuk log diagnostik Proton. Pencatatan log dapat mengurangi performa dan menggunakan penyimpanan.</string> + <string name="game_env_wined3d">Atur ke 1 untuk menggunakan WineD3D berbasis OpenGL sebagai pengganti DXVK untuk pengujian kompatibilitas D3D9–11.</string> + <string name="game_env_no_ntsync">Atur ke 1 untuk menjaga gim ini di luar ntsync (droiddeck-ntsync atau driver kernel); gim ini menggunakan droiddeck-esync jika ada paket yang tersedia, jika tidak droiddeck-fsync.</string> + <string name="game_env_no_esync">Atur ke 1 untuk menonaktifkan droiddeck-esync bagi gim ini; gim ini menggunakan droiddeck-fsync, atau droiddeck-ntsync jika fsync juga nonaktif.</string> + <string name="game_env_no_fsync">Atur ke 1 untuk menjaga gim ini di luar droiddeck-fsync; gim ini menggunakan droiddeck-esync jika ada paket yang tersedia, jika tidak droiddeck-ntsync.</string> + <string name="game_env_fsync_first">Atur ke 1 untuk menjalankan gim ini pada droiddeck-fsync, atau 0 untuk mengutamakan droiddeck-esync jika ada paket yang cocok dengan Proton-nya.</string> + <string name="game_env_xalia">Atur ke 0 untuk menonaktifkan fitur bantu navigasi gamepad milik Proton untuk gim ini.</string> + <string name="game_env_hud">Overlay DXVK untuk D3D9–11. fps menampilkan frame rate; kosongkan untuk menyembunyikannya.</string> + <string name="game_env_dxvk_config">Setelan DXVK dipisahkan dengan titik koma. Versi terbaru menggunakan dxvk.maxFrameRate untuk membatasi frame rate; gunakan pembatas frame rate sesi untuk permainan biasa.</string> + <string name="game_env_vkd3d_limit">Batas frame rate D3D12. 0 menonaktifkan batas.</string> + <string name="game_env_glthread">Threading OpenGL Mesa. true dapat membantu gim OpenGL yang terbatas CPU; uji per gim.</string> + <string name="sync_backend_title">Sinkronisasi Wine</string> + <string name="sync_backend_hint">Cara gim menjaga thread-nya tetap selaras. Berlaku pada sesi berikutnya.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">Preset FEX</string> + <string name="fex_next_launch">Berlaku pada peluncuran gim berikutnya.</string> + <string name="game_settings_title">Gim</string> + <string name="fex_default_label">Default FEX</string> + <string name="fex_default_detail">Menggunakan default FEX.</string> + <string name="fex_stability_label">Stabilitas</string> + <string name="fex_stability_detail">Setelan paling aman dan paling lambat.</string> + <string name="fex_compatibility_label">Kompatibilitas</string> + <string name="fex_compatibility_detail">TSO penuh dengan multiblock.</string> + <string name="fex_intermediate_label">Menengah</string> + <string name="fex_intermediate_detail">TSO seimbang dengan presisi x87 yang dikurangi.</string> + <string name="fex_performance_label">Performa</string> + <string name="fex_performance_detail">Semua TSO dinonaktifkan; multiblock.</string> + <string name="fex_performance_tso_label">Performa + TSO</string> + <string name="fex_performance_tso_detail">Setelan performa dengan TSO utama diaktifkan.</string> + <string name="fex_extreme_label">Ekstrem</string> + <string name="fex_extreme_detail">Menonaktifkan pemeriksaan self-modifying-code. Gim JIT dan DRM dapat rusak.</string> + <string name="fex_extreme_tso_label">Ekstrem + TSO</string> + <string name="fex_extreme_tso_detail">Setelan ekstrem dengan TSO utama diaktifkan.</string> + <string name="fex_extreme_gn_label">Extreme-gn</string> + <string name="fex_extreme_gn_detail">Performa dengan TSC dan metadata hints. Coba sebelum Ekstrem.</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">Performa dengan pemeriksaan kode penuh dan bit hypervisor disembunyikan.</string> + <string name="game_env_custom_value">Nilai khusus</string> + <string name="game_env_select_value">Pilih nilai</string> + <string name="user_apps_section">Emulator & aplikasi</string> + <string name="user_apps_add">Tambah</string> + <string name="user_apps_add_detail">Aplikasi atau gim Anda sendiri</string> + <string name="user_apps_kind_script">Skrip</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d%%</string> + <string name="user_apps_starting">Memulai…</string> + <string name="user_apps_added">%1$s ditambahkan</string> + <string name="user_apps_removed">%1$s dihapus</string> + <string name="user_apps_back">Desktop</string> + <string name="user_apps_gone">Aplikasi itu sudah tidak ada. Pilih lagi dari Desktop.</string> + <string name="user_apps_open">Buka %1$s</string> + <string name="user_apps_remove">Hapus</string> + <string name="user_apps_remove_confirm">Tekan lagi untuk menghapus</string> + <string name="user_apps_stop_session">Hentikan sesi untuk menghapus</string> + <string name="user_apps_script_linked">Menjalankan %1$s dari foldernya, ditautkan ke dalam sesi tempatnya berada. Menghapus aplikasi ini tidak memengaruhi folder tersebut.</string> + <string name="user_apps_script_copied">Menjalankan %1$s dari salinan foldernya di dalam DroidDeck. Menghapus aplikasi ini menghapus salinannya, bukan aslinya.</string> + <string name="user_apps_runtime_required">Instal runtime Linux terlebih dahulu</string> + <string name="user_apps_file_gone">File tersebut sudah tidak ada</string> + <string name="user_apps_no_space">Penyimpanan tidak cukup: menyalin folder memerlukan %1$s</string> + <string name="user_apps_copying">Menyalin %1$s</string> + <string name="user_apps_failed">Gagal menambahkan aplikasi</string> + <string name="user_apps_github_checking">Mencari rilis %1$s</string> + <string name="user_apps_github_unreachable">Tidak dapat membaca rilis %1$s</string> + <string name="user_apps_github_no_appimage">Tidak ada rilis %1$s yang memiliki AppImage ARM64 atau x86_64</string> + <string name="user_apps_downloading">Mengunduh %1$s</string> + <string name="user_apps_download_failed">Unduhan gagal</string> + <string name="user_apps_update_failed">Gagal memperbarui aplikasi</string> + <string name="user_apps_icon_failed">Ikon tersebut tidak dapat dibaca</string> + <string name="user_apps_saved">%1$s disimpan</string> + <string name="user_apps_edit">Edit</string> + <string name="user_apps_check_updates">Periksa pembaruan</string> + <string name="user_apps_checking_updates">Memeriksa pembaruan…</string> + <string name="user_apps_update_to">Perbarui ke %1$s</string> + <string name="user_apps_up_to_date">Terbaru · %1$s</string> + <string name="user_apps_updated">%1$s diperbarui</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">Tambahkan aplikasi</string> + <string name="add_app_tab_script">Skrip</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">File .sh. Berjalan dari foldernya sendiri, layar penuh di bawah gamescope.</string> + <string name="add_app_appimage_hint">AppImage ARM64 (aarch64), atau AppImage x86_64 yang berjalan melalui FEX. Dibongkar ke dalam runtime Linux sekali saja.</string> + <string name="add_app_github_hint">Menginstal AppImage ARM64 dari rilis terbaru repositori, atau AppImage x86_64-nya melalui FEX.</string> + <string name="app_fex_title">Gunakan FEX</string> + <string name="app_fex_auto">Otomatis</string> + <string name="app_fex_on">Aktif</string> + <string name="app_fex_off">Nonaktif</string> + <string name="app_fex_hint">FEX menjalankan program Linux x86 di perangkat ARM64 ini. Otomatis menggunakannya saat program dibuat untuk x86.</string> + <string name="app_fex_x86">%1$s melalui FEX</string> + <string name="app_fex_not_ready">Program ini dibuat untuk PC %1$s dan berjalan melalui FEX, yang belum disiapkan. Buka Steam sekali saat masuk: DroidDeck akan mengunduh FEX dan Steam Linux Runtime yang diperlukan, lalu ini akan terbuka.</string> + <string name="session_end_program">Program ini</string> + <string name="session_end_fex_missing">%1$s dibuat untuk PC x86 dan berjalan melalui FEX, yang belum terinstal. Mulai Steam sekali saat masuk dan biarkan FEX serta Steam Linux Runtime diunduh, lalu mulai lagi.</string> + <string name="add_app_flatpak_hint">ID aplikasi Flathub atau halaman Flathub-nya. Flatpak disiapkan dahulu jika belum.</string> + <string name="add_app_file">File</string> + <string name="add_app_no_file">Belum ada file yang dipilih</string> + <string name="add_app_choose_file">Pilih file</string> + <string name="add_app_change_file">Ubah</string> + <string name="add_app_pick_script">Pilih skrip</string> + <string name="add_app_pick_appimage">Pilih AppImage</string> + <string name="add_app_pick_icon">Pilih ikon</string> + <string name="add_app_repository">Repositori</string> + <string name="add_app_repository_placeholder">owner/repo atau tautan github.com</string> + <string name="add_app_repository_invalid">Bukan repositori GitHub</string> + <string name="add_app_flatpak_id">ID aplikasi</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">Bukan ID aplikasi Flathub</string> + <string name="add_app_flatpak_bundle_hint">Atau file .flatpak, untuk aplikasi yang tidak ada di Flathub.</string> + <string name="add_app_pick_flatpak">Pilih file .flatpak</string> + <string name="add_app_source">Sumber</string> + <string name="add_app_appearance">Di Desktop Anda</string> + <string name="add_app_preview_name">Aplikasi baru</string> + <string name="add_app_name">Nama</string> + <string name="add_app_name_placeholder">Dari aplikasi</string> + <string name="add_app_icon">Ikon</string> + <string name="add_app_icon_default">Ikon default</string> + <string name="add_app_choose_icon">Pilih ikon</string> + <string name="add_app_clear_icon">Hapus</string> + <string name="add_app_icon_suggestions">Dari repositori</string> + <string name="add_app_icon_searching">Mencari ikon…</string> + <string name="add_app_icon_reset">Default</string> + <string name="edit_app_title">Edit %1$s</string> + <string name="edit_app_save">Simpan</string> + <string name="add_app_checking_folder">Memeriksa folder…</string> + <string name="add_app_folder_linked">Foldernya ditautkan ke dalam sesi: %1$s</string> + <string name="add_app_folder_copied">Foldernya berisi program Linux, yang tidak dapat dijalankan dari penyimpanan bersama, sehingga disalin ke dalam DroidDeck (%1$s).</string> + <string name="add_app_confirm">Tambah</string> + <string name="add_app_cancel">Batal</string> + <string name="add_app_runtime_required">Instal runtime Linux dari Penyiapan terlebih dahulu.</string> + <string name="add_app_busy">Tunggu %1$s selesai.</string> + <string name="steam_compat_label">Kompatibel</string> + <string name="proton_compatible_note">Build yang ditandai “%1$s” di menu Kompatibilitas Steam memiliki perbaikan prefix dari DroidDeck dan dapat menjalankan gim di sini. Defaultnya adalah Proton Experimental ARM64 (%1$s), atau Proton 11.0 ARM64 (%1$s) setelah build yang lebih baru itu terinstal. Entri Proton asli milik Valve tanpa tanda ini tidak dapat menjalankan gim di perangkat ini.</string> + <string name="proton_installed_compatible">Terinstal %1$s · %2$s</string> + <string name="common_cancel">Batal</string> + <string name="common_auto">Otomatis</string> + <string name="common_never">Tidak pernah</string> + <string name="common_always">Selalu</string> + <string name="common_uninstall">Hapus instalasi</string> + <string name="common_check">Periksa</string> + <string name="common_update">Perbarui</string> + <string name="common_checking">Memeriksa…</string> + <string name="common_working">Memproses…</string> + <string name="common_forget">Lupakan</string> + <string name="common_add_ellipsis">Tambah…</string> + <string name="common_use">Gunakan</string> + <string name="common_applies_next_session">Berlaku pada sesi berikutnya.</string> + <string name="mode_steam_title">Sesi Steam</string> + <string name="mode_desktop_title">Sesi Desktop</string> + <string name="mode_display">Tampilan</string> + <string name="mode_resolution">Resolusi</string> + <string name="mode_res_720">Hingga 720p</string> + <string name="mode_res_900">Hingga 900p</string> + <string name="mode_res_1080">Hingga 1080p</string> + <string name="mode_res_native">Asli</string> + <string name="mode_res_default">%1$s (default)</string> + <string name="mode_res_custom_value">Khusus · %1$d×%2$d</string> + <string name="mode_res_custom">Khusus…</string> + <string name="mode_res_note">720p dapat membuat menu lebih responsif.</string> + <string name="mode_ratio">Rasio layar</string> + <string name="mode_ratio_custom">Ditentukan oleh resolusi khusus.</string> + <string name="mode_ratio_auto">Otomatis menggunakan setidaknya 16:9.</string> + <string name="mode_fps">Batas frame rate</string> + <string name="mode_fps_note">Membatasi seluruh sesi. Tampilan beralih ke refresh rate yang habis dibagi oleh batas ini.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">Output HDR10</string> + <string name="mode_hdr_unavailable">Tidak tersedia: %1$s.</string> + <string name="mode_hdr_restart">Mulai ulang aplikasi untuk menerapkannya.</string> + <string name="mode_drivers">Driver</string> + <string name="mode_gpu_drivers">Driver GPU</string> + <string name="mode_gpu_drivers_hint">Digunakan bersama oleh Steam dan Desktop. Diatur di halaman Komponen.</string> + <string name="mode_touch_controls">Sentuhan & kontrol</string> + <string name="mode_touch">Sentuhan</string> + <string name="mode_touch_touchpad">Touchpad</string> + <string name="mode_touch_direct">Langsung</string> + <string name="mode_touch_note">Otomatis: touchpad di Desktop, langsung di Steam.</string> + <string name="mode_osc">Kontrol di layar</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">Otomatis menampilkan kontrol saat tidak ada pengontrol yang terhubung.</string> + <string name="osc_steam_touch">Sentuhan Steam (beta)</string> + <string name="osc_style_next_session">Kontrol sentuh Steam dimulai pada sesi Steam berikutnya</string> + <string name="osc_steam_layout">Tata letak sentuh Steam</string> + <string name="osc_steam_layout_hint">Pindahkan, ubah ukuran, atau sembunyikan kontrol sentuh gim ini; disimpan ke Steam</string> + <string name="mode_controller">Pengontrol</string> + <string name="mode_controller_hint">Cara Steam mendeteksi pengontrol Anda. Berlaku pada sesi berikutnya.</string> + <string name="mode_controller_deck">Pengontrol Steam Deck</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Pengontrol Xbox 360</string> + <string name="mode_controller_note">Deck: tombol Quick Access dan giroskop. Steam Controller: sama, dengan grip sense pada tombol belakang. DualShock 4 dan DualSense: giroskop, dan trackpad sebagai touchpad-nya. Xbox 360: buka Quick Access dengan Guide+A.</string> + <string name="mode_back">Kembali</string> + <string name="back_menu_then_qam">Menu → QAM</string> + <string name="back_qam_then_menu">QAM → Menu</string> + <string name="mode_session">Sesi</string> + <string name="mode_suspend">Perilaku latar belakang</string> + <string name="mode_suspend_hint">Saat aplikasi disembunyikan atau layar mati.</string> + <string name="mode_suspend_manual">Manual</string> + <string name="mode_suspend_native">Native</string> + <string name="mode_suspend_steam_note">Otomatis menjeda dan melanjutkan saat ditampilkan. Native menyiapkan Steam untuk tidur terlebih dahulu. Manual menunggu Lanjutkan. Tidak pernah terus berjalan.</string> + <string name="mode_background_downloads">Pertahankan unduhan tetap berjalan</string> + <string name="mode_background_downloads_hint">Menunda penjedaan latar belakang saat Steam sedang aktif mengunduh.</string> + <string name="mode_suspend_note">Otomatis menjeda di latar belakang dan melanjutkan saat ditampilkan. Manual menunggu Lanjutkan. Tidak pernah terus berjalan.</string> + <string name="mode_startup">Mulai</string> + <string name="mode_steam_startup">Mulai Steam bersama DroidDeck</string> + <string name="mode_steam_startup_hint">Membuka sesi Steam saat DroidDeck diluncurkan.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">Memeriksa rilis…</string> + <string name="mode_decky_ready">Siap menginstal %1$s.</string> + <string name="mode_decky_none">Tidak ditemukan build yang kompatibel. Coba lagi nanti.</string> + <string name="mode_decky_installed">Terinstal · %1$s</string> + <string name="mode_decky_update">Pembaruan tersedia · %1$s</string> + <string name="mode_decky_current">Terbaru · %1$s</string> + <string name="mode_decky_loader">Loader</string> + <string name="mode_decky_install_latest">Instal versi terbaru</string> + <string name="mode_decky_on">Dimulai bersama Steam. Saat aktif, aplikasi lain di perangkat ini mungkin dapat mengendalikan Steam.</string> + <string name="mode_decky_off">Nonaktif. Port debugging jarak jauh Steam tetap tertutup.</string> + <string name="mode_decky_remove_title">Hapus instalasi Decky Loader?</string> + <string name="mode_decky_remove_text">Menghapus loader. Plugin dan setelan Anda tetap disimpan.</string> + <string name="mode_client">Klien</string> + <string name="mode_mangoapp">Overlay performa</string> + <string name="mode_mangoapp_hint">Nonaktifkan jika gim crash atau layar menjadi hitam di mode Deck. Berlaku pada sesi berikutnya.</string> + <string name="mode_steam_repair">Perbaiki klien Steam</string> + <string name="mode_steam_repair_hint">Mengunduh ulang klien Steam saat Steam dimulai lagi. Gunakan ini jika Steam tetap berada di layar pemuatannya. Gim, status masuk, dan setelan tetap disimpan.</string> + <string name="mode_steam_repair_queued">Klien Steam akan diunduh ulang saat Steam dimulai lagi.</string> + <string name="mode_steam_repair_button">Perbaiki</string> + <string name="mode_added_art">Karya seni</string> + <string name="mode_sgdb_key">Kunci API SteamGridDB</string> + <string name="mode_sgdb_key_yours">Kunci Anda</string> + <string name="mode_sgdb_edit">Edit…</string> + <string name="mode_added_art_hint">Melengkapi karya seni yang belum ada dari Steam, lalu SteamGridDB. File cover.jpg, hero.jpg, logo.png, atau icon.png Anda sendiri di folder gim lebih diutamakan.</string> + <string name="summary_remove_game">Hapus %1$s</string> + <string name="mode_added_remove_confirm">Hapus?</string> + <string name="games_add">Tambah gim</string> + <string name="games_add_title">Tambahkan gim:</string> + <string name="games_pick_exe_title">Pilih exe:</string> + <string name="games_add_unreachable">Lokasi ini tidak tersedia dalam sesi</string> + <string name="mode_added_exe_check">Periksa exe ini</string> + <string name="wincomp_title">Komponen Windows</string> + <string name="wincomp_pad_hint">LB / RB berpindah antar bagian.</string> + <string name="wincomp_card_none">Tidak ada</string> + <string name="wincomp_applies">DLL Windows yang tidak disertakan Proton, dari daftar komponen. Aktifkan yang diperlukan gim ini: DLL tersebut masuk ke folder Windows milik gim sendiri, bukan ke file gimnya, dan tetap ada pada Proton apa pun yang menjalankannya.</string> + <string name="wincomp_offline">Tidak dapat menjangkau daftar komponen. Komponen yang sudah diunduh masih dapat dialihkan.</string> + <string name="wincomp_recommended">Direkomendasikan untuk gim ini</string> + <string name="wincomp_checking">Memeriksa kebutuhan gim ini…</string> + <string name="wincomp_when_online">Tersedia saat online</string> + <plurals name="wincomp_turning_on"> + <item quantity="other">Mengaktifkan %1$d komponen…</item> + </plurals> + <string name="wincomp_auto_section">Aktif untuk gim ini</string> + <string name="wincomp_auto_steam">Steam menginstal %1$s dengan %2$s (app %3$s)</string> + <string name="wincomp_auto_gog">Dari daftar GOG untuk gim ini</string> + <string name="wincomp_auto_epic">Dari prasyarat Epic untuk gim ini</string> + <string name="wincomp_auto_amazon">Dari langkah instalasi Amazon untuk gim ini</string> + <string name="wincomp_steam_by_name">Steam menginstal %1$s dengan app %2$s, dicocokkan berdasarkan nama</string> + <string name="wincomp_suggestions">Juga tercantum</string> + <string name="wincomp_suggestion_reason">Tercantum oleh %1$s; bukan komponen di sini</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">Gim ini menyertakan %1$s</string> + <string name="wincomp_found_shipped">Gim ini memiliki %1$s (sering berfungsi tanpa perubahan)</string> + <string name="wincomp_already_here">Sudah ada di gim ini</string> + <string name="wincomp_no_recommendation">Tidak ada yang di folder gim mengarah ke komponen.</string> + <string name="wincomp_active">Terinstal untuk gim ini (%1$d)</string> + <string name="wincomp_all">Semua komponen (%1$d)</string> + <string name="wincomp_installers">Memerlukan installer Windows (%1$d)</string> + <string name="wincomp_installers_note">Ini adalah program penyiapan sendiri tanpa paket di dalamnya yang dapat dibongkar, dan belum ada catatan tentang apa yang diinstalnya. Kebanyakan adalah runtime lama yang sudah digantikan oleh Mono di dalam Proton; catatan ditambahkan seiring dibuatnya.</string> + <string name="wincomp_show">Tampilkan</string> + <string name="wincomp_hide">Sembunyikan</string> + <string name="wincomp_needs_installer">Menjalankan program penyiapan sendiri (belum tercatat)</string> + <string name="wincomp_unsupported">Belum didukung di sini</string> + <string name="wincomp_loading">Memuat komponen…</string> + <string name="wincomp_failed">Tidak dapat menginstal: %1$s</string> + <string name="wincomp_downloaded">Terunduh</string> + <string name="wincomp_downloading">Mengunduh %1$s</string> + <string name="wincomp_installing">Menginstal: %1$s</string> + <string name="wincomp_reverting">Menonaktifkan…</string> + <string name="wincomp_download_size">Unduhan %1$s</string> + <string name="wincomp_next_launch">Berlaku saat gim dimulai lagi.</string> + <string name="mode_stretch169">Rentangkan gim 16:9</string> + <string name="mode_stretch169_hint">Pada layar yang lebih persegi dari 16:9, merentangkan gim agar memenuhi layar, bukan menampilkan batang hitam. Berlaku pada sesi berikutnya.</string> + <string name="mode_audio">Audio</string> + <string name="mode_directaudio">DirectAudio untuk gim</string> + <string name="mode_directaudio_hint">Melewati PulseAudio untuk latensi yang lebih rendah.</string> + <string name="mode_mic">Mikrofon</string> + <string name="mode_mic_hint">Untuk obrolan suara.</string> + <string name="mode_network">Jaringan</string> + <string name="mode_wifi_discovery">Pencarian Wi-Fi</string> + <string name="mode_wifi_discovery_hint">Menampilkan nama Wi-Fi dan jaringan di sekitar di Steam.</string> + <string name="mode_wifi_location_off">Layanan lokasi tidak aktif.</string> + <string name="mode_wifi_permission_denied">Izin lokasi tidak diberikan.</string> + <string name="mode_wifi_open_settings">Buka setelan</string> + <string name="mode_wifi_continue">Lanjutkan</string> + <string name="mode_wifi_explain_title">Aktifkan pencarian Wi-Fi?</string> + <string name="mode_wifi_explain_text">Android memerlukan izin Lokasi untuk membaca nama Wi-Fi dan menemukan jaringan di sekitar. DroidDeck menggunakan akses ini untuk menampilkan jaringan tersebut di Steam. Kami tidak melacak lokasi Anda. Akses internet tetap berfungsi tanpa izin ini.</string> + <string name="mode_storage">Penyimpanan gim</string> + <string name="mode_storage_auto">Otomatis - kartu SD jika ada, ditambah folder Games Anda</string> + <string name="mode_storage_auto_none">Otomatis - folder Games Anda, ditambah kartu SD jika ada (tidak ada sekarang)</string> + <string name="mode_storage_internal">Hanya internal</string> + <string name="mode_storage_folder">Folder: %1$s</string> + <string name="mode_second_library">Pustaka kedua</string> + <string name="mode_storage_note">Gim yang melakukan streaming aset dari SD atau penyimpanan bersama dapat tersendat.</string> + <string name="mode_storage_diagnostics">Diagnostik penyimpanan</string> + <string name="mode_storage_diagnostics_hint">Mencatat hasil alokasi dan waktu penyimpanan yang disampel di Bagikan log. Dapat memperlambat instalasi saat diaktifkan. Berlaku pada sesi berikutnya.</string> + <string name="mode_choose_folder">Pilih folder…</string> + <string name="mode_custom_res_title">Resolusi khusus</string> + <string name="mode_custom_res_text">Menggantikan batas dan rasio. Ukuran yang tidak sesuai dengan layar akan menampilkan batang.</string> + <string name="mode_width">Lebar</string> + <string name="mode_height">Tinggi</string> + <string name="mode_custom_res_range">Antara 320×240 dan 3840×2160.</string> + <plurals name="mode_added_count"> + <item quantity="other">%1$d gim</item> + </plurals> + <string name="common_ok">OK</string> + <string name="common_delete">Hapus</string> + <string name="common_restore">Pulihkan</string> + <string name="common_download">Unduh</string> + <string name="common_delete_named">Hapus %1$s</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Driver runtime</string> + <string name="comp_runtime_driver_hint">Digunakan oleh Steam, gim, dan desktop. Berlaku pada sesi berikutnya.</string> + <string name="comp_import_turnip">Impor zip Turnip…</string> + <string name="comp_display_driver">Driver tampilan</string> + <string name="comp_display_driver_hint">Menggambar frame ke layar di kedua mode. Mulai ulang aplikasi untuk menerapkannya.</string> + <string name="comp_import_adrenotools">Impor zip AdrenoTools…</string> + <string name="comp_never">tidak pernah</string> + <string name="comp_nightlies_checked">Nightlies diperiksa %1$s</string> + <string name="comp_title">Komponen</string> + <string name="comp_about">Tentang Komponen</string> + <string name="comp_check_drivers">Periksa driver baru</string> + <string name="comp_check_nightlies">Periksa Nightlies untuk paket baru</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">Proton yang dipilih di sini menjalankan setiap gim yang belum memilih Proton sendiri di Steam, dan perubahan yang dibuat di Steam juga ditampilkan di sini. Steam menggantikan Proton milik Valve saat memperbarui; aslinya tetap disimpan per build.</string> + <string name="comp_scope">Konfigurasi</string> + <string name="comp_profile_configure">Konfigurasikan profil</string> + <string name="comp_profile">Profil</string> + <string name="comp_scope_general">Profil default</string> + <string name="comp_scope_general_detail">Digunakan secara otomatis oleh gim tanpa penggantian.</string> + <string name="comp_profile_game_detail">Setelan tersimpan yang digunakan secara otomatis saat %1$s dijalankan.</string> + <string name="comp_game_hint">Pilih hanya setelan yang diperlukan gim ini. Semua yang lain tetap menggunakan profil Default.</string> + <string name="comp_game_fex">Preset FEX</string> + <string name="comp_game_fex_inherited">Menggunakan preset profil Default.</string> + <string name="comp_game_fex_override">Preset khusus tersimpan untuk gim ini.</string> + <string name="comp_game_fex_custom">Variabel FEX khusus</string> + <string name="comp_game_use_general">Gunakan Default</string> + <string name="comp_game_compatibility">Kompatibilitas gim</string> + <string name="comp_game_environment_hint">Opsi lingkungan Proton, DXVK, VKD3D, dan sinkronisasi untuk gim ini.</string> + <string name="comp_game_proton_inherited">Menggunakan Proton profil Default.</string> + <string name="comp_game_proton_override">Proton khusus tersimpan untuk gim ini melalui pemetaan kompatibilitas Steam.</string> + <string name="comp_no_proton_short">Tanpa Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Menggunakan paket profil Default dari %1$s kecuali gim ini memilih paket lain.</string> + <string name="comp_game_component_general">Menggunakan paket komponen profil Default.</string> + <string name="comp_game_component_override">Menggunakan %1$s hanya untuk gim ini.</string> + <string name="comp_game_runtime_options">Opsi runtime</string> + <string name="comp_game_environment_other_hint">Opsi lingkungan Proton, DXVK, VKD3D-Proton, dan sinkronisasi lain per gim.</string> + <string name="comp_game_saved_next">Profil %1$s tersimpan untuk peluncuran gim berikutnya.</string> + <string name="game_no_steam_title">Mulai tanpa Steam</string> + <string name="game_no_steam_detail">Untuk game yang langsung tertutup: menjalankannya tanpa pengaturan Steam dari Proton. Fitur Steam seperti overlay dimatikan untuknya.</string> + <string name="game_no_steam_on">Aktif</string> + <string name="comp_game_proton_live">Pilihan Proton %1$s tersimpan dan sudah tersedia di Steam sekarang.</string> + <string name="comp_game_proton_next">Pilihan Proton %1$s tersimpan. Mulai ulang Steam sebelum meluncurkan gim.</string> + <string name="comp_game_proton_not_runnable">%1$s tidak dapat menggunakan Proton yang dipilih di perangkat ini.</string> + <string name="comp_game_load_failed">Tidak dapat memuat profil %1$s: %2$s</string> + <string name="comp_game_save_failed">Profil gim tidak dapat disimpan: %1$s</string> + <string name="comp_game_reset">Atur ulang profil gim</string> + <string name="comp_game_reset_title">Atur ulang profil %1$s?</string> + <string name="comp_game_reset_body">Hapus Proton, FEX, paket komponen, lingkungan, pemfilteran tekstur, dan penggantian komponen Windows gim ini? Gim ini akan menggunakan profil Default.</string> + <string name="comp_game_reset_done">%1$s sekarang menggunakan profil Default.</string> + <string name="comp_game_reset_failed">Profil gim tidak dapat diatur ulang secara lengkap: %1$s</string> + <string name="comp_default_live">%1$s sekarang menjalankan setiap gim yang belum memilih Proton sendiri di Steam.</string> + <string name="comp_default_next">%1$s akan menjalankan setiap gim yang belum memilih Proton sendiri, mulai dari saat Steam berikutnya dijalankan.</string> + <string name="comp_default_not_runnable">%1$s tidak dapat memulai gim di sini, sehingga tidak dapat menjadi default.</string> + <string name="comp_default_failed">Proton default tidak dapat disimpan: %1$s</string> + <string name="comp_swapped">%1$s · %2$d ditukar</string> + <string name="comp_all_original">%1$s · semua asli</string> + <string name="comp_game_running_suffix">%1$s · gim berjalan</string> + <string name="comp_esync_pack">%1$s · paket droiddeck-esync</string> + <string name="comp_esync_wanted">%1$s · belum ada paket droiddeck-esync</string> + <string name="comp_esync_builtin">%1$s · esync sudah terpasang, tidak perlu paket</string> + <string name="comp_gpu_drivers">Driver GPU</string> + <string name="comp_reapplied">%1$s · diterapkan ulang saat peluncuran %2$s</string> + <string name="comp_next_queued">Berikutnya: %1$s · setelah gim ditutup</string> + <string name="comp_in_use">%1$s digunakan: %2$s</string> + <string name="comp_waits">Menunggu sampai gim ditutup</string> + <string name="comp_applies_next_game">Berlaku saat gim berikutnya dimulai</string> + <string name="comp_import_zip">Impor .zip</string> + <string name="comp_reading">Membaca Proton…</string> + <string name="comp_no_proton">Belum ada Proton yang terinstal. Mulai Steam sekali untuk mengunduh Proton ARM64-nya, atau tambahkan GE-Proton, CachyOS, atau DroidDeck-Proton dari Penyiapan.</string> + <string name="comp_original">Asli · %1$s</string> + <string name="comp_this_build">%1$s · build ini</string> + <string name="comp_earlier_build">%1$s · build sebelumnya</string> + <string name="comp_installed_count">Terinstal · %1$d</string> + <string name="comp_nothing_stored">Belum ada yang tersimpan untuk %1$s. File asli Proton ini disimpan sebagai aslinya.</string> + <string name="comp_waits_long">Sebuah gim sedang berjalan pada Proton ini, sehingga ini menunggu sampai gim ditutup.</string> + <string name="comp_applies_long">Berlaku saat gim berikutnya dimulai.</string> + <string name="comp_swap_title">Tukar %1$s?</string> + <string name="comp_swap_body">Masukkan %1$s ke %2$s? File aslinya tetap tersimpan. %3$s</string> + <string name="comp_restore_title">Pulihkan %1$s?</string> + <string name="comp_restore_body">Kembalikan %1$s asli ke %2$s? %3$s</string> + <string name="comp_delete_package_body">Menghapus paket dari aplikasi. Anda dapat mengunduhnya lagi.</string> + <string name="comp_delete_original_title">Hapus yang asli ini?</string> + <string name="comp_delete_original_body">%1$s asli ini berasal dari build %2$s yang lebih lama. Asli build saat ini selalu dipertahankan.</string> + <string name="comp_nightlies_count">Di Nightlies · %1$d</string> + <string name="comp_refresh_to_list">Segarkan untuk melihat daftar paket di Nightlies.</string> + <string name="comp_nothing_new">Tidak ada yang baru sejak pemeriksaan terakhir.</string> + <string name="comp_import_wcp">Impor .wcp</string> + <string name="comp_restore_original">Pulihkan yang asli dari Proton ini</string> + <string name="comp_restore_original_body">Kembalikan %1$s asli %2$s?</string> + <string name="comp_swap">Tukar</string> + <string name="comp_about_text">FEX, DXVK, dan VKD3D-Proton untuk setiap Proton yang digunakan Steam menjalankan gim.\n\nPilih baris yang terinstal untuk menukarnya. Berlaku saat gim berikutnya dimulai, atau setelah gim yang sedang berjalan ditutup.\n\nPilihan Anda diperiksa ulang sebelum setiap peluncuran dan dikembalikan jika ada yang mengubahnya. File asli setiap Proton tetap disimpan, sehingga pembaruan Steam tidak akan pernah menghilangkannya.</string> + <string name="comp_tag_original">Asli</string> + <string name="comp_tag_in_use">Digunakan</string> + <string name="comp_tag_new">Baru</string> + <string name="comp_tag_stored">Tersimpan</string> + <string name="perf_title">Performa</string> + <string name="perf_lede">Core CPU, perbaikan sesi, dan nama host. Berlaku pada sesi berikutnya.</string> + <string name="perf_client_cores_group">Core klien Steam</string> + <string name="perf_override">Timpa pilihan core Steam</string> + <string name="perf_override_hint">Mengikat Steam, pembantu UI-nya, dan gamescope ke core di bawah.</string> + <string name="perf_client_cores">Core klien</string> + <string name="perf_client_cores_on">Core untuk Steam.</string> + <string name="perf_client_cores_off">Aktifkan penggantian untuk memilih.</string> + <string name="perf_game_cores_group">Core gim</string> + <string name="perf_game_cores">Core gim</string> + <string name="perf_game_cores_hint">Core untuk gim. Dengan semua dipilih, Android menjadwalkan secara bebas.</string> + <string name="perf_client_ui">Antarmuka klien</string> + <string name="perf_glthread">GL berulir (threaded)</string> + <string name="perf_glthread_hint">Dapat membuat menu Steam lebih responsif.</string> + <string name="perf_zink">Zink: deskriptor lazy</string> + <string name="perf_zink_hint">Direkomendasikan untuk driver tanpa descriptor buffer.</string> + <string name="perf_noglerror">Lewati pemeriksaan error GL</string> + <string name="perf_noglerror_hint">Menonaktifkan validasi GL per panggilan.</string> + <string name="perf_gsrealtime">gamescope: antrean GPU realtime</string> + <string name="perf_gsrealtime_hint">Memprioritaskan kerja GPU compositor. Dapat menghaluskan pacing frame dengan sedikit mengorbankan gim.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">Pertahankan GPU pada clock tertingginya</string> + <string name="perf_gpuclock_hint">Hanya Adreno. Lebih sedikit tersendat, lebih banyak penggunaan baterai dan panas.</string> + <string name="perf_fixes">Perbaikan sesi</string> + <string name="perf_sysmem">Turnip: rendering sysmem</string> + <string name="perf_sysmem_hint">Lebih cepat untuk Steam dan sebagian besar gim. Diperlukan pada Adreno 710/720/722.</string> + <string name="perf_xalia">Lewati bantuan xalia milik Steam</string> + <string name="perf_fastsync">droiddeck-ntsync (eksperimental)</string> + <string name="perf_fastsync_hint">Pengganti userspace untuk driver ntsync Linux, untuk pengujian. Saat aktif, Proton menggunakannya sebagai pengganti droiddeck-esync dan droiddeck-fsync. Berfungsi dengan Proton Experimental ARM64, GE-Proton, dan proton-cachyos. Berlaku pada sesi berikutnya.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync lebih dulu</string> + <string name="perf_fsync_hint">Menjalankan fsync milik Proton sendiri pada file Proton standar, dengan futex_waitv yang disediakan di userspace, sehingga tidak memerlukan paket dan tetap berfungsi begitu Steam memperbarui Proton. Saat aktif, gim menggunakannya sebelum droiddeck-esync, kecuali beberapa gim yang oleh Proton dijaga tetap di luar fsync. Berlaku pada sesi berikutnya.</string> + <string name="perf_esync_hint">Thread Windows milik gim saling menunggu melalui eventfd dan memori bersama, bukan melalui bolak-balik ke wineserver. Menggunakan paket droiddeck-esync yang dibuat khusus untuk build Proton tersebut; paket diambil secara otomatis. Sampai paket untuk build Proton baru tersedia, droiddeck-fsync menjadi penggantinya. Berlaku pada sesi berikutnya.</string> + <string name="perf_xalia_hint">Menonaktifkan bantuan navigasi gamepad milik Proton, yang menggunakan CPU di setiap gim.</string> + <string name="perf_seccomp">Jalankan proot tanpa seccomp</string> + <string name="perf_seccomp_hint">Dapat memperbaiki galat syscall yang hilang pada beberapa kernel. Dapat mengurangi performa.</string> + <string name="perf_fastpath">Jalur cepat proot</string> + <string name="perf_fastpath_needs_seccomp">Memerlukan filter seccomp milik proot.</string> + <string name="perf_fastpath_hint">Pencarian file lebih cepat dan lebih sedikit tersendat. Nonaktifkan jika program tidak dapat menemukan filenya.</string> + <string name="perf_identity">Identitas sesi</string> + <string name="perf_hostname">Nama host</string> + <string name="perf_hostname_hint">Nama yang dilihat Steam untuk perangkat ini. Kosongkan untuk memulihkan %1$s.</string> + <string name="perf_hostname_invalid">Hanya huruf, angka, dan tanda hubung di tengah, hingga 63 karakter.</string> + <string name="perf_phantom_warning">Android diatur untuk menghentikan sesi ini</string> + <string name="driver_installed">Terinstal</string> + <string name="driver_available">Tersedia untuk diunduh</string> + <string name="driver_nothing_new">Tidak ada yang baru sejak pemeriksaan terakhir.</string> + <string name="driver_restore_builtin">Pulihkan driver bawaan</string> + <string name="driver_delete_builtin">Bawaan aplikasi: ini menghapus file yang telah dibongkar dan menyembunyikannya. Pulihkan driver bawaan akan mengembalikannya.</string> + <string name="driver_delete_bundle">Separuh dari paket Android + Linux: kedua separuhnya dihapus. Driver yang sedang digunakan akan beralih kembali ke default.</string> + <string name="driver_delete_plain">Filenya dihapus dari aplikasi.</string> + <string name="driver_delete_in_use">Filenya dihapus. Driver ini sedang digunakan, jadi default akan mengambil alih.</string> + <string name="drawer_page_display">Tampilan</string> + <string name="drawer_page_effects">Efek</string> + <string name="drawer_page_controls">Kontrol</string> + <string name="drawer_page_components">Komponen</string> + <string name="drawer_page_controller">Pengontrol</string> + <string name="drawer_page_settings">Setelan</string> + <string name="drawer_page_desc">Halaman %1$s</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Desktop</string> + <string name="drawer_steam_menu">Menu Steam</string> + <string name="drawer_quick_access">Akses cepat</string> + <string name="drawer_open_qam">Buka Menu Akses Cepat</string> + <string name="drawer_prev_page">Halaman sebelumnya</string> + <string name="drawer_next_page">Halaman berikutnya</string> + <string name="drawer_hud">HUD performa</string> + <string name="drawer_scaling">Mode penskalaan</string> + <string name="drawer_scaling_sharpness">Ketajaman penskalaan</string> + <string name="drawer_effects">Efek layar</string> + <string name="drawer_effects_hint">Langsung. Menggunakan sedikit waktu GPU saat ada efek yang aktif.</string> + <string name="drawer_look">Preset</string> + <string name="drawer_look_custom">Khusus</string> + <string name="drawer_effects_advanced">Lanjutan</string> + <string name="widgets_expanded">Diperluas</string> + <string name="widgets_collapsed">Dilipat</string> + <string name="drawer_cas">Penajaman (CAS)</string> + <string name="drawer_cas_level">Tingkat penajaman</string> + <string name="drawer_fake_hdr">HDR palsu</string> + <string name="drawer_deband">Debanding</string> + <string name="drawer_deband_strength">Kekuatan debanding</string> + <string name="drawer_brightness">Kecerahan</string> + <string name="drawer_contrast">Kontras</string> + <string name="drawer_gamma">Gamma</string> + <string name="drawer_saturation">Saturasi</string> + <string name="drawer_fxaa">FXAA (penghalusan tepi)</string> + <string name="drawer_toon">Garis tepi toon</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Pemfilteran tekstur</string> + <string name="drawer_texture_hint">Gim DirectX 9–11, mulai dari peluncuran berikutnya.</string> + <string name="drawer_anisotropy">Pemfilteran anisotropik</string> + <string name="drawer_texture_sharpness">Ketajaman tekstur</string> + <string name="drawer_texture_sharpness_note">Bias mip negatif: tekstur lebih tajam, sedikit kilauan. Otomatis menyesuaikan seberapa besar sesi diperbesar ke layar.</string> + <string name="drawer_touch_auto">Otomatis (%1$s)</string> + <string name="drawer_keyboard">Keyboard</string> + <string name="drawer_pc_keyboard">Keyboard PC</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">Layar kedua</string> + <string name="drawer_second_screen_mode">Mode</string> + <string name="drawer_second_screen_shows">Menampilkan</string> + <string name="drawer_android_apps">Aplikasi Android</string> + <string name="drawer_launch_app">Luncurkan aplikasi</string> + <string name="common_hide">Sembunyikan</string> + <string name="common_show">Tampilkan</string> + <string name="drawer_no_apps">Tidak ada aplikasi yang dapat diluncurkan</string> + <string name="drawer_session_behavior">Perilaku sesi</string> + <string name="drawer_suspend_hint">Berlaku sekarang dan untuk sesi mendatang dalam mode ini.</string> + <string name="drawer_next_session">Sesi berikutnya</string> + <string name="drawer_support">Dukungan</string> + <string name="drawer_logs">Log sesi</string> + <string name="drawer_logs_hint">Lampirkan ke laporan bug</string> + <string name="drawer_share_logs">Bagikan log</string> + <string name="drawer_background">Latar belakang</string> + <string name="drawer_stop">Hentikan</string> + <string name="drawer_stop_title">Hentikan sesi?</string> + <string name="drawer_running_now">Sedang berjalan</string> + <string name="drawer_no_game">Tidak ada gim yang berjalan pada Proton</string> + <string name="drawer_selected_profile">Profil gim</string> + <string name="drawer_selected_profile_none">Detail profil belum tersedia.</string> + <string name="drawer_profile_refresh_failed_initial">Tidak dapat memuat profil gim. Mencoba lagi secara otomatis.</string> + <string name="drawer_profile_refresh_failed_stale">Tidak dapat menyegarkan profil gim. Menampilkan data terakhir yang tersedia; mencoba lagi secara otomatis.</string> + <string name="drawer_profile_inspect_hint">Lihat dan edit setelan tersimpan yang digunakan pada peluncuran berikutnya.</string> + <string name="drawer_profile_default">Menggunakan profil Default</string> + <string name="drawer_profile_custom">Profil khusus</string> + <string name="drawer_component_inherited">Menggunakan %1$s dari profil Default.</string> + <string name="drawer_game_components_note">Perubahan disimpan ke profil %1$s dan berlaku saat gim diluncurkan berikutnya.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="other">%1$d penggantian gim</item> + </plurals> + <string name="drawer_no_proton">Belum ada Proton yang terinstal.</string> + <string name="drawer_game_running">gim berjalan</string> + <string name="drawer_reapplied">diterapkan ulang pada peluncuran %1$s</string> + <string name="drawer_original">Asli</string> + <string name="drawer_components_note">Impor dan hapus di halaman Komponen.</string> + <string name="setup_check_done">Selesai</string> + <string name="setup_check_warn">Perlu perhatian</string> + <string name="setup_check_busy">Memproses</string> + <string name="setup_limit_adb_failed">Periksa alamat IP dan port Debugging nirkabel, atau sandingkan lagi jika Android telah menghapus perangkat ini.</string> + <string name="setup_install">Instal</string> + <string name="setup_manage">Kelola</string> + <string name="setup_tab_overview">Ringkasan</string> + <string name="setup_tab_controller">Pengontrol</string> + <string name="setup_tab_session">Sesi</string> + <string name="setup_tab_launcher">Peluncur</string> + <string name="setup_title">Penyiapan</string> + <string name="setup_all_set">● Semua siap</string> + <string name="setup_n_ready">%1$d dari %2$d siap</string> + <string name="setup_system_check">Pemeriksaan sistem</string> + <string name="setup_gpu_supported">Perangkat didukung</string> + <string name="setup_gpu_untested">GPU belum diuji</string> + <string name="setup_gpu_unsupported">GPU tidak didukung</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">Steam memerlukan driver Adreno; %1$s mungkin menampilkan layar hitam</string> + <string name="setup_runtime">Runtime Linux</string> + <string name="setup_runtime_progress">%1$s · %2$d%%</string> + <string name="setup_runtime_missing">Belum terinstal · sekitar 3 GB, terinstal saat Play pertama kali</string> + <string name="setup_installed">Terinstal</string> + <string name="setup_runtime_update">%1$s · pembaruan tersedia</string> + <string name="setup_runtime_current">%1$s · terbaru</string> + <string name="setup_limit">Batas proses turunan</string> + <string name="setup_limit_on">Aktif · Android dapat menutup Steam. Menonaktifkannya memerlukan waktu sekitar satu menit.</string> + <string name="setup_limit_unset">Belum diatur · Android dapat menutup Steam. Menonaktifkannya memerlukan waktu sekitar satu menit.</string> + <string name="setup_limit_unknown">Tidak dapat diperiksa · menonaktifkannya memerlukan waktu sekitar satu menit.</string> + <string name="setup_fix_it">Perbaiki</string> + <string name="setup_details">Detail</string> + <string name="setup_dev_options">Opsi developer</string> + <string name="setup_use_wireless">Gunakan Debugging nirkabel</string> + <string name="setup_check_again">Periksa lagi</string> + <string name="setup_limit_turn_on">Aktifkan batas</string> + <string name="setup_account">Akun Steam</string> + <string name="setup_signed_in_offline">Masuk sebagai %1$s · mode luring</string> + <string name="setup_signed_in">Masuk sebagai %1$s</string> + <string name="setup_sign_in">Tekan Play dan masuk ke Steam</string> + <string name="setup_tools">Alat</string> + <string name="setup_controller">Pengontrol</string> + <string name="setup_controller_unavailable">Setelan pengontrol tidak tersedia.</string> + <string name="setup_session">Sesi</string> + <string name="setup_logs">Log sesi</string> + <string name="setup_logs_hint">Disimpan setelah setiap sesi</string> + <string name="setup_agent_commands">Alat debug</string> + <string name="setup_latest_logs">Log sesi terbaru</string> + <string name="setup_saved_logs">Log sesi tersimpan</string> + <string name="setup_saved_logs_hint">Menyimpan %1$d terbaru</string> + <string name="setup_clear_logs">Hapus log</string> + <string name="setup_offline">Mode luring</string> + <string name="setup_sign_in_first">Masuk ke Steam terlebih dahulu</string> + <string name="setup_launcher">Peluncur</string> + <string name="setup_language">Bahasa</string> + <string name="setup_language_hint">Steam juga menggunakannya, mulai dari saat berikutnya dimulai.</string> + <string name="setup_language_system">Default sistem (%1$s)</string> + <string name="setup_language_system_short">Default sistem</string> + <string name="setup_app_scale">Skala aplikasi</string> + <string name="setup_app_scale_hint">Teks dan kontrol DroidDeck</string> + <string name="setup_app_scale_default">%1$d%% (Default)</string> + <string name="setup_theme">Tema</string> + <string name="setup_theme_hint">Tampilan peluncur</string> + <string name="setup_home">Gunakan sebagai Layar Utama</string> + <string name="setup_home_on">DroidDeck dapat menjadi aplikasi Utama</string> + <string name="setup_home_off">DroidDeck tidak pernah ditawarkan sebagai aplikasi Utama</string> + <string name="setup_orientation">Orientasi</string> + <string name="setup_orientation_auto">Lanskap otomatis</string> + <string name="setup_orientation_landscape">Lanskap</string> + <string name="setup_orientation_reverse">Lanskap terbalik</string> + <string name="setup_fullscreen">Layar penuh</string> + <string name="setup_fullscreen_on">Menyembunyikan bilah status dan navigasi Android</string> + <string name="setup_fullscreen_off">Menampilkan bilah status dan navigasi Android</string> + <string name="setup_default_home">Aplikasi Utama default</string> + <string name="setup_choose_home">Pilih aplikasi Utama</string> + <string name="setup_choose">Pilih</string> + <string name="setup_linux_apps">Aplikasi Linux (beta)</string> + <string name="setup_tool_files">File</string> + <string name="setup_tool_files_hint">Jelajahi dan kelola file</string> + <string name="setup_tool_protons">Versi Proton</string> + <string name="setup_tool_protons_hint">Instal build Proton ARM64</string> + <string name="setup_tool_performance">Performa</string> + <string name="setup_tool_performance_hint">Penugasan inti CPU</string> + <string name="setup_tool_roms">Folder ROM</string> + <string name="setup_tool_roms_hint">Tempat gim emulator disimpan</string> + <string name="setup_card_components">Komponen</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Kontrol</string> + <string name="setup_card_controls_hint">Pemetaan tombol</string> + <string name="store_tab_discover">Temukan</string> + <string name="store_tab_search">Cari</string> + <string name="store_tab_installed">Terinstal</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Toko</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak siap</string> + <string name="store_needs_runtime">Instal runtime Linux dari Penyiapan terlebih dahulu.</string> + <string name="store_setup_title">Siapkan Flatpak</string> + <string name="store_setup_text">Menambahkan Flatpak dan menghubungkannya ke Flathub (sekitar 20 MB). Aplikasi pertama juga mengunduh runtime-nya.</string> + <string name="store_setting_up">Menyiapkan…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Pembaruan</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d%%</string> + <string name="store_starting">Memulai…</string> + <string name="store_categories">Kategori</string> + <string name="store_unreachable">Tidak dapat menjangkau Flathub. Periksa koneksi Anda dan coba lagi.</string> + <string name="store_try_again">Coba lagi</string> + <string name="store_search_label">Cari di Flathub</string> + <string name="store_search_placeholder">Firefox, gim, emulator…</string> + <string name="store_search">Cari</string> + <string name="store_all">Semua</string> + <string name="store_searching">Mencari…</string> + <string name="store_search_hint">Cari berdasarkan nama atau pilih kategori. Hanya aplikasi ARM64 yang ditampilkan.</string> + <string name="store_no_results">Tidak ada aplikasi ARM64 di Flathub yang cocok.</string> + <string name="store_results">Hasil</string> + <string name="store_check_updates">Periksa pembaruan</string> + <string name="store_update_all">Perbarui semua (%1$d)</string> + <string name="store_all_current">● Semuanya sudah terbaru</string> + <string name="store_no_apps">Belum ada aplikasi. Temukan di tab Temukan atau Cari.</string> + <string name="store_setup_first_note">Siapkan Flatpak untuk menginstal aplikasi.</string> + <string name="store_update_available_prefix">Pembaruan tersedia · %1$s</string> + <string name="store_open">Buka</string> + <string name="store_details">Detail</string> + <string name="store_remove">Hapus</string> + <string name="store_press_again">Tekan lagi untuk menghapus</string> + <string name="store_installing">Menginstal…</string> + <string name="store_install">Instal</string> + <string name="store_setup_first">Siapkan Flatpak terlebih dahulu</string> + <string name="store_download_size">Unduhan %1$s</string> + <string name="store_installed_chip">● Terinstal</string> + <string name="store_details_failed">Tidak dapat memuat halaman Flathub aplikasi ini.</string> + <string name="store_loading">Memuat dari Flathub…</string> + <string name="store_no_arm">Tidak ada build ARM64 di Flathub, sehingga tidak dapat diinstal di sini.</string> + <string name="store_screenshots">Tangkapan layar</string> + <string name="store_about">Tentang</string> + <string name="store_app_id">ID aplikasi</string> + <string name="store_license">Lisensi</string> + <string name="store_runtime">Runtime</string> + <string name="store_installed_size">Ukuran terinstal</string> + <string name="store_installed_size_value">%1$s (ditambah runtime bersamanya)</string> + <string name="store_website">Situs web</string> + <string name="store_verified">Terverifikasi</string> + <string name="upd_title">Pembaruan</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Diperiksa %1$s</string> + <string name="upd_not_checked">Belum diperiksa</string> + <string name="upd_perm_title">Izinkan DroidDeck memperbarui dirinya sendiri</string> + <string name="upd_perm_text">Pada layar berikutnya, aktifkan Izinkan dari sumber ini, lalu kembali. Pembaruan akan berlanjut secara otomatis.</string> + <string name="upd_open_settings">Buka setelan</string> + <string name="upd_not_now">Nanti saja</string> + <string name="upd_signed_status">Ditandatangani berbeda</string> + <string name="upd_signed_headline">Tidak dapat memperbarui di tempat</string> + <string name="upd_signed_detail">Salinan ini tidak ditandatangani dengan kunci rilis DroidDeck, sehingga pembaruan tidak dapat diinstal menimpanya. Hapus instalasinya untuk beralih.</string> + <string name="upd_available">Pembaruan tersedia</string> + <string name="upd_blocked_status">Tidak dapat diinstal</string> + <string name="upd_switch">Siap untuk beralih</string> + <string name="upd_ahead_status">Lebih baru dari Stable</string> + <string name="upd_ahead_headline">Anda lebih baru dari Stable</string> + <string name="upd_ahead_detail">Build ini lebih baru daripada Stable terbaru. Anda akan beralih ke Stable pada rilis berikutnya.</string> + <string name="upd_gone_status">Pengujian berakhir</string> + <string name="upd_gone_headline">Pengujian ini telah berakhir</string> + <string name="upd_gone_detail">Perbaikannya sudah digabungkan atau dibatalkan. Ikuti Preview untuk perbaikan terbaru.</string> + <string name="upd_current_status">Terbaru</string> + <string name="upd_current_headline">Anda memiliki %1$s terbaru</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Perbarui%1$s</string> + <string name="upd_install_button">Instal%1$s</string> + <string name="upd_install_anyway">Tetap instal Stable %1$s</string> + <string name="upd_follow_preview">Ikuti Preview</string> + <string name="upd_stop_session">Hentikan sesi yang berjalan untuk memperbarui.</string> + <string name="upd_less">Lebih sedikit</string> + <string name="upd_more">Lebih banyak</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Build Preview baru</string> + <string name="upd_new_test">Build pengujian PR #%1$d</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">SALURAN PEMBARUAN</string> + <string name="upd_stable">Stable</string> + <string name="upd_stable_hint">Rilis yang telah diuji, untuk kebanyakan orang</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Preview</string> + <string name="upd_preview_hint">Perbaikan terbaru, sebelum Stable</string> + <string name="upd_preview_current">Terbaru</string> + <string name="upd_tests">Build pengujian</string> + <string name="upd_tests_hint">Coba perbaikan sebelum dirilis</string> + <string name="upd_tests_none">Tidak ada saat ini</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Tidak dibuat untuk salinan DroidDeck ini</string> + <string name="upd_updated">Diperbarui %1$s</string> + <string name="upd_test_name">pengujian PR #%1$d</string> + <string name="upd_while_ago">beberapa waktu lalu</string> + <string name="upd_just_now">baru saja</string> + <plurals name="upd_tests_count"> + <item quantity="other">%1$d untuk dicoba</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="other">Perubahan sejak build Anda · %1$d build di belakang</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="other">Perubahan terbaru · %1$d build</item> + </plurals> + <string name="gate_title">Satu setelan Android yang harus diubah</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Android menghentikan aplikasi yang memulai banyak proses latar belakang, dan Steam memulai puluhan. Sampai ini berubah, gim akan tertutup tanpa pesan kesalahan.</string> + <string name="gate_fix_for_me">Perbaiki untuk saya</string> + <string name="gate_toggle_steps">Aktifkan “Disable child process restrictions”, lalu kembali. Biarkan Opsi pengembang tetap aktif, atau sakelarnya akan nonaktif lagi.</string> + <string name="gate_open_dev">Buka Opsi pengembang</string> + <string name="gate_no_toggle">Versi Android ini tidak memiliki sakelar untuk itu, sehingga DroidDeck mengubahnya melalui Debugging nirkabel. Membutuhkan waktu sekitar satu menit, tanpa komputer.</string> + <string name="gate_needs_wifi">Debugging nirkabel memerlukan Wi-Fi. Jaringan apa pun berfungsi; tidak perlu internet.</string> + <string name="gate_needs_notifications">Kode pemasangan muncul di notifikasi. Izinkan notifikasi untuk DroidDeck, atau gunakan Cara lain.</string> + <string name="gate_allow_notifications">Izinkan notifikasi</string> + <string name="gate_step1">Buka Setelan → Opsi pengembang → Debugging nirkabel dan aktifkan</string> + <string name="gate_step2">Ketuk “Pasangkan perangkat dengan kode pemasangan”</string> + <string name="gate_step3">Masukkan kode ke notifikasi DroidDeck</string> + <string name="gate_waiting">Menunggu jendela pemasangan…</string> + <string name="gate_code_needed">Jendela ditemukan. Masukkan kodenya di notifikasi.</string> + <string name="gate_status">Status</string> + <string name="gate_polling">Memeriksa setiap 2 detik. Halaman ini akan tertutup saat statusnya nonaktif.</string> + <string name="gate_other_ways">Cara lain</string> + <string name="gate_other_toggle">Debugging nirkabel, alamat manual, ADB komputer</string> + <string name="gate_other_no_toggle">Alamat manual, ADB komputer, Opsi pengembang</string> + <string name="gate_use_wireless_instead">Gunakan Debugging nirkabel sebagai gantinya</string> + <string name="gate_some_roms">Beberapa ROM menambahkan sakelar ini juga. Cari “child process” di Opsi pengembang.</string> + <string name="gate_manual_hint">Jika notifikasinya tidak berfungsi, masukkan sendiri alamat dan kode dari jendela pemasangan.</string> + <string name="gate_enter_manually">Masukkan alamat secara manual</string> + <string name="gate_from_computer">Dari komputer dengan ADB:</string> + <string name="gate_copy_command">Salin perintah</string> + <string name="gate_choose_display">Pilih tampilan tempat membuka Setelan Android.</string> + <string name="gate_main_screen">Layar utama</string> + <string name="wadb_finding_port">Berhasil dipasangkan. Mencari port Debugging nirkabel…</string> + <string name="wadb_enter_port">Berhasil dipasangkan. Masukkan port dari Setelan.</string> + <string name="wadb_applying">Perangkat ditemukan. Menerapkan setelan…</string> + <string name="wadb_apply_failed">Tidak dapat menerapkan secara otomatis. Periksa alamat di bawah dan coba lagi. %1$s</string> + <string name="wadb_title_wireless">Debugging nirkabel</string> + <string name="wadb_title_done">Batas proses turunan diperbarui</string> + <string name="wadb_title">Ubah batas proses turunan</string> + <string name="wadb_lede_compact0">Jika opsinya tidak ada, pasangkan melalui Debugging nirkabel.</string> + <string name="wadb_lede_compact1">Berhasil dipasangkan. Periksa alamat IP dan port di Debugging nirkabel.</string> + <string name="wadb_lede_done">Batas proses turunan telah diperbarui.</string> + <string name="wadb_lede0">Gunakan langkah 2 jika Opsi pengembang tidak memiliki setelan proses turunan. Buka Opsi pengembang → Debugging nirkabel → Pasangkan perangkat dengan kode pemasangan.</string> + <string name="wadb_lede1">Berhasil dipasangkan. Gunakan alamat di bawah, atau perbarui dari Debugging nirkabel → Alamat IP & Port.</string> + <string name="wadb_lede_done_long">Batas proses turunan telah diperbarui melalui Debugging nirkabel.</string> + <string name="wadb_confirmed_on">Android mengonfirmasi batas ini aktif.</string> + <string name="wadb_confirmed_off">Android mengonfirmasi batas ini nonaktif.</string> + <string name="wadb_pair_group">2 · Pasangkan Debugging nirkabel</string> + <string name="wadb_pair_address">Alamat IP & port jendela pemasangan</string> + <string name="wadb_pin">Kode pemasangan (PIN)</string> + <string name="wadb_pin_placeholder">6 digit</string> + <string name="wadb_pair">Pasangkan</string> + <string name="wadb_apply_group">2 · Terapkan melalui Debugging nirkabel</string> + <string name="wadb_connect_address">Alamat IP & port koneksi</string> + <string name="wadb_pair_again">Pasangkan lagi</string> + <string name="wadb_turn_off">Nonaktifkan batas</string> + <string name="wadb_other_ways">Cara lain untuk mengubahnya</string> + <string name="wadb_dev_try_first">1 · Opsi pengembang (coba dahulu)</string> + <string name="wadb_restrict_on">Aktifkan “Restrict child processes”.</string> + <string name="wadb_restrict_off">Nonaktifkan “Restrict child processes”.</string> + <string name="wadb_computer_last">3 · ADB komputer (upaya terakhir)</string> + <string name="wadb_only_if">Hanya jika Debugging nirkabel tidak tersedia.</string> + <string name="wadb_computer_group">3 · ADB komputer (upaya terakhir)</string> + <string name="ctrl_osc_hint">Saat kontrol sentuh muncul dalam sesi</string> + <string name="ctrl_color">Warna</string> + <string name="ctrl_color_hint">Warna tombol di layar</string> + <string name="ctrl_custom">Khusus</string> + <string name="ctrl_opacity">Opasitas</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">Ukuran tombol</string> + <string name="ctrl_size_hint" formatted="false">100% adalah ukuran standar</string> + <string name="ctrl_stick_click">Klik stik</string> + <string name="ctrl_stick_click_hint">Ketuk dua kali dan tahan sebuah stik untuk L3 atau R3</string> + <string name="ctrl_adaptive">Stik adaptif</string> + <string name="ctrl_rumble">Getaran gim</string> + <string name="ctrl_steam_button">Tombol Steam</string> + <string name="ctrl_qam_button">Tombol QAM</string> + <string name="ctrl_keyboard_button">Tombol keyboard</string> + <string name="ctrl_select_steam">Tekan Select untuk membuka menu Steam</string> + <string name="ctrl_select_steam_hint">Bukan View/Select.</string> + <string name="setup_animations">Animasi</string> + <string name="ctrl_adaptive_hint">Stik muncul di tempat Anda menyentuh dan tersembunyi saat dilepas</string> + <string name="ctrl_layout">Tata letak</string> + <string name="ctrl_layout_custom">Posisi khusus disimpan</string> + <string name="ctrl_layout_default">Disesuaikan dengan layar ini</string> + <string name="ctrl_edit">Edit</string> + <string name="ctrl_reset">Reset</string> + <string name="ctrl_mapping">Pemetaan tombol</string> + <string name="ctrl_mapping_default">Setiap tombol mengirim input-nya sendiri</string> + <string name="ctrl_mapping_count">%1$d dari %2$d tombol dipetakan ulang</string> + <string name="ctrl_configure">Konfigurasi</string> + <string name="ctrl_reset_all">Reset pengontrol</string> + <string name="ctrl_reset_all_hint">Mengembalikan semua setelan pengontrol dan tata letak</string> + <string name="ctrl_mapping_lede">Apa yang dikirim setiap tombol di layar ke gim. Tersembunyi menghapus tombol tersebut.</string> + <string name="ctrl_onscreen_buttons">Tombol di layar</string> + <string name="ctrl_defaults">Default</string> + <string name="ctrl_reset_mapping">Reset pemetaan</string> + <string name="content_android_apps">Aplikasi Android</string> + <string name="content_apps_count">%1$d aplikasi</string> + <string name="content_no_android_apps">Tidak ada aplikasi Android yang dapat dijalankan.</string> + <string name="content_desktop_installed">● Desktop terinstal</string> + <string name="content_desktop_first_open">Terinstal saat pertama kali dibuka</string> + <string name="content_available">Tersedia untuk diinstal</string> + <string name="content_not_installed">Belum terinstal.</string> + <string name="content_not_installed_suffix">%1$s · belum terinstal</string> + <string name="content_open_named">Buka %1$s</string> + <string name="content_games">Gim</string> + <string name="content_choose_roms">Pilih folder ROM.</string> + <string name="content_retroarch_roms">Jelajahi ke /root/ROMs di RetroArch.</string> + <string name="content_add_roms">Tambahkan gim %1$s ke ROMs/%2$s.</string> + <string name="content_install_named">Instal %1$s</string> + <string name="content_stop_to_install">Hentikan sesi untuk menginstal</string> + <string name="content_runtime_required">Runtime diperlukan</string> + <string name="content_loading_details">Memuat detail instalasi…</string> + <string name="content_details_unavailable">Detail instalasi tidak tersedia. Coba lagi nanti.</string> + <string name="content_needs_runtime">Instal runtime Linux dari Penyiapan terlebih dahulu.</string> + <string name="content_stop_first">Hentikan sesi yang berjalan untuk menginstal aplikasi desktop.</string> + <string name="content_install_into">Menginstal %1$s ke desktop Linux.</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">Menyembunyikan menghapusnya dari Desktop. Filenya tetap berada di runtime Linux.</string> + <string name="content_rom_gone">Gim tersebut sudah tidak ada di folder ROMs.</string> + <string name="content_desktop_system">Desktop · %1$s</string> + <string name="content_launch_in">Luncurkan di %1$s</string> + <string name="content_also_in">Juga di %1$s</string> + <string name="content_page_gone">Halaman tersebut sudah tidak ada. Pilih bagian di sebelah kiri.</string> + <string name="content_open_desktop">Buka desktop</string> + <string name="content_install_open_desktop">Instal & buka desktop</string> + <string name="desktop_remove_title">Hapus instalasi desktop</string> + <string name="desktop_remove_text">Hapus desktop dan aplikasi bawaannya. Simpan gim, aplikasi yang ditambahkan, setelan, dan pustaka bersama.</string> + <string name="desktop_removing">Menghapus desktop</string> + <string name="desktop_removed">Desktop dihapus</string> + <string name="desktop_cleaning_legacy">Menghapus file LXQt lama</string> + <string name="desktop_removal_unknown">Perbarui DroidDeck sebelum menghapus versi desktop ini.</string> + <string name="content_linux_desktop">Desktop Linux</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox, dan emulator Anda</string> + <string name="games_empty">Gim yang Anda instal di Steam akan muncul di sini setelah sesi.</string> + <string name="games_play_steam">Mainkan Steam</string> + <string name="games_launch_settings">Setelan peluncuran</string> + <string name="games_launch">Luncurkan</string> + <string name="games_files">File gim</string> + <string name="games_prefix">Prefix Proton</string> + <string name="games_saves_looking">Mencari data simpanan…</string> + <string name="games_saves_none">Tidak ditemukan folder simpanan berdasarkan nama. Ekspor mengambil seluruh folder pengguna.</string> + <string name="games_saves_folders">%1$d folder simpanan</string> + <string name="games_saves_summary">%1$s · %2$d file · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">Kelola data simpanan</string> + <string name="games_saves">Data simpanan gim</string> + <string name="games_import_saves">Impor data simpanan…</string> + <string name="games_export_gamehub">Ekspor sebagai zip GameHub…</string> + <string name="games_export_winlator">Ekspor sebagai zip Winlator…</string> + <string name="games_open_save_folder">Buka folder simpanan</string> + <string name="games_never_played">Belum pernah dimainkan</string> + <string name="games_runtime_busy_percent">Runtime sibuk · %1$d%%</string> + <string name="games_runtime_busy">Runtime sibuk</string> + <string name="games_runtime_first_play">Runtime terinstal saat Main pertama kali</string> + <string name="games_runtime_update">Pembaruan runtime tersedia</string> + <string name="games_runtime_ready">● Runtime siap</string> + <string name="games_last_played">Terakhir dimainkan %1$s</string> + <string name="games_internal">Penyimpanan internal</string> + <string name="games_added">Gim ditambahkan</string> + <string name="games_custom">Gim khusus</string> + <string name="games_store_storage">Penyimpanan gim</string> + <string name="gpu_auto_hint">Pasangan terbaik untuk GPU ini, selalu diperbarui</string> + <string name="gpu_manual_hint">Pilih pasangan sendiri</string> + <string name="gpu_checking">Memeriksa driver…</string> + <string name="gpu_none_listed">Belum ada driver yang terdaftar. Segarkan untuk mencarinya.</string> + <string name="gpu_hide_other">Sembunyikan driver untuk GPU lain</string> + <string name="gpu_show_other">Tampilkan driver untuk GPU lain</string> + <string name="gpu_advanced">Lanjutan</string> + <string name="gpu_set_with_display">Diatur bersama driver tampilan: %1$s membawa keduanya.</string> + <string name="gpu_set_with_runtime">Diatur bersama driver runtime: %1$s membawa keduanya.</string> + <string name="gpu_runtime_hint">Digunakan oleh gim dan desktop.</string> + <string name="gpu_runtime_default">Default runtime</string> + <string name="gpu_display_hint">Menggambar frame ke layar. Berlaku setelah DroidDeck dimulai ulang.</string> + <string name="gpu_auto_by_gpu">Otomatis - dipilih berdasarkan GPU</string> + <string name="gpu_manage">Unduh, impor, atau hapus…</string> + <string name="gpu_in_use_caps">DIGUNAKAN</string> + <string name="gpu_builtin">Driver bawaan DroidDeck</string> + <string name="gpu_matched_pair">Driver runtime dan tampilan diatur bersama sebagai pasangan yang cocok.</string> + <string name="gpu_recommended_for">Direkomendasikan untuk GPU ini: %1$s.</string> + <string name="gpu_refresh_hint">Segarkan untuk mencari driver bagi GPU ini.</string> + <string name="gpu_recommended_caps">DIREKOMENDASIKAN</string> + <string name="gpu_unavailable">Tidak tersedia</string> + <string name="load_runtime">Runtime Linux</string> + <string name="load_runtime_detail">Menyiapkan runtime Linux</string> + <string name="load_session">Sesi</string> + <string name="load_session_detail">Memulai tampilan dan audio</string> + <string name="load_steam">Klien Steam</string> + <string name="load_steam_detail">Memeriksa Steam dan alat kompatibilitasnya</string> + <string name="load_opening_steam">Membuka Steam</string> + <string name="load_opening_steam_detail">Menunggu frame pertama Steam</string> + <string name="load_desktop">Desktop</string> + <string name="load_desktop_detail">Menyiapkan desktop</string> + <string name="load_opening">Membuka</string> + <string name="load_opening_detail">Menunggu frame pertama</string> + <string name="load_starting_steam">Memulai Steam</string> + <string name="load_ended">Sesi berakhir</string> + <string name="load_step">Langkah %1$d dari %2$d</string> + <string name="load_steam_paused">Steam dijeda</string> + <string name="deck_tab_grips">Pegangan</string> + <string name="deck_tab_left">Pad kiri</string> + <string name="deck_tab_right">Pad kanan</string> + <string name="deck_tab_both">Keduanya</string> + <string name="deck_tab_gamepad">Gamepad</string> + <string name="deck_surface_stick">Stik</string> + <string name="deck_surface_trackpad">Trackpad</string> + <string name="deck_surface_switch">Beralih antara stik dan trackpad</string> + <string name="deck_more_controls">Kontrol Deck lainnya</string> + <string name="deck_panels">PANEL</string> + <string name="deck_close">Tutup kontrol layar kedua</string> + <string name="deck_open_steam_menu">Buka menu Steam</string> + <string name="deck_left_upper">Kiri · atas</string> + <string name="deck_left_lower">Kiri · bawah</string> + <string name="deck_right_upper">Kanan · atas</string> + <string name="deck_right_lower">Kanan · bawah</string> + <string name="deck_click">Klik</string> + <string name="deck_right_trackpad">Trackpad kanan</string> + <string name="deck_left_trackpad">Trackpad kiri</string> + <string name="second_trackpad">Trackpad</string> + <string name="second_show_keyboard">Tampilkan keyboard</string> + <string name="second_hide_keyboard">Sembunyikan keyboard</string> + <string name="second_terminal">Terminal Linux</string> + <string name="second_terminal_not_ready">Sesi Linux belum siap untuk terminal.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Game</string> + <string name="rail_desktop">Desktop</string> + <string name="rail_store">Toko</string> + <string name="rail_components">Komponen</string> + <string name="rail_apps">Aplikasi</string> + <string name="rail_setup">Penyiapan</string> + <string name="rail_updates">Pembaruan</string> + <string name="rail_percent">%1$d%%</string> + <string name="rail_resume_named">Lanjutkan %1$s</string> + <string name="roms_text">Muncul sebagai /root/ROMs di setiap sesi. Penyimpanan internal berada di /root/Storage.</string> + <string name="roms_none">Belum ada folder yang dipilih</string> + <string name="roms_applies">Berlaku pada sesi berikutnya. Kartu SD juga bisa.</string> + <string name="roms_choose">Pilih folder</string> + <string name="roms_change">Ubah folder</string> + <string name="proton_lede">Instal build Proton ARM64, lalu pilih per game di Steam → Properti → Kompatibilitas.</string> + <string name="proton_stop_first">Hentikan sesi yang berjalan untuk menginstal atau menghapus alat kompatibilitas.</string> + <string name="proton_available">Build yang tersedia</string> + <string name="proton_installed">Terinstal %1$s</string> + <string name="proton_queued">Tertunda dari permintaan sebelumnya</string> + <string name="proton_not_installed">Belum terinstal</string> + <string name="proton_update_available">Pembaruan tersedia → %1$s</string> + <string name="proton_remove_older">Hapus versi %1$s yang lama</string> + <string name="proton_remove_older_body">Menghapus %1$s. Game yang diatur ke versi ini harus dialihkan ke Proton lain.</string> + <string name="proton_remove_body">Game yang diatur ke versi ini harus dialihkan ke Proton lain.</string> + <string name="proton_install_now">Instal sekarang</string> + <string name="proton_cancel_queued">Batalkan antrean</string> + <string name="pckb_android">Keyboard Android</string> + <string name="pckb_hide">Sembunyikan ✕</string> + <string name="main_launch_named">Luncurkan %1$s</string> + <string name="main_choose_screen">Pilih layar. Sekunder: %1$s.</string> + <string name="main_secondary_gone">Layar sekunder tidak lagi tersedia.</string> + <string name="main_primary">Layar utama</string> + <string name="main_secondary">Layar sekunder</string> + <string name="widgets_prev_tab">Tab sebelumnya</string> + <string name="widgets_next_tab">Tab selanjutnya</string> + <string name="widgets_on">Aktif</string> + <string name="widgets_all_n">Semua %1$d</string> + <string name="widgets_none">Tidak ada</string> + <string name="widgets_n_of_m">%1$d dari %2$d</string> + <string name="widgets_off">Nonaktif</string> + <string name="screen_bottom">Layar bawah</string> + <string name="game_env_applies">Berlaku pada peluncuran game berikutnya. Profil khusus game ini menimpa profil bersama.</string> + <string name="game_env_other">Game lain berdasarkan app ID…</string> + <string name="game_env_done">Selesai</string> + <string name="game_env_default">Default</string> + <string name="game_env_off">Nonaktif</string> + <string name="game_env_change">Ubah…</string> + <string name="game_env_change_title">Ubah variabel</string> + <string name="game_env_restore_default">Kembali ke default</string> + <string name="game_env_eyebrow">Game Proton</string> + <string name="pip_title">Gambar dalam gambar</string> + <string name="pip_auto">Otomatis masuk ke mode PiP</string> + <string name="unfolded_controls">Kontrol di layar saat dibuka</string> + <string name="unfolded_controls_hint">Saat dibuka, game berada di atas trackpad dan grip belakang Steam Deck. Saat dilipat, game tampil layar penuh.</string> + <string name="pip_suspended">Ditangguhkan</string> + <string name="pip_not_ready">Gambar dalam gambar tersedia setelah sesi siap.</string> + <string name="pip_permission">Izinkan gambar dalam gambar untuk DroidDeck di setelan Android.</string> + <string name="pip_unavailable">Android tidak dapat membuka gambar dalam gambar.</string> + <string name="game_shortcut">Pintasan</string> + <string name="game_shortcut_add">Tambahkan ke layar utama</string> + <string name="game_shortcut_choose">Pilih game</string> + <string name="game_shortcut_scanning">Memindai game yang terinstal…</string> + <string name="game_link_copy">Salin tautan peluncuran</string> + <string name="game_link_copied">Tautan peluncuran disalin</string> + <string name="game_shortcut_unsupported">Launcher ini tidak dapat menambahkan pintasan. Gunakan Salin tautan peluncuran.</string> + <string name="game_link_invalid">Tautan peluncuran game tidak valid.</string> + <string name="game_link_missing">Game ini tidak lagi ada di pustaka DroidDeck Anda.</string> + <string name="game_link_session_busy">Hentikan sesi saat ini sebelum meluncurkan game Steam.</string> + <string name="game_link_failed">Tidak dapat mengirim game ke Steam. Coba luncurkan lagi.</string> + <string name="setup_retry_removal">Ulangi penghapusan</string> + <string name="runtime_removing">Menghapus</string> + <string name="runtime_removal_incomplete">Penghapusan belum selesai</string> + <string name="game_file_export">Ekspor file game</string> + <string name="game_file_sync">Sinkronkan semua file game ke folder</string> + <string name="game_file_stop_sync">Hentikan sinkronisasi file game</string> + <string name="game_file_folder">Pilih folder untuk file game frontend</string> + <string name="game_file_exported">File game disimpan ke %1$s</string> + <string name="game_frontend_files">File frontend</string> + + <string name="main_wait_runtime">Tunggu operasi runtime selesai</string> + <string name="main_wait_proton_install">Tunggu instalasi alat kompatibilitas selesai</string> + <string name="main_wait_desktop_install">Tunggu instalasi aplikasi desktop selesai</string> + <string name="main_wait_install_game">Tunggu instalasi selesai sebelum meluncurkan game</string> + <string name="main_saves_close_session">Tutup sesi Steam dulu, agar game tidak menyimpan dan menimpa hasil impor</string> + <string name="main_saves_importing">Mengimpor ke %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="other">%1$d file diimpor dari %2$s ke %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="other">%1$d file diimpor dari %2$s ke %3$s. Save lama dicadangkan ke Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Pilih zip save untuk %1$s</string> + <string name="main_saves_exporting">Mengekspor %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="other">%1$d file diekspor sebagai %2$s: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Pilih tempat menyimpan %1$s (%2$s)</string> + <string name="main_action_failed">%1$s gagal: %2$s</string> + <string name="main_no_logs_yet">Belum ada log sesi: jalankan sesi dulu.</string> + <string name="main_no_logs_to_clear">Tidak ada log sesi untuk dihapus.</string> + <plurals name="main_logs_cleared"> + <item quantity="other">%1$d folder log sesi dihapus.</item> + </plurals> + <string name="main_wadb_pair_failed">Penyambungan debugging nirkabel gagal</string> + <string name="main_wadb_command_failed">Perintah debugging nirkabel gagal</string> + <string name="main_wadb_unavailable">Debugging nirkabel tidak tersedia</string> + <string name="main_adb_copied">Perintah ADB disalin</string> + <string name="main_pick_roms">Pilih folder ROMs</string> + <string name="main_non_adreno_title">Bukan GPU Adreno</string> + <string name="main_non_adreno_text">Turnip mendukung GPU Adreno. Di %1$s, Steam mungkin menampilkan layar hitam. Unduhan: %2$.0f MB.</string> + <string name="main_install_anyway">Tetap instal</string> + <string name="main_remove_runtime_title">Hapus runtime Linux</string> + <string name="main_remove_runtime_text">Ini menghapus runtime, klien Steam di dalamnya, dan setiap game yang terinstal di sana.</string> + <string name="main_applied">Diterapkan: %1$s</string> + <string name="android_app_open_failed_primary">Tidak dapat membuka %1$s di layar utama</string> + <string name="android_app_open_failed_on">Tidak dapat membuka %1$s di %2$s</string> + <string name="android_app_open_failed_display">Tidak dapat membuka %1$s di layar %2$d</string> + <string name="comp_busy_importing">Mengimpor</string> + <string name="comp_busy_swapping">Mengganti</string> + <string name="comp_busy_restoring">Memulihkan</string> + <string name="comp_busy_cancelling">Membatalkan</string> + <string name="comp_busy_deleting">Menghapus</string> + <string name="comp_cannot_read">tidak dapat membaca file</string> + <string name="comp_imported">Diimpor %1$s</string> + <string name="comp_swap_cancelled">Pergantian yang tertunda dibatalkan.</string> + <string name="comp_pick_wcp">Pilih paket komponen (-linux .wcp)</string> + <string name="comp_nightlies_unreachable">Nightlies tidak dapat dihubungi</string> + <string name="comp_stored">Disimpan %1$s</string> + <string name="comp_download_failed">Unduhan gagal: %1$s</string> + <string name="drivers_pick_linux">Pilih driver runtime Linux (zip -Linux atau bundle Android + Linux)</string> + <string name="drivers_pick_android">Pilih driver tampilan (zip AdrenoTools atau bundle Android + Linux)</string> + <string name="drivers_pick_any">Pilih zip driver atau bundle Android + Linux</string> + <string name="main_pkg_problem">%1$s: %2$s</string> + <string name="main_removing_named">Menghapus %1$s…</string> + <string name="main_forgetting_named">Melupakan %1$s…</string> + <string name="main_pick_game_storage">Pilih folder penyimpanan game</string> + <string name="main_pick_decky_zip">Pilih ZIP plugin Decky</string> + <string name="main_storage_not_usable">Tidak dapat digunakan: %1$s</string> + <string name="main_running_program">Program</string> + <string name="main_removing_runtime">Menghapus runtime Linux</string> + <string name="main_settings_main_screen">Tidak dapat membuka Setelan di layar bawah; dibuka di layar utama.</string> + <string name="main_open_dev_options">Buka Opsi Pengembang di Setelan</string> + + <string name="upd_unknown_apps_failed">Tidak dapat membuka setelan Android “Instal aplikasi tidak dikenal”</string> + <string name="upd_couldnt_check">Tidak dapat memeriksa: %1$s</string> + <string name="upd_no_download">Build ini tidak memiliki unduhan untuk salinan DroidDeck ini</string> + <string name="upd_stop_first">Hentikan sesi yang berjalan dulu, baru perbarui</string> + <string name="upd_stage_downloading">Mengunduh</string> + <string name="upd_stage_installing">Menginstal</string> + <string name="upd_stage_confirm">Menunggu konfirmasi Anda</string> + <string name="upd_failed">Pembaruan gagal</string> + <string name="upd_not_offered">Android tidak menawarkan untuk menginstal pembaruan</string> + + <string name="decky_install_failed">Instalasi Decky gagal: %1$s</string> + <string name="decky_plugin_starting">Memulai impor plugin…</string> + <string name="decky_plugin_failed">Impor plugin gagal: %1$s</string> + <string name="decky_plugin_installed">Plugin terinstal. Mulai ulang Steam untuk memuatnya.</string> + + <string name="drivers_not_checked">Belum diperiksa - ketuk refresh untuk mencari driver baru</string> + <string name="drivers_bundle_sets_display">Android + Linux: mengatur driver tampilan dengannya</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: mengatur driver runtime dengannya</string> + <string name="drivers_runtime_builtin">Turnip bawaan runtime</string> + <string name="drivers_system_vulkan">Vulkan sistem: tidak ada build bawaan untuk GPU ini</string> + <string name="drivers_bundled">%1$s (bawaan)</string> + <string name="drivers_manual">Manual</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">Hanya satu bagian yang dirilis saat ini</string> + <string name="drivers_pair_size">%1$.0f MB untuk keduanya</string> + <string name="drivers_none_to_set">Tidak ada driver untuk diatur: %1$s</string> + <string name="drivers_checking_latest">Memeriksa driver terbaru…</string> + <string name="drivers_up_to_date_check_failed">Sudah terbaru (tidak dapat memeriksa: %1$s)</string> + <string name="drivers_recommended_unpublished">Driver yang direkomendasikan belum dirilis saat ini</string> + <string name="drivers_downloading_pair">Mengunduh %1$s %2$s…</string> + <string name="drivers_updated_to">Diperbarui ke %1$s %2$s.</string> + <string name="drivers_updated_to_restart">Diperbarui ke %1$s %2$s. Driver tampilan berlaku setelah DroidDeck dimulai ulang.</string> + <string name="drivers_switched_to">Beralih ke %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Beralih ke %1$s %2$s. Driver tampilan berlaku setelah DroidDeck dimulai ulang.</string> + <string name="drivers_using">Menggunakan %1$s %2$s.</string> + <string name="drivers_using_restart">Menggunakan %1$s %2$s. Driver tampilan berlaku setelah DroidDeck dimulai ulang.</string> + <string name="drivers_not_a_driver">Bukan zip driver: bukan driver AdrenoTools, Turnip -Linux, maupun bundle Android + Linux</string> + <string name="drivers_import_failed">Impor gagal: %1$s</string> + <string name="drivers_imported_unnamed">Driver diimpor</string> + <string name="drivers_using_bundle">Menggunakan %1$s sebagai driver runtime dan tampilan.</string> + <string name="drivers_using_bundle_restart">Menggunakan %1$s sebagai driver runtime dan tampilan. Driver tampilan berlaku setelah DroidDeck dimulai ulang.</string> + <string name="drivers_deleted_bundle">Dihapus %1$s (kedua driver)</string> + <string name="drivers_deleted">Dihapus %1$s</string> + <string name="drivers_release_status">Terbaru: %1$s · diperiksa %2$s</string> + <string name="drivers_release_status_failed">Terbaru: %1$s · %2$s tidak dapat dihubungi · diperiksa %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="other">%1$d mnt lalu</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="other">%1$d jam lalu</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="other">%1$d hari lalu</item> + </plurals> + <string name="drivers_checking_sources">Memeriksa Banners-Turnip, WinNative, dan DroidDeck…</string> + <string name="drivers_check_failed">pemeriksaan gagal</string> + <string name="drivers_installed_pick_either">Terinstal %1$s - pilih di salah satu daftar untuk mengatur kedua driver</string> + <string name="drivers_installed_pick">Terinstal %1$s - pilih di menu</string> + + <string name="session_wait_removal">Tunggu penghapusan runtime selesai</string> + <string name="session_title_game">Game</string> + <string name="session_touch_auto_touchpad">touchpad</string> + <string name="session_touch_auto_direct">langsung</string> + <string name="session_swap_failed">Pergantian gagal: %1$s</string> + <string name="session_profile_save_failed">Penyimpanan profil gagal: %1$s</string> + <string name="session_desktop_paused">Desktop dijeda</string> + <string name="session_named_paused">%1$s dijeda</string> + <string name="session_program_paused">Program dijeda</string> + <string name="session_starting_desktop">Memulai desktop</string> + <string name="session_starting_named">Memulai %1$s</string> + <string name="session_starting_program">Memulai program</string> + <string name="session_no_logs">Tidak ada log untuk sesi ini.</string> + <string name="session_logs_archive_failed">Tidak dapat membuat arsip log sesi.</string> + <string name="session_logs_share_failed">Tidak dapat membagikan log sesi.</string> + <string name="session_starting_session">Memulai sesi…</string> + <string name="session_runtime_downloading">mengunduh runtime Linux</string> + <string name="session_runtime_downloading_mb">mengunduh runtime Linux · %1$d dari %2$d MB</string> + <string name="session_runtime_checking">memeriksa runtime Linux</string> + <string name="session_runtime_unpacking">mengekstrak runtime Linux</string> + <string name="session_desktop_downloading">mengunduh desktop</string> + <string name="session_desktop_downloading_mb">mengunduh desktop · %1$d dari %2$d MB</string> + <string name="session_desktop_checking">memeriksa desktop</string> + <string name="session_desktop_unpacking">mengekstrak desktop</string> + <string name="session_proton_downloading">mengunduh Proton Experimental (ARM64)</string> + <string name="session_proton_downloading_mb">mengunduh Proton Experimental (ARM64) · %1$d dari %2$d MB</string> + <string name="session_proton_checking">memeriksa Proton Experimental (ARM64)</string> + <string name="session_proton_unpacking">mengekstrak Proton Experimental (ARM64)</string> + <string name="session_runtime_catalog_unreachable">Tidak dapat menghubungi katalog runtime. Periksa koneksi dan tekan Mainkan lagi.</string> + <string name="session_runtime_install_failed">Runtime Linux tidak terinstal. Periksa runtime di Penyiapan dan tekan Mainkan lagi.</string> + <string name="session_desktop_install_failed">Desktop tidak terinstal (%1$s). Periksa koneksi dan tekan Desktop lagi.</string> + <string name="session_ended_named">%1$s berhenti secara tak terduga. Bagikan log bersama laporan bug, atau coba lagi.</string> + <string name="session_ended_program">Program berhenti secara tak terduga. Bagikan log bersama laporan bug, atau coba lagi.</string> + <string name="session_ended_desktop">Desktop berhenti secara tak terduga. Bagikan log bersama laporan bug, atau coba lagi.</string> + <string name="session_ended_detail">Status keluar %1$d · %2$s</string> + <string name="session_ended_no_log">tidak ada log</string> + <plurals name="session_hint_enosys"> + <item quantity="other">Log menunjukkan %1$d error “Function not implemented”: akselerasi seccomp proot gagal pada salah satu helper di perangkat ini. Coba %2$s → “%3$s” (atau “%4$s”) lalu mulai lagi.</item> + </plurals> + + <string name="mode_decky_plugins">Plugin</string> + <string name="mode_decky_plugins_hint">Instal file ZIP plugin untuk sesi Steam berikutnya.</string> + <string name="mode_decky_plugin_downloading">Mengunduh…</string> + <string name="mode_decky_plugin_install_zip">Instal dari ZIP</string> + <string name="store_cat_games">Gim</string> + <string name="store_cat_audio_video">Audio & Video</string> + <string name="store_cat_graphics">Grafis</string> + <string name="store_cat_internet">Internet</string> + <string name="store_cat_office">Perkantoran</string> + <string name="store_cat_developer">Pengembang</string> + <string name="store_cat_education">Pendidikan</string> + <string name="store_cat_science">Sains</string> + <string name="store_cat_system">Sistem</string> + <string name="store_cat_utilities">Utilitas</string> + <string name="store_section_popular">Populer</string> + <string name="store_section_trending">Tren</string> + <string name="store_section_new">Baru</string> + <string name="store_section_updated">Diperbarui</string> + <string name="store_section_loading">memuat…</string> + <string name="store_flatpak_setup">Penyiapan Flatpak</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Tidak dapat memeriksa pembaruan di Flathub</string> + <string name="content_rom_installed">terinstal</string> + <string name="content_rom_folder">folder</string> + <string name="theme_graphite">Graphite</string> + <string name="theme_graphite_detail">Permukaan graphite, dengan biru orb khas logo untuk semua yang dipilih, difokuskan, atau ditekan.</string> + <string name="theme_paper">Kertas di atas hitam</string> + <string name="theme_paper_detail">Putih adalah warna utama, biru pada ikon menjadi sinyal untuk garis, cincin, dan titik.</string> + <string name="theme_icon_blue">Biru ikon</string> + <string name="theme_icon_blue_detail">Dasar hitam, tipe putih, biru untuk semua yang dipilih, difokuskan, atau ditekan.</string> + <string name="theme_electric">Navy elektrik</string> + <string name="theme_electric_detail">Biru yang lebih cerah, pada permukaan yang condong ke navy, bukan abu-abu.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Space</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <string name="game_sync_err_runtime">Instal runtime terlebih dahulu</string> + <string name="game_sync_err_steam_library">Pustaka Steam tidak tersedia</string> + <string name="game_sync_err_previous_sd">Pustaka SD sebelumnya tidak tersedia</string> + <string name="game_sync_err_game_storage">Penyimpanan gim tidak tersedia</string> + <string name="game_sync_err_added_games">Folder gim yang ditambahkan tidak tersedia</string> + <string name="game_sync_err_manifests">Manifes Steam tidak tersedia</string> + <string name="content_system_many">Banyak sistem</string> + <string name="fm_drive_internal">Internal</string> + <string name="fm_drive_linux">Sistem Linux</string> + <string name="fm_drive_sd">Kartu SD</string> + <string name="fm_drive_storage">Penyimpanan</string> + <string name="fm_select">Pilih</string> + <string name="fm_properties">Properti</string> + <string name="fm_favorite_add">Tambahkan ke Favorit</string> + <string name="fm_favorite_remove">Hapus dari Favorit</string> + <string name="fm_rename">Ganti nama</string> + <string name="fm_copy">Salin</string> + <string name="fm_cut">Potong</string> + <string name="fm_actions">Tindakan</string> + <string name="fm_favorites">Favorit</string> + <string name="fm_pin_current">Sematkan folder saat ini</string> + <string name="fm_no_favorites">Belum ada favorit - sematkan folder dengan menu ⋮-nya untuk kembali ke sini dengan cepat.</string> + <string name="fm_unpin_favorite">Hapus dari favorit</string> + <string name="fm_size_zero">0 byte</string> + <string name="fm_unit_bytes">byte</string> + <string name="fm_prop_location">Lokasi</string> + <string name="fm_prop_type">Jenis</string> + <string name="fm_prop_size">Ukuran</string> + <string name="fm_prop_modified">Diubah</string> + <string name="fm_type_folder">Folder</string> + <string name="fm_type_file">File</string> + <string name="fm_type_ext_file">File %1$s</string> + <string name="fm_read_only">Hanya-baca</string> + <string name="fm_read_only_hint">Mencegah gim (dan Wine) menimpa atau menghapus file ini.</string> + <string name="fm_read_only_failed">Tidak dapat mengubah Hanya-baca</string> + <string name="fm_hidden">Tersembunyi</string> + <string name="fm_hidden_unsupported">Penyimpanan ini tidak dapat menyimpan tanda tersembunyi.</string> + <string name="fm_hidden_hint">Menandai file tersembunyi di Windows (atribut tersembunyi milik Wine).</string> + <string name="fm_hidden_failed">Tidak dapat mengubah Tersembunyi pada penyimpanan ini</string> + <string name="fm_done">Selesai</string> + <string name="fm_op_deleting">Menghapus...</string> + <string name="fm_delete_failed">Penghapusan gagal</string> + <string name="fm_op_moving">Memindahkan...</string> + <string name="fm_op_copying">Menyalin...</string> + <string name="fm_op_moving_item">Memindahkan - %1$s</string> + <string name="fm_op_copying_item">Menyalin - %1$s</string> + <string name="fm_op_moving_batch">Memindahkan %1$d/%2$d - %3$s</string> + <string name="fm_op_copying_batch">Menyalin %1$d/%2$d - %3$s</string> + <string name="fm_paste_failed">%1$d selesai, %2$d gagal</string> + <string name="fm_paste_skipped">%1$d selesai, %2$d dilewati</string> + <plurals name="fm_paste_moved"> + <item quantity="other">%1$d item dipindahkan</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="other">%1$d item disalin</item> + </plurals> + <string name="fm_already_exists">\"%1$s\" sudah ada</string> + <string name="fm_op_renaming">Mengganti nama...</string> + <string name="fm_rename_failed">Gagal mengganti nama</string> + <string name="fm_op_creating_folder">Membuat folder...</string> + <string name="fm_create_folder_failed">Tidak dapat membuat folder</string> + <string name="fm_new_folder">Folder Baru</string> + <string name="fm_folder_name">Nama folder</string> + <string name="fm_create">Buat</string> + <string name="fm_new_name">Nama baru</string> + <string name="fm_delete_title">Hapus?</string> + <string name="fm_delete_text">Hapus \"%1$s\" secara permanen?</string> + <plurals name="fm_delete_items_title"> + <item quantity="other">Hapus %1$d item?</item> + </plurals> + <string name="fm_cannot_undo">Ini tidak dapat dibatalkan.</string> + <plurals name="fm_and_more"> + <item quantity="other">…dan %1$d lainnya</item> + </plurals> + <string name="fm_op_deleting_batch">Menghapus %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="other">%1$d tidak dapat dihapus</item> + </plurals> + <string name="fm_conflict_merge_hint">Gabungkan menambahkan dan mengganti file di dalam folder yang sudah ada.</string> + <string name="fm_conflict_overwrite_hint">Timpa mengganti file yang sudah ada.</string> + <string name="fm_conflict_apply_all">Terapkan ke semua konflik</string> + <string name="fm_conflict_merge">Gabungkan</string> + <string name="fm_conflict_overwrite">Timpa</string> + <string name="fm_conflict_keep_both">Simpan keduanya</string> + <string name="fm_conflict_skip">Lewati</string> + <string name="fm_select_this_folder">Pilih folder ini</string> + <string name="fm_back">Kembali</string> + <string name="fm_drive_not_readable">%1$s terpasang tetapi tidak dapat dibaca saat ini</string> + <string name="fm_list_view">Tampilan daftar</string> + <string name="fm_grid_view">Tampilan kisi</string> + <string name="fm_search">Cari</string> + <string name="fm_sort">Urutkan</string> + <string name="fm_sort_name">Nama</string> + <string name="fm_sort_date">Tanggal diubah</string> + <string name="fm_rows_comfortable">Baris nyaman</string> + <string name="fm_rows_compact">Baris ringkas</string> + <string name="fm_hide_hidden">Sembunyikan file tersembunyi</string> + <string name="fm_show_hidden">Tampilkan file tersembunyi</string> + <string name="fm_hide_favorites">Sembunyikan favorit</string> + <string name="fm_show_favorites">Tampilkan favorit</string> + <string name="fm_filter_folder">Filter folder ini</string> + <string name="fm_free_space">%1$s bebas</string> + <string name="fm_selected_count">%1$d dipilih</string> + <string name="fm_select_none">Tidak Ada</string> + <string name="fm_select_all">Semua</string> + <string name="fm_paste">Tempel</string> + <plurals name="fm_items"> + <item quantity="other">%1$d item</item> + </plurals> + <string name="fm_paste_here">Tempel %1$s di sini</string> + <string name="fm_paste_move_here">Tempel %1$s (pindahkan) di sini</string> + <string name="fm_rail_title">File</string> + <string name="fm_rail_storage">PENYIMPANAN</string> + <string name="fm_rail_quick">CEPAT</string> + <string name="fm_rail_favorites">FAVORIT</string> + <string name="fm_rail_downloads">Unduhan</string> + <string name="fm_rail_downloads_short">Unduhan</string> + <string name="fm_rail_roms">ROM</string> + <string name="fm_rail_session_logs">Log sesi</string> + <string name="fm_rail_pictures">Gambar</string> + <string name="fm_favorite_added">\"%1$s\" ditambahkan ke Favorit</string> + <string name="fm_favorite_removed">\"%1$s\" dihapus dari Favorit</string> + <string name="fm_empty_directory">Direktori kosong</string> + <string name="fm_up_one_level">Naik satu tingkat</string> + <string name="fm_choose_folder">Pilih folder</string> + <string name="fm_choose_file">Pilih file</string> + <string name="ctrl_tint_steam_blue">Biru Steam</string> + <string name="ctrl_tint_sky">Langit</string> + <string name="ctrl_tint_white">Putih</string> + <string name="ctrl_tint_violet">Violet</string> + <string name="ctrl_tint_green">Hijau</string> + <string name="ctrl_tint_amber">Amber</string> + <string name="ctrl_tint_red">Merah</string> + <string name="ctrl_tint_pink">Merah muda</string> + <string name="ctrl_button_a">Tombol A</string> + <string name="ctrl_button_b">Tombol B</string> + <string name="ctrl_button_x">Tombol X</string> + <string name="ctrl_button_y">Tombol Y</string> + <string name="ctrl_button_lb">Bumper kiri</string> + <string name="ctrl_button_rb">Bumper kanan</string> + <string name="ctrl_button_lt">Trigger kiri</string> + <string name="ctrl_button_rt">Trigger kanan</string> + <string name="ctrl_button_view">Tombol View</string> + <string name="ctrl_button_menu">Tombol Menu</string> + <string name="ctrl_target_view">View</string> + <string name="ctrl_target_menu">Menu</string> + <string name="ctrl_target_up">D-pad atas</string> + <string name="ctrl_target_down">D-pad bawah</string> + <string name="ctrl_target_left">D-pad kiri</string> + <string name="ctrl_target_right">D-pad kanan</string> + <string name="ctrl_target_hidden">Tersembunyi</string> + <string name="ctrl_editor_drag">Seret untuk memindahkan</string> + <string name="ctrl_editor_snap">Ratakan</string> + <string name="ctrl_editor_save">Simpan</string> + <string name="second_mode_none">Tidak ada</string> + <string name="second_mode_keyboard_trackpad">Keyboard + trackpad</string> + <string name="second_mode_terminal">Terminal</string> + <string name="second_mode_deck_controls">Pegangan Deck + trackpad</string> + + <string name="rtinst_removing">Menghapus runtime Linux</string> + <plurals name="rtinst_removing_entries"> + <item quantity="other">Menghapus runtime Linux · %1$d entri</item> + </plurals> + <string name="rtinst_removed">Runtime Linux dihapus</string> + <string name="rtinst_removal_failed">Tidak dapat menyelesaikan penghapusan runtime Linux. Coba hapus lagi di Penyiapan.</string> + <string name="rtinst_removing_leftovers">Menghapus sisa runtime</string> + <string name="rtinst_downloading">Mengunduh</string> + <string name="rtinst_verifying">Memverifikasi</string> + <string name="rtinst_extracting">Mengekstrak</string> + <string name="rtinst_channel_description">Ditampilkan saat runtime Linux diinstal atau dihapus</string> + <string name="rtinst_title_installing">Menginstal runtime Linux</string> + <string name="rtinst_title_removing">Menghapus runtime Linux</string> + + <string name="deskpkg_runtime_missing">Runtime Linux tidak terinstal</string> + <string name="deskpkg_no_checksum">Katalog tidak memiliki checksum untuk %1$s</string> + <string name="deskpkg_checksum_mismatch">Checksum tidak cocok - tidak ada yang diubah</string> + <string name="deskpkg_installing">Menginstal %1$s</string> + <string name="deskpkg_place_failed">Tidak dapat menempatkan AppImage</string> + <string name="deskpkg_extract_failed">Ekstraksi gagal</string> + <string name="deskpkg_install_failed">Instalasi gagal</string> + + <string name="flatpakmgr_setup_failed">Penyiapan Flatpak gagal (keluar %1$d)</string> + <string name="flatpakmgr_setup_unfinished">Penyiapan Flatpak tidak selesai</string> + <string name="flatpakmgr_busy">Tugas toko lainnya sedang berjalan</string> + <string name="flatpakmgr_not_ready">Siapkan Flatpak terlebih dahulu</string> + <string name="flatpakmgr_starting">Memulai</string> + <string name="flatpakmgr_exit_status">Flatpak keluar dengan status %1$d</string> + <string name="flatpakmgr_installing">Menginstal %1$s</string> + <string name="flatpakmgr_updating">Memperbarui %1$s</string> + <string name="flatpakmgr_removing">Menghapus %1$s</string> + <string name="flatpakmgr_installing_step">Menginstal %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_updating_step">Memperbarui %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_removing_step">Menghapus %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_ref_translations">terjemahan</string> + <string name="flatpakmgr_ref_graphics_drivers">driver grafis</string> + <string name="flatpakmgr_ref_media_codecs">kodek media</string> + <string name="flatpakmgr_ref_runtime">Runtime %1$s</string> + <string name="flatpakmgr_ref_runtime_branch">Runtime %1$s %2$s</string> + + <string name="appimg_too_small">File terlalu kecil untuk menjadi AppImage</string> + <string name="appimg_unreadable">File tidak dapat dibaca: %1$s</string> + <string name="appimg_not_linux">Ini bukan AppImage (bukan program Linux)</string> + <string name="appimg_wrong_arch">AppImage ini tidak dibuat untuk ARM64 (aarch64) maupun PC x86</string> + <string name="appimg_not_appimage">Ini adalah program Linux tetapi bukan AppImage</string> + <string name="appimg_wrong_type">Hanya AppImage tipe 2 yang dapat diimpor (yang ini tipe %1$d)</string> + <string name="appimg_no_space_import">Ruang kosong tidak cukup: mengimpor memerlukan sekitar %1$s</string> + <string name="appimg_no_space_update">Ruang kosong tidak cukup: memperbarui memerlukan sekitar %1$s</string> + <string name="appimg_import_failed">Impor gagal</string> + <string name="appimg_gone">AppImage itu sudah tidak ada</string> + <string name="appimg_update_failed">Pembaruan gagal</string> + <string name="appimg_extracting">Mengekstrak %1$s</string> + <string name="appimg_extract_failed">AppImage tidak dapat diekstrak</string> + <string name="appimg_extract_failed_detail">AppImage tidak dapat diekstrak: %1$s</string> + + <string name="deckymgr_stop_session_loader">Hentikan sesi aktif sebelum menginstal Decky Loader</string> + <string name="deckymgr_download_failed">Unduhan Decky Loader gagal</string> + <string name="deckymgr_size_mismatch">Ukuran Decky Loader tidak sesuai dengan metadata rilis</string> + <string name="deckymgr_checksum_download_failed">Tidak dapat mengunduh checksum rilis</string> + <string name="deckymgr_checksum_missing">Checksum rilis tidak ada</string> + <string name="deckymgr_checksum_invalid">Checksum rilis tidak valid</string> + <string name="deckymgr_checksum_mismatch">Checksum Decky Loader tidak cocok</string> + <string name="deckymgr_not_elf">Aset rilis bukan file eksekusi Linux 64-bit yang kompatibel</string> + <string name="deckymgr_session_started_loader">Sesi dimulai saat pengunduhan berlangsung; hentikan sesi tersebut sebelum menginstal Decky Loader</string> + <string name="deckymgr_loader_not_executable">Tidak dapat menandai PluginLoader sebagai dapat dieksekusi</string> + <string name="deckymgr_cef_failed">Tidak dapat mengaktifkan debugging jarak jauh CEF Steam</string> + <string name="deckymgr_stage_failed">Tidak dapat menyiapkan PluginLoader</string> + <string name="deckymgr_loader_preserve_failed">Tidak dapat mempertahankan PluginLoader yang ada</string> + <string name="deckymgr_loader_activate_failed">Tidak dapat mengaktifkan PluginLoader baru</string> + <string name="deckymgr_stop_session_plugin">Hentikan sesi aktif sebelum menginstal plugin</string> + <string name="deckymgr_loader_required">Instal Decky Loader sebelum mengimpor plugin</string> + <string name="deckymgr_zip_unreadable">Tidak dapat membaca file ZIP yang dipilih</string> + <string name="deckymgr_plugins_dir_failed">Tidak dapat membuat direktori plugin Decky</string> + <string name="deckymgr_extracting_zip">Mengekstrak ZIP plugin</string> + <string name="deckymgr_zip_one_folder">ZIP plugin harus berisi satu folder plugin tingkat atas</string> + <string name="deckymgr_zip_missing_files">ZIP plugin tidak memiliki plugin.json, package.json, atau dist/index.js</string> + <string name="deckymgr_plugin_name_missing">Nama plugin tidak ada</string> + <string name="deckymgr_session_started_plugin">Sesi dimulai saat instalasi berlangsung; hentikan sesi tersebut sebelum menginstal plugin</string> + <string name="deckymgr_plugin_installed">Plugin terinstal · mulai ulang Steam untuk memuatnya</string> + <string name="deckymgr_plugin_install_failed">Instalasi ZIP plugin gagal</string> + <string name="deckymgr_zip_too_many">ZIP plugin berisi terlalu banyak entri</string> + <string name="deckymgr_zip_invalid_path">ZIP plugin berisi jalur yang tidak valid</string> + <string name="deckymgr_zip_duplicate_paths">ZIP plugin berisi jalur duplikat</string> + <string name="deckymgr_zip_unsafe_path">ZIP plugin berisi jalur yang tidak aman</string> + <string name="deckymgr_folder_failed">Tidak dapat membuat folder plugin</string> + <string name="deckymgr_zip_too_large">ZIP plugin mengembang melebihi batas 1 GiB</string> + <string name="deckymgr_bundled_not_executable">Tidak dapat menandai biner plugin bawaan sebagai dapat dieksekusi</string> + <string name="deckymgr_remote_entry_invalid">Entri remote_binary tidak valid</string> + <string name="deckymgr_remote_name_invalid">Nama biner jarak jauh tidak valid</string> + <string name="deckymgr_remote_url_https">URL biner jarak jauh harus menggunakan HTTPS</string> + <string name="deckymgr_remote_hash_invalid">Biner jarak jauh %1$s memiliki hash SHA-256 yang tidak valid</string> + <string name="deckymgr_bin_dir_failed">Tidak dapat membuat direktori bin plugin</string> + <string name="deckymgr_downloading_binary">Mengunduh biner plugin · %1$s</string> + <string name="deckymgr_binary_download_failed">Tidak dapat mengunduh biner plugin %1$s</string> + <string name="deckymgr_binary_checksum_mismatch">Checksum biner plugin tidak cocok · %1$s</string> + <string name="deckymgr_binary_not_executable">Tidak dapat menandai biner plugin sebagai dapat dieksekusi · %1$s</string> + <string name="deckymgr_binary_replace_failed">Tidak dapat mengganti biner plugin · %1$s</string> + <string name="deckymgr_binary_install_failed">Tidak dapat menginstal biner plugin · %1$s</string> + <string name="deckymgr_folder_taken">Folder plugin milik plugin lain</string> + <string name="deckymgr_preserve_failed">Tidak dapat mempertahankan plugin yang ada</string> + <string name="deckymgr_activate_failed">Tidak dapat mengaktifkan plugin yang diimpor</string> + <string name="deckymgr_settings_failed">Tidak dapat memperbarui setelan plugin Decky</string> + <string name="deckymgr_restore_failed">Tidak dapat memulihkan plugin yang ada</string> + + <string name="adbfix_save_failed">Tidak dapat menyimpan identitas Debugging nirkabel</string> + <string name="adbfix_command_failed">Perintah ADB gagal (%1$d)</string> + <string name="adbfix_not_confirmed">Android tidak mengonfirmasi bahwa batas proses turunan telah diubah: %1$s</string> + <string name="adbfix_no_pairing">Tidak ada pemasangan Debugging nirkabel yang tersimpan</string> + <string name="adbfix_enter_address">Masukkan alamat IP & Port Debugging nirkabel saat ini</string> + + <string name="adbpair_timed_out">Waktu tunggu munculan pemasangan habis. Coba lagi dari DroidDeck.</string> + <string name="adbpair_discovery_failed_code">Tidak dapat mencari munculan pemasangan (%1$d). Pastikan Wi-Fi aktif.</string> + <string name="adbpair_discovery_failed">Tidak dapat mencari munculan pemasangan: %1$s</string> + <string name="adbpair_six_digits">Masukkan ke-6 digit kode tersebut</string> + <string name="adbpair_pairing">Memasangkan…</string> + <string name="adbpair_code_rejected">Kode tersebut tidak berfungsi. Buka kembali munculan pemasangan dan masukkan kode barunya.</string> + <string name="adbpair_connecting">Terpasang. Menghubungkan…</string> + <string name="adbpair_port_not_found">Terpasang, tetapi port Debugging nirkabel tidak ditemukan. Biarkan Debugging nirkabel tetap aktif dan coba lagi.</string> + <string name="adbpair_applying">Menerapkan setelan…</string> + <string name="adbpair_command_failed">Perintah Debugging nirkabel gagal</string> + <string name="adbpair_channel_name">Penyiapan Debugging nirkabel</string> + <string name="adbpair_channel_description">Meminta kode pemasangan Debugging nirkabel saat Setelan terbuka</string> + <string name="adbpair_title_pair">Pasangkan Debugging nirkabel</string> + <string name="adbpair_waiting_text">Aktifkan Debugging nirkabel, lalu ketuk “Pasangkan perangkat dengan kode pemasangan”.</string> + <string name="adbpair_waiting_long">Aktifkan Debugging nirkabel, lalu ketuk “Pasangkan perangkat dengan kode pemasangan”. Tetap di Setelan: kodenya akan muncul di notifikasi ini.</string> + <string name="adbpair_code_label">Kode pemasangan 6 digit</string> + <string name="adbpair_code_hint">Ketuk Masukkan kode dan ketikkan 6 digit yang ditampilkan di Setelan.</string> + <string name="adbpair_title_code">Masukkan kode pemasangan Wi-Fi</string> + <string name="adbpair_title_retry">Coba kode lagi</string> + <string name="adbpair_enter_code">Masukkan kode</string> + <string name="adbpair_title_working">Menyiapkan Steam</string> + <string name="adbpair_title_done">Steam siap</string> + <string name="adbpair_done_text">Ketuk untuk kembali ke DroidDeck. Anda bisa menonaktifkan Debugging nirkabel sekarang.</string> + <string name="adbpair_title_failed">Penyiapan Debugging nirkabel dihentikan</string> + + <string name="selfinst_http">Unduhan merespons HTTP %1$d</string> + <string name="selfinst_size_mismatch">Ukuran unduhan tidak sesuai dengan APK yang dipublikasikan dan dibuang - coba lagi</string> + <string name="selfinst_checksum_mismatch">Unduhan tidak sesuai dengan checksum-nya dan dibuang - coba lagi</string> + <string name="selfinst_wrong_package">Pembaruan ini untuk paket Android %1$s, bukan %2$s</string> + <string name="selfinst_metadata_signer">Metadata pembaruan tidak mencantumkan kunci penandatanganan rilis DroidDeck</string> + <string name="selfinst_unreadable">Android tidak dapat membaca APK yang diunduh</string> + <string name="selfinst_apk_wrong_package">APK yang diunduh untuk %1$s, bukan %2$s</string> + <string name="selfinst_version_mismatch">APK yang diunduh mencantumkan versionCode %1$d, bukan %2$d yang dipublikasikan</string> + <string name="selfinst_downgrade">Android tidak akan menginstal versionCode %1$d di atas %2$d yang terinstal</string> + <string name="selfinst_no_signing">APK yang diunduh tidak memiliki informasi penandatanganan</string> + <string name="selfinst_wrong_signer">APK yang diunduh tidak ditandatangani dengan kunci rilis DroidDeck</string> + <string name="appupd_http">Katalog pembaruan merespons HTTP %1$d</string> + + <string name="loadstate_starting">Memulai sesi…</string> + <string name="loadstate_elapsed">%1$d:%2$02d berlalu · masih berjalan</string> + <string name="loadstate_steam_update">Mengunduh pembaruan klien Steam · %1$d%%</string> + <string name="loadstate_steam_client">Mengunduh klien Steam · %1$s</string> + + <string name="pextra_busy">Instalasi alat kompatibilitas sedang berjalan</string> + <string name="pextra_stop_session">Hentikan sesi aktif sebelum menginstal alat kompatibilitas</string> + <string name="pextra_no_release">Tidak dapat menemukan rilis ARM64 untuk %1$s</string> + <string name="pextra_no_space">Setidaknya 4 GB ruang kosong ditambah ukuran unduhan diperlukan untuk menginstal %1$s</string> + <string name="pextra_downloading">Mengunduh %1$s %2$s</string> + <string name="pextra_retrying">Mencoba lagi unduhan · percobaan %1$d dari %2$d</string> + <string name="pextra_interrupted">Unduhan terputus</string> + <string name="pextra_download_failed">Unduhan gagal; file sebagian disimpan agar percobaan ulang dapat dilanjutkan</string> + <string name="pextra_too_big">Unduhan melebihi ukuran rilis dan telah dihapus</string> + <string name="pextra_incomplete">Unduhan tidak lengkap; file sebagian disimpan untuk percobaan ulang yang dapat dilanjutkan</string> + <string name="pextra_verifying">Memverifikasi unduhan</string> + <string name="pextra_checksum_unreadable">Tidak dapat membaca checksum rilis; coba lagi</string> + <string name="pextra_no_checksum">%1$s %2$s tidak mempublikasikan checksum; tidak ada yang diinstal</string> + <string name="pextra_checksum_mismatch">Checksum tidak cocok; unduhan telah dihapus</string> + <string name="pextra_session_started">Sesi dimulai saat pengunduhan berlangsung; hentikan sesi tersebut sebelum menginstal alat kompatibilitas</string> + <string name="pextra_installing">Menginstal %1$s</string> + <string name="pextra_install_failed">Instalasi %1$s gagal (keluar %2$d)</string> + <string name="pextra_could_not_install">Tidak dapat menginstal %1$s</string> + <string name="esync_fetching">Mengambil paket droiddeck-esync</string> + + <string name="cmgr_fex_linux_helpers">dengan helper Linux</string> + <string name="cmgr_fex_dlls_only">Hanya DLL</string> + <string name="cmgr_original_proton_updated">Asli (Proton diperbarui sejak penggantian terakhir)</string> + <string name="cmgr_changed_outside">Diubah di luar Komponen</string> + <string name="cmgr_not_present">tidak ada</string> + <string name="cmgr_original_version">Asli %1$s</string> + <string name="cmgr_original_from">Asli dari %1$s</string> + <string name="cmgr_swap_queued">%1$s %2$s akan dipasang ke %3$s saat gim yang berjalan di sana ditutup.</string> + <string name="cmgr_swap_done">%1$s %2$s sekarang ada di %3$s. Berlaku pada peluncuran gim berikutnya.</string> + <string name="cmgr_original_missing">Paket asli tersebut tidak tersimpan</string> + <string name="cmgr_restore_queued">%1$s dari %2$s akan dikembalikan ke aslinya saat gim yang berjalan di sana ditutup.</string> + <string name="cmgr_restore_done">%1$s dari %2$s dipulihkan dari aslinya %3$s.</string> + <string name="cmgr_package_in_use">%1$s sedang digunakan di %2$s. Ganti Proton tersebut ke yang lain terlebih dahulu.</string> + <string name="cmgr_package_not_stored">%1$s tidak tersimpan</string> + <string name="cmgr_deleted">%1$s dihapus.</string> + <string name="cmgr_original_kept">Yang asli dari build terinstal disimpan: itulah cara untuk kembali.</string> + <string name="cmgr_not_stored">Tidak tersimpan</string> + <string name="cmgr_deleted_original">Menghapus yang asli %1$s dari %2$s.</string> + <string name="cmgr_nightlies_only">Unduhan hanya berasal dari rilis Nightlies</string> + <string name="cmgr_list_predates_checksums">Daftar paket ini dibuat sebelum adanya checksum - segarkan dan coba lagi</string> + <string name="cmgr_checksum_mismatch">Checksum tidak cocok</string> + + <string name="sprefs_upscaler_linear">Linear</string> + <string name="sprefs_upscaler_nearest">Nearest</string> + <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (kualitas)</string> + <string name="sprefs_upscaler_sharpen">Hanya penajaman</string> + <string name="sprefs_lod_bias_auto">Otomatis (sesuaikan skala)</string> + + <string name="look_off_desc">Tidak ada yang diterapkan.</string> + <string name="look_game_clarity">Kejernihan Gim</string> + <string name="look_game_clarity_desc">Penajaman dengan sedikit kontras - cocok untuk sehari-hari.</string> + <string name="look_vivid">Vivid</string> + <string name="look_vivid_desc">Warna didorong lebih kuat. Deband aktif agar langit tidak bergaris.</string> + <string name="look_cinematic">Sinematik</string> + <string name="look_cinematic_desc">Kontras lebih dalam, warna lebih dingin, tepi dihaluskan.</string> + <string name="look_competitive">Kompetitif</string> + <string name="look_competitive_desc">Bayangan diangkat dan penajaman berat - temukan orang lebih dulu.</string> + <string name="look_adaptive_sharpen">Penajaman Adaptif</string> + <string name="look_adaptive_sharpen_desc">Hanya penajaman, warna tidak diubah.</string> + <string name="look_filmic">Filmic</string> + <string name="look_filmic_desc">Kurva seperti film, sedikit diredam.</string> + <string name="look_arcade">Arcade</string> + <string name="look_arcade_desc">Mencolok dan berdampak kuat.</string> + <string name="look_retro_crt">CRT Retro</string> + <string name="look_retro_crt_desc">Tabung CRT dan sinyal analog digabungkan bersama.</string> + <string name="look_upscale_sharp">Upscale Tajam</string> + <string name="look_upscale_sharp_desc">Untuk menjalankan di bawah resolusi panel.</string> + <string name="look_pixel_clean">Piksel Bersih</string> + <string name="look_pixel_clean_desc">Piksel tajam untuk gim 2D dan judul lawas.</string> + <string name="look_anime_edge">Tepi Anime</string> + <string name="look_anime_edge_desc">Garis luar toon ditambah penajaman.</string> + + <string name="gpuinfo_not_adreno">Bukan GPU Adreno</string> + <string name="gpuinfo_supported">Didukung</string> + <string name="gpuinfo_experimental">Eksperimental: drivernya adalah build uji</string> + <string name="gpuinfo_below_tested">Di bawah perangkat keras yang diuji (Adreno 725 dan lebih baru): mungkin tidak dapat berjalan</string> + <string name="gpuinfo_unsupported">Tidak didukung: DroidDeck memerlukan GPU Adreno (Snapdragon)</string> + + <string name="turnip_rate_limited">Batas kecepatan GitHub tercapai - coba lagi nanti</string> + <string name="turnip_github_http">GitHub merespons HTTP %1$d</string> + <string name="turnip_download_http">unduhan merespons HTTP %1$d</string> + <string name="turnip_list_predates_checksums">Daftar driver ini dibuat sebelum adanya checksum - segarkan dan coba lagi</string> + <string name="turnip_checksum_mismatch">Checksum tidak cocok - unduhan dibuang</string> + <string name="turnip_not_adrenotools">Bukan driver AdrenoTools: %1$s</string> + <string name="turnip_reject_no_meta">tidak ada meta.json</string> + <string name="turnip_reject_meta_unreadable">meta.json tidak dapat dibaca (%1$s)</string> + <string name="turnip_reject_linux_driver">ini adalah driver runtime Linux (impor di bagian “Linux runtime driver”)</string> + <string name="turnip_reject_wayland">ini adalah driver gim Wayland, yang tidak digunakan oleh aplikasi ini</string> + <string name="turnip_reject_no_library_name">meta.json tidak menyebutkan libraryName, sehingga tidak ada yang dapat diserahkan ke AdrenoTools</string> + <string name="turnip_reject_library_missing">meta.json menyebutkan %1$s, yang tidak ada di dalam zip</string> + <string name="turnip_reject_not_aarch64">%1$s bukan library bersama ELF AArch64 64-bit</string> + <string name="turnip_reject_glibc">%1$s menautkan glibc - Turnip “-Linux”, yang termasuk di bagian “Linux runtime driver”</string> + <string name="lvd_no_library">Tidak ada libvulkan_freedreno*.so dalam zip ini. Zip Turnip Android (AdrenoTools) atau -Wayland bukan driver runtime Linux.</string> + <string name="lvd_not_aarch64">%1$s bukan library bersama ELF AArch64 64-bit.</string> + <string name="lvd_not_glibc">%1$s bukan driver glibc - ia menautkan libc milik Android. Runtime Linux memerlukan zip “-Linux”; Turnip biasa atau “-Wayland” sama sekali tidak dapat dimuat oleh klien Steam.</string> + <string name="bundle_no_manifest">Bukan paket driver: tidak ada manifest.json yang menyebutkan driver android dan linux</string> + <string name="bundle_entry_outside">Bukan paket driver: %1$s mengarah ke luar paket</string> + <string name="bundle_too_large">Bukan paket driver: ukurannya setelah diekstrak lebih dari %1$d MB</string> + + <string name="gstore_sd_card">Kartu SD</string> + <string name="gstore_folder">Folder</string> + + <string name="logshare_chooser">Bagikan log sesi</string> + + <string name="hdr_needs_android_10">memerlukan Android 10 atau yang lebih baru (lapisan tampilan dengan ruang warna)</string> + <string name="hdr_none">tampilan ini melaporkan tidak ada dukungan HDR</string> + <string name="hdr_not_hdr10">tampilan ini mendukung %1$s, bukan HDR10</string> + + <string name="gpu_this_gpu">GPU ini</string> + <string name="phantom_title_on">Batas proses turunan aktif</string> + <string name="phantom_title_unset">Batas proses turunan belum diatur</string> + <string name="phantom_title_unreadable">Batas proses turunan tidak dapat diperiksa</string> + <string name="phantom_title_off">Batas proses turunan nonaktif</string> + <string name="phantom_title_not_required">Batas proses turunan tidak diperlukan</string> + <string name="phantom_instructions_fix">%1$s Jika itu tidak memungkinkan, sambungkan perangkat ini ke komputer dengan ADB dan jalankan perintah di bawah. Kembali ke sini dan periksa lagi.</string> + <string name="phantom_instructions_off">Sesi Steam dapat dimulai.</string> + <string name="phantom_instructions_not_applicable">Versi Android ini tidak menggunakan batas ini.</string> + <string name="phantom_fix_toggle">Aktifkan “Disable child process restrictions” di Opsi developer, dan biarkan Opsi developer tetap aktif setelahnya.</string> + <string name="phantom_fix_no_toggle">Versi Android ini tidak memiliki sakelar untuk itu. Gunakan Debugging nirkabel untuk mengubahnya dari perangkat ini.</string> + <string name="upd_block_no_apk">Build ini tidak memiliki unduhan untuk salinan DroidDeck ini.</string> + <string name="upd_block_version_code">Android tidak akan menginstalnya di atas build ini karena versionCode %1$d lebih rendah dari %2$d yang terinstal.</string> + <string name="upd_fail_downgrade">Android tidak akan menginstal versi yang lebih lama di atas versi yang lebih baru. Tetap gunakan build ini sampai saluran tersebut menyusul.</string> + <string name="upd_fail_signature">Salinan DroidDeck ini ditandatangani secara berbeda (build lokal?), sehingga Android tidak akan memperbaruinya di tempat.</string> + <string name="upd_fail_storage">Ruang kosong tidak cukup untuk pembaruan ini.</string> + <string name="upd_fail_aborted">Pembaruan dibatalkan.</string> + <string name="upd_fail_with_message">Pembaruan tidak terinstal: %1$s</string> + <string name="upd_fail_generic">Pembaruan tidak terinstal.</string> + <string name="saves_layout_zip">zip %1$s</string> + <string name="saves_zip">zip</string> + <string name="saves_not_launched">%1$s belum pernah dijalankan</string> + <string name="saves_none_found">Tidak ditemukan file simpanan untuk %1$s</string> + <string name="saves_not_save_zip">%1$s tidak memiliki folder drive_c, sehingga bukan zip simpanan GameHub atau Winlator</string> + <string name="gstore_cannot_create">tidak dapat membuat folder di %1$s</string> + <string name="gstore_cannot_write">tidak dapat menulis di %1$s</string> + <string name="gstore_cannot_write_detail">tidak dapat menulis di %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">Folder tidak tersedia</string> + <string name="gfiles_name_taken">File lain sudah menggunakan %1$s</string> + <string name="gfiles_art_unavailable">Folder artwork tidak tersedia</string> + <string name="gfiles_unrelated_inventory">Folder memiliki inventaris sinkronisasi yang tidak terkait</string> + <string name="gfiles_remove_failed">Tidak dapat menghapus %1$s</string> + <string name="gfiles_outside_folder">File berada di luar folder ekspor</string> + <string name="cmgr_fex_unnamed">FEX (build tanpa nama versi)</string> + <string name="cmgr_no_profile">tidak ada profile.json di %1$s</string> + <string name="cmgr_not_component_package">%1$s bukan paket komponen Linux (tipe %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s kosong</string> + <string name="mode_storage_auto_short">Otomatis</string> + <string name="appupd_catalog_rejected">Katalog pembaruan gagal dalam pemeriksaannya (%1$s)</string> + <string name="main_adb_clip_label">Setelan proses turunan DroidDeck</string> + <string name="setup_signed_in_offline_unnamed">Masuk · mode offline</string> + <string name="setup_signed_in_unnamed">Masuk</string> + <string name="driver_tag_bundled">BAWAAN</string> + <string name="driver_tag_downloaded">DIUNDUH</string> + <string name="driver_tag_imported">DIIMPOR</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Tampilan sesi</string> + <string name="display_resolution">Resolusi</string> + <string name="display_game_fit">Penyesuaian gim 16:9</string> + <string name="display_game_fit_hint">Cara gim 16:9 disesuaikan dengan tampilan ini. Berlaku pada sesi berikutnya.</string> + <string name="display_game_fit_unavailable">Pilih Sesuaikan layar atau ukuran khusus yang lebih sempit dari 16:9.</string> + <string name="display_preserve">Pertahankan proporsi</string> + <string name="display_stretch">Bentangkan ke tampilan</string> + <string name="display_image_scaling">Penskalaan gambar</string> + <string name="display_filter">Filter penskalaan</string> + <string name="display_filter_hint">Digunakan bersama oleh Steam dan Desktop.</string> + <string name="display_filter_live">Berlaku langsung. Digunakan bersama oleh Steam dan Desktop.</string> + <string name="display_filter_note">FSR, GSR, dan NIS berlaku saat memperbesar gambar sesi. Hanya penajaman juga berfungsi tanpa pembesaran.</string> + <string name="display_sharpness">Ketajaman filter</string> + <string name="display_frame_rate">Frame rate</string> + <string name="display_look_filter">%1$s Mengatur skala ke %2$s.</string> + <string name="display_match_screen">Sesuaikan layar (%1$d×%2$d)</string> + <string name="display_follow_screen">Ikuti layar (ukuran berubah saat dilipat atau dibuka)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Tampilkan detail perangkat</string> + <string name="device_info_hide">Sembunyikan detail perangkat</string> + <string name="device_info_loading">Membaca perangkat…</string> + <string name="device_info_copy">Salin</string> + <string name="device_info_copy_hint">Salin detail ini ke dalam laporan bug.</string> + <string name="device_info_copied">Disalin.</string> + <string name="force_ssbs_title">Paksa SSBS</string> + <string name="force_ssbs_hint">Menjaga setelan kecepatan CPU yang terus dinonaktifkan oleh Wine. Dapat membuat gim Windows lebih cepat di Snapdragon 8 Elite; perubahan kecil terlihat pada chip yang lebih lama. Berlaku pada peluncuran gim berikutnya.</string> + + <string name="rail_stores">Toko</string> + <string name="source_steam">Steam</string> + <string name="source_added">Khusus</string> + <string name="setup_stores">Toko</string> + <string name="setup_stores_open_on">Buka toko di</string> + <string name="setup_stores_speed">Tingkat kecepatan unduhan</string> + <string name="setup_stores_engine">Mesin toko</string> + <string name="setup_stores_engine_chip_ready">● Mesin siap</string> + <string name="setup_stores_engine_chip_missing">Belum dibuat</string> + <string name="stores_tier_balanced">Seimbang</string> + <string name="stores_tier_fast">Cepat</string> + <string name="stores_tier_max">Maks</string> + <string name="stores_title">Toko</string> + <string name="stores_settings">Setelan toko</string> + <string name="stores_settings_eyebrow">Setelan</string> + <string name="stores_chip_signed_out">(belum masuk)</string> + <string name="stores_tab_store">Toko</string> + <string name="stores_tab_installed">Terinstal</string> + <string name="stores_tab_library">Pustaka</string> + <string name="stores_tab_all">Semua</string> + <string name="stores_tab_downloads">Unduhan</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Masuk</string> + <string name="stores_signout_named">%1$s · keluar</string> + <string name="stores_signout_title">Keluar dari %1$s?</string> + <string name="stores_signout_note">Gim yang terinstal tetap ada. Pustaka diambil lagi saat Anda masuk kembali.</string> + <string name="stores_signout_stay">Tetap masuk</string> + <string name="stores_signout_go">Keluar</string> + <string name="stores_search_catalog">Cari katalog %1$s…</string> + <string name="stores_search_installed">Cari gim yang terinstal…</string> + <string name="stores_search_library">Cari pustaka Anda…</string> + <string name="stores_search_open">Cari</string> + <string name="stores_installed_empty">Belum ada yang terinstal</string> + <string name="stores_library_loading">Memuat</string> + <string name="stores_nothing_matches">Tidak ada gim</string> + <string name="stores_shelves_loading">Memuat toko…</string> + <string name="stores_amazon_no_catalog">Tidak ada katalog</string> + <string name="stores_shelf_new">Yang baru</string> + <string name="stores_shelf_deals">Penawaran</string> + <string name="stores_shelf_free">Gim gratis</string> + <string name="stores_shelf_trending">Sedang tren</string> + <string name="stores_shelf_library">Pustaka Anda</string> + <string name="stores_play">Mainkan</string> + <string name="stores_install">Instal</string> + <string name="stores_resume_install">Lanjutkan instalasi</string> + <string name="stores_dl_interrupted">Terhenti</string> + <string name="stores_install_size">Instal · %1$s</string> + <string name="stores_get_free">Dapatkan gratis</string> + <string name="stores_view_on">Lihat di %1$s</string> + <string name="stores_buy_price">Beli · %1$s</string> + <string name="stores_installed_chip">Terinstal</string> + <string name="stores_in_library">Dalam pustaka</string> + <string name="stores_free">Gratis</string> + <string name="stores_resume">Lanjutkan</string> + <string name="stores_refresh">Segarkan</string> + <string name="stores_show_mature">Tampilkan konten dewasa</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Offline</string> + <string name="epic_card_none">Nonaktif</string> + <string name="epic_eos">Masuk Epic (EOS)</string> + <string name="epic_offline">Jalankan secara offline</string> + <string name="epic_resolve">Selesaikan masuk Epic</string> + <string name="store_cloud_saves">Simpanan cloud</string> + <string name="cloud_upload">Unggah ke cloud</string> + <string name="cloud_download">Unduh dari cloud</string> + <string name="cloud_none">Tidak ada simpanan cloud</string> + <string name="cloud_conflict">Konflik cloud</string> + <string name="cloud_conflict_files">Konflik cloud · %1$d file</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Pertahankan cloud</string> + <string name="cloud_keep_local">Pertahankan lokal</string> + <string name="cloud_checking">Memeriksa cloud…</string> + <string name="cloud_last_sync">Sinkronisasi terakhir %1$s</string> + <string name="cloud_never">Belum disinkronkan</string> + <string name="cloud_done">%1$d file</string> + <string name="stores_eyebrow_installed">terinstal</string> + <string name="stores_eyebrow_library">dalam pustaka Anda</string> + <string name="stores_eyebrow_store">toko</string> + <string name="achievements_title">Pencapaian</string> + <string name="achievements_count">%1$d dari %2$d terbuka</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Terbuka · %1$d</string> + <string name="achievements_group_locked">Terkunci · %1$d</string> + <string name="achievements_group_hidden">Tersembunyi · %1$d</string> + <string name="achievements_hidden">Pencapaian tersembunyi</string> + <string name="achievements_hidden_detail">Detail ditampilkan setelah terbuka.</string> + <string name="achievements_all">Semua pencapaian terbuka</string> + <string name="achievements_reading">Memuat…</string> + <string name="achievements_signed_out">Masuk ke %1$s untuk melihat pencapaianmu.</string> + <string name="achievements_unreachable">Tidak dapat menjangkau %1$s. Periksa lagi saat sudah online.</string> + <string name="achievements_none">%1$s tidak mencantumkan pencapaian untuk game ini.</string> + <string name="achievements_show_all">Tampilkan semua %1$d</string> + <string name="achievements_dialog_title">Pencapaian · %1$s</string> + <string name="stores_in_steam">✓ Di Steam</string> + <string name="stores_uninstall">Hapus instalasi</string> + <string name="stores_uninstalling">Menghapus instalasi…</string> + <string name="stores_uninstall_title">Hapus instalasi %1$s?</string> + <string name="stores_uninstall_body_size">Membebaskan %1$s dan menghapusnya dari Steam.</string> + <string name="stores_uninstall_body">Menghapus filenya dan pintasan Steam-nya.</string> + <string name="stores_launch_not_registered">Steam akan mendeteksi gim ini saat berikutnya dimulai; jalankan dari sana, atau mulai Steam sekarang.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Pilihan Steam per gim</string> + <string name="stores_card_audio">Audio</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Komponen</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Mesin</string> + <string name="stores_card_engine_value">%1$s · asli</string> + <string name="stores_card_engine_java">%1$s · bawaan</string> + <string name="stores_how_it_runs">Cara menjalankannya</string> + <string name="stores_kv_fetched">Diambil dari</string> + <string name="stores_kv_fetched_value">%1$s, dengan akun Anda</string> + <string name="stores_kv_installed_to">Terinstal di</string> + <string name="stores_kv_installed_to_value">Penyimpanan gim (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Dijalankan oleh</string> + <string name="stores_kv_launched_by_value">Steam dan Proton, jalur yang sama seperti gim Steam</string> + <string name="stores_kv_launched_by_epic">Steam dan Proton · Masuk Epic setiap peluncuran, overlay EOS nonaktif</string> + <string name="stores_kv_launched_by_amazon">Steam dan Proton · Variabel Amazon Games Services diatur</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">ditambahkan ke Steam saat instalasi (UI Deck + Steam Input)</string> + <string name="stores_downloads_empty">Tidak ada unduhan</string> + <string name="stores_dl_paused">Dijeda</string> + <string name="stores_dl_queued">Dalam antrean</string> + <string name="stores_dl_queued_at">Dalam antrean · #%1$d</string> + <string name="stores_dl_cancelled">Dibatalkan</string> + <string name="stores_dl_failed">Gagal</string> + <string name="stores_dl_failed_reason">Gagal · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Jeda</string> + <string name="stores_dl_resume">Lanjutkan</string> + <string name="stores_dl_cancel">Batal</string> + <string name="stores_dl_retry">Coba lagi</string> + <string name="stores_dl_clear">Hapus</string> + <string name="stores_dl_clear_all">Hapus yang selesai</string> + <string name="stores_stage_manifest">Manifest</string> + <string name="stores_stage_download">Unduhan</string> + <string name="stores_stage_verify">Verifikasi</string> + <string name="stores_stage_install">Instal</string> + <string name="stores_stage_done">Selesai</string> + <string name="stores_stage_checking">Memeriksa</string> + <string name="stores_stage_downloading">Mengunduh</string> + <string name="stores_dl_cancel_confirm">Hapus unduhan?</string> + <string name="stores_dl_failed_kept">Gagal · %1$s disimpan</string> + <string name="stores_stage_verifying">Memverifikasi</string> + <string name="stores_stage_installing">Menginstal</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d file</string> + <string name="stores_download_manager">Manajer unduhan</string> + <string name="stores_parallel">Unduhan sekaligus</string> + <string name="stores_install_where">Instal ke</string> + <string name="stores_install_title">Instal %1$s:</string> + <string name="stores_target_internal">Internal</string> + <string name="stores_target_free">%1$s tersedia</string> + <string name="stores_notification_channel">Unduhan toko</string> + <string name="stores_notification_channel_hint">Menampilkan progres unduhan gim dan menjaga unduhan tetap berjalan di latar belakang</string> + <string name="stores_notification_one">Mengunduh</string> + <string name="stores_notification_many">%1$d unduhan</string> + <string name="stores_notification_waiting">Menyiapkan…</string> + <string name="stores_notification_done_channel">Unduhan toko selesai</string> + <string name="stores_notification_installed">%1$s terinstal</string> + <string name="stores_notification_failed">Unduhan %1$s gagal</string> + <string name="stores_notification_delete">Hapus</string> + <string name="stores_notification_keep">Pertahankan</string> + <string name="stores_login_title">Masuk ke %1$s</string> + <string name="stores_login_provider_title">Masuk dengan penyedia Anda</string> + <string name="stores_login_reload">Muat ulang</string> + <string name="stores_login_close">Tutup</string> + <string name="stores_login_retry">Coba lagi</string> + <string name="stores_login_finishing">Menyelesaikan proses masuk…</string> + <string name="stores_login_error_network">Halaman masuk tidak dapat dimuat: %1$s</string> + <string name="stores_login_error_idp_blocked">Penyedia ini menolak proses masuk dari halaman tersemat. Masuk dengan kata sandi toko Anda sebagai gantinya.</string> + <string name="stores_login_error_http">Halaman masuk merespons dengan kesalahan (HTTP %1$d).</string> + <string name="stores_login_error_ssl">Koneksi ke halaman masuk tidak aman.</string> + <string name="stores_login_error_verification">Proses masuk tidak dapat diverifikasi. Coba lagi.</string> + <string name="stores_login_error_generic">Proses masuk gagal. Coba lagi.</string> + <string name="common_close">Tutup</string> + <string name="common_back">Kembali</string> + <string name="common_edit">Edit</string> + <string name="games_add_all">Tambahkan semua gim di folder ini</string> + <string name="games_added_none">Tidak ada yang baru di folder ini</string> + <string name="edit_name">Nama</string> + <string name="edit_target">Target</string> + <string name="edit_start_in">Mulai di</string> + <string name="edit_launch_options">Opsi peluncuran</string> + <string name="edit_artwork">Artwork</string> + <string name="edit_other_exe">Exe lain…</string> + <string name="edit_remove">Hapus</string> + <string name="art_cover">Sampul</string> + <string name="art_background">Latar belakang</string> + <string name="art_logo">Logo</string> + <string name="art_icon">Ikon</string> + <string name="art_src_folder">Folder gim</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">File Anda</string> + <string name="art_src_exe">Dari exe</string> + <string name="art_src_auto">Otomatis</string> + <string name="art_pick_image">Pilih gambar…</string> + <string name="art_pick_title">Pilih gambar: %1$s</string> + <string name="art_reset">Setel ulang ke otomatis</string> + <plurals name="games_added_n"> + <item quantity="other">%1$d gim ditambahkan</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="other">%1$d sudah ada di Games</item> + </plurals> + <string name="hop_back">B · Kembali ke %1$s</string> + <string name="hop_from_games">gim ini</string> + <string name="hop_from_play">Setelan Main</string> + <string name="sheet_graphics_drivers">Driver grafis</string> + <string name="comp_this_proton">Proton ini</string> + <string name="comp_every_proton">Setiap Proton</string> + <string name="comp_for_every_proton">Untuk setiap Proton</string> + <string name="comp_available_count">Tersedia · %1$d</string> + <string name="comp_checked">Diperiksa %1$s</string> + <string name="comp_check_all">Periksa komponen dan driver baru</string> + <string name="comp_get">Dapatkan</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton, dan FEX di bawah ini milik Proton ini</string> + <string name="comp_device_tested">%1$s · diuji</string> + <string name="comp_device_untested">%1$s · belum diuji</string> + <string name="comp_device_unsupported">%1$s · tidak didukung</string> + <string name="gpu_auto_keeps">Menjaga pasangan yang direkomendasikan tetap terbaru</string> + <string name="gpu_bar_auto">Otomatis · %1$s</string> + <string name="gpu_separately">Runtime dan driver tampilan secara terpisah</string> + <string name="gpu_use_title">Gunakan %1$s?</string> + <string name="proton_remove_title">Hapus %1$s?</string> + <string name="setup_tab_diagnostics">Diagnostik</string> + <string name="setup_ready">Siap bermain</string> + <string name="setup_not_ready">Belum siap bermain</string> + <string name="setup_ready_runtime">runtime %1$s</string> + <string name="setup_ready_limit_off">batas proses nonaktif</string> + <string name="setup_ready_signed_in">masuk sebagai %1$s</string> + <string name="setup_needs_you">Saat ada yang memerlukan Anda</string> + <string name="setup_group_look">Tampilan</string> + <string name="setup_group_device">Perangkat ini</string> + <string name="setup_group_art">Artwork pustaka</string> + <string name="flathub_eyebrow">APLIKASI LINUX</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Aplikasi desktop untuk desktop Linux, dibuka dari Desktop.</string> + <string name="flathub_setup">Siapkan Flathub</string> + <string name="flathub_remove_title">Hapus %1$s?</string> + <string name="flathub_remove_note">Setelan dan filenya tetap disimpan.</string> + <string name="flathub_keep">Pertahankan</string> + <string name="desktop_size">Ukuran di disk</string> + <string name="desktop_uninstall_title">Hapus instalasi desktop?</string> + <string name="desktop_all_systems">Semua</string> + <string name="gog_achievements">Pencapaian GOG</string> + <string name="gog_achievements_hint">Menyinkronkan pencapaian untuk game GOG yang didukung. Berlaku saat peluncuran berikutnya.</string> + <string name="gog_achievements_off">Pencapaian nonaktif</string> + <string name="gog_achievements_on">Pencapaian aktif</string> + <string name="gog_achievements_signin">Masuk lagi ke GOG</string> + <string name="gog_achievements_uploaded">Pencapaian terunggah</string> + <string name="gog_achievements_pending">Unggahan pencapaian tertunda</string> + <string name="gog_achievements_enable_store">Aktifkan pencapaian GOG di setelan Stores</string> + <string name="gog_achievements_unavailable">Pencapaian tidak tersedia; periksa log sesi</string> + <string name="achievement_unlocked">Pencapaian terbuka</string> + <string name="touch_paste">Tempel</string> + <string name="touch_dpad">Tombol arah</string> + <string name="touch_left_stick">Stik kiri</string> + <string name="touch_right_stick">Stik kanan</string> + <string name="touch_menu">Menu</string> + <string name="touch_keyboard">Keyboard</string> + <string name="touch_dpad_up">Tombol arah atas</string> + <string name="touch_dpad_down">Tombol arah bawah</string> + <string name="touch_dpad_left">Tombol arah kiri</string> + <string name="touch_dpad_right">Tombol arah kanan</string> + <string name="touch_title">Kontrol sentuh</string> + <string name="touch_controller">Kontroler sentuh</string> + <string name="touch_mouse">Mouse</string> + <string name="touch_on">Aktif</string> + <string name="touch_off">Nonaktif</string> + <string name="touch_speed_slow">Lambat</string> + <string name="touch_speed_normal">Normal</string> + <string name="touch_speed_fast">Cepat</string> + <string name="touch_edit_layout">Edit tata letak</string> + <string name="touch_droiddeck_menu">Menu DroidDeck</string> + <string name="touch_drag_hint">Seret kontrol ke layar</string> + <string name="touch_arrange">Atur</string> + <string name="touch_discard">Buang</string> + <string name="touch_saving">Menyimpan…</string> + <string name="touch_icon">Ikon</string> + <string name="touch_narrower">L−</string> + <string name="touch_wider">L+</string> + <string name="touch_shorter">T−</string> + <string name="touch_taller">T+</string> + <string name="touch_hide">Sembunyikan</string> + <string name="touch_color_opacity">Warna dan opasitas…</string> + <string name="touch_copy_layout">Salin tata letak ini</string> + <string name="touch_layout_copied">Tata letak disalin; tempelkan ke tata letak sentuh game apa pun</string> + <string name="touch_paste_layout">Tempel tata letak yang disalin</string> + <string name="touch_reset_layout">Setel ulang ke default Steam</string> + <string name="touch_no_binding">Kontrol ini tidak memiliki pengikatan di Steam untuk diberi ikon</string> + <string name="touch_icon_for">Ikon untuk %1$s</string> + <string name="touch_layout_color">Warna tata letak sentuh</string> + <string name="touch_done">Selesai</string> + <string name="touch_save_failed">Tata letak sentuh tidak dapat disimpan</string> + <string name="touch_saved">Tata letak sentuh disimpan ke Steam</string> + <string name="touch_app_id">App %1$d</string> + <string name="touch_label_hint">Label (opsional)</string> + <string name="touch_icons_missing">Ikon Steam tidak ditemukan</string> + <plurals name="touch_icons_found"> + <item quantity="other">%1$d ikon dari Steam</item> + </plurals> + <string name="touch_icon_color">Warna ikon</string> + <string name="touch_button_color">Warna tombol</string> + <string name="touch_no_icon">Tidak ada ikon</string> + + <string name="stores_sync_sign_in">Memeriksa status masuk…</string> + <string name="stores_sync_library">Mengambil pustaka kamu…</string> + <string name="stores_sync_game_list">Mengambil daftar game…</string> + <string name="stores_sync_details">Memuat detail game… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="other">Menyinkronkan %1$d game…</item> + </plurals> + <string name="stores_sync_progress">Menyinkronkan… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Mengambil gambar sampul…</string> + <string name="stores_sync_failed">Sinkronisasi gagal</string> + <string name="stores_sync_no_games">Tidak ada game yang ditemukan di pustaka %1$s kamu</string> + <string name="stores_sync_amazon_no_answer">Amazon Games tidak merespons untuk pustaka kamu: segarkan, atau masuk lagi</string> + <string name="stores_sync_gog_unreachable">Tidak dapat menghubungi GOG</string> + <string name="stores_sync_gog_unreadable">Pustaka GOG kamu tidak dapat dibaca</string> + <string name="stores_registering">Mendaftar ke Steam…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Mode Cardboard</string> + <string name="cardboard_mode_hint">Tampilan mata kiri dan kanan untuk kacamata VR ponsel.</string> + <string name="cardboard_source">Gambar</string> + <string name="cardboard_source_screen">Layar virtual</string> + <string name="cardboard_source_sbs">3D berdampingan</string> + <string name="cardboard_viewer">Kacamata</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Tanpa lensa</string> + <string name="cardboard_ipd">Jarak mata</string> + <string name="cardboard_screen_width">Lebar layar</string> + <string name="cardboard_screen_distance">Jarak layar</string> + <string name="cardboard_head_tracking">Pelacakan kepala</string> + <string name="cardboard_no_sensor">Perangkat ini tidak punya sensor rotasi.</string> + <string name="cardboard_recenter">Posisikan ulang tampilan</string> + <string name="cardboard_recenter_button">Posisikan ulang</string> + <string name="cardboard_steamvr">SteamVR (eksperimental)</string> + <string name="cardboard_steamvr_install">Pasang</string> + <string name="cardboard_steamvr_install_hint">Mengunduh SteamVR ARM64 dari Valve (571 MB).</string> + <string name="cardboard_steamvr_installing">Mengunduh dan mengekstrak SteamVR…</string> + <string name="cardboard_steamvr_start">Mulai</string> + <string name="cardboard_steamvr_ready_hint">Menjalankan SteamVR dengan kepala Anda sebagai headset; gambar beralih ke 3D berdampingan.</string> + <string name="cardboard_steamvr_stop">Hentikan</string> + <string name="cardboard_steamvr_running_hint">SteamVR sedang berjalan.</string> + <string name="cardboard_steamvr_unavailable">Tidak tersedia di sesi ini.</string> + <string name="cardboard_steamvr_failed">SteamVR tidak merespons; periksa log sesi.</string> + <string name="drawer_component_downloads">Unduh komponen</string> + <string name="drawer_component_downloads_hint">Unduhan dipakai bersama halaman Komponen. Pilih versi yang sudah diunduh di atas untuk memakainya pada peluncuran gim berikutnya.</string> + <string name="drawer_component_choose_download">Pilih versi untuk diunduh</string> + <string name="stores_fact_developer">Pengembang</string> + <string name="stores_fact_publisher">Penerbit</string> + <string name="stores_fact_released">Rilis</string> + <string name="stores_hltb_main">Utama</string> + <string name="stores_hltb_extras">+ Ekstra</string> + <string name="stores_hltb_completionist">Tuntas</string> + <string name="stores_hltb_hours">%1$s j</string> + <string name="stores_hltb_minutes">%1$d mnt</string> +</resources> diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml new file mode 100644 index 000000000..26e984240 --- /dev/null +++ b/app/src/main/res/values-it/strings.xml @@ -0,0 +1,2260 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>Al primo avvio viene scaricato Steam. Potrebbero volerci alcuni minuti.</item> + <item>Per impostazione predefinita, uscire dall\'app mette in pausa Steam; al tuo ritorno riprende da dove si era interrotto.</item> + <item>Il tuo controller viene visto come un pad Xbox 360. In alternativa, usa i controlli su schermo.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>Al primo avvio viene configurato il desktop. Potrebbero volerci alcuni minuti.</item> + <item>Per impostazione predefinita, uscire dall\'app mette in pausa la sessione; al tuo ritorno riprende da dove si era interrotta.</item> + <item>Il touchscreen funziona come un touchpad: trascina per spostare il puntatore, tocca per fare clic.</item> + <item>Il tuo controller viene visto come un pad Xbox 360. In alternativa, usa i controlli su schermo.</item> + </string-array> + <string name="session_channel">Sessione</string> + <string name="session_channel_description">Visibile mentre è aperta una sessione Steam o desktop</string> + <string name="session_notification">Steam è in esecuzione</string> + <string name="session_notification_downloads">Download di Steam in corso</string> + <string name="session_notification_paused">Sessione in pausa - tocca Riprendi quando sei pronto</string> + <string name="resume_session">Riprendi</string> + <string name="suspend_now">Sospendi ora</string> + <string name="stop_session">Termina sessione</string> + <string name="frame_gen_title">Generazione di frame</string> + <string name="frame_gen_hint">Seleziona la modalità di generazione di frame</string> + <string name="frame_gen_off">Disattivata</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG adattivo %1$d</string> + <string name="lsfg_needs_steam">Richiede Lossless Scaling su Steam</string> + <string name="lsfg_needs_install">Installa Lossless Scaling in Steam o importa Lossless.dll</string> + <string name="lsfg_import">Importa Lossless.dll…</string> + <string name="lsfg_using_steam">In uso la copia di Steam</string> + <string name="lsfg_using_import">In uso una copia importata</string> + <string name="lsfg_pick_title">Scegli Lossless.dll</string> + <string name="lsfg_imported">Lossless.dll importato</string> + <string name="lsfg_import_unchanged">Questo Lossless.dll è già in uso</string> + <string name="lsfg_import_not_owned">Questo account Steam non possiede Lossless Scaling</string> + <string name="lsfg_import_invalid">Questo file non è il Lossless.dll di Lossless Scaling</string> + <string name="lsfg_import_failed">Impossibile preparare LSFG da questo Lossless.dll</string> + <string name="lsfg_problem_missing">LSFG richiede Lossless Scaling da Steam o un Lossless.dll importato</string> + <string name="lsfg_problem_failed">LSFG non è riuscito a preparare gli shader da Lossless.dll</string> + <string name="hud_fg_unsupported">%1$s: non supportato</string> + <string name="hud_fg_failed">%1$s: errore del motore</string> + <string name="hud_fg_needs_lossless">%1$s: richiede Lossless Scaling</string> + <string name="hud_fg_starting">%1$s %2$s · avvio</string> + <string name="second_library_import_hint">Aggiunge una libreria di Steam e la analizza alla ricerca di giochi Windows. I giochi posseduti usano le loro voci di Steam; gli altri diventano collegamenti non di Steam.</string> + <string name="game_env_title">Ambiente di gioco</string> + <string name="game_env_hint">Variabili d\'ambiente per i giochi Proton.</string> + <string name="game_env_edit">Modifica</string> + <string name="game_env_shared">Tutti i giochi Proton</string> + <string name="game_env_scope">Profilo</string> + <string name="game_env_app_id">ID app di Steam</string> + <string name="game_env_open">Apri</string> + <string name="game_env_profile">Gioco %1$s</string> + <string name="game_env_add">Aggiungi variabile</string> + <string name="game_env_name">Nome variabile</string> + <string name="game_env_value">Valore</string> + <string name="game_env_save">Salva</string> + <string name="game_env_remove">Rimuovi</string> + <string name="game_env_restore">Eredita di nuovo</string> + <string name="game_env_reset">Cancella le personalizzazioni dell\'ambiente</string> + <string name="game_env_unset">Annulla impostazione per questo profilo</string> + <string name="game_env_empty">Nessuna variabile impostata.</string> + <string name="game_env_inherited">Ereditata</string> + <string name="game_env_custom">Variabile personalizzata</string> + <string name="game_env_error">Impossibile caricare o salvare. Controlla lo spazio libero e riprova.</string> + <string name="game_env_invalid">Usa un nome valido e un valore di massimo 8192 caratteri.</string> + <string name="game_env_unused">Il runtime Proton standard non legge questa variabile.</string> + <string name="game_env_loading">Caricamento…</string> + <string name="game_env_feature">Sostituisce il feature level D3D12. 12_2 dichiara le funzionalità di DirectX 12 Ultimate; quelle non supportate possono causare errori. Lascia vuota per il rilevamento automatico.</string> + <string name="game_env_shader">Sostituisce lo shader model D3D12. 6_9 richiede un VKD3D abbastanza recente e funzionalità GPU adeguate. Lascia vuota per il rilevamento automatico.</string> + <string name="game_env_cache">Mantieni attiva la cache degli shader di Mesa per ridurre le compilazioni ripetute. false attiva la cache.</string> + <string name="game_env_cache_size">Dimensione massima della cache degli shader di Mesa, ad esempio 1G. Una cache più grande occupa più spazio di archiviazione.</string> + <string name="game_env_vkd3d_config">Opzioni VKD3D per D3D12. nodxr disattiva il ray tracing; usalo solo per un gioco che lo richiede.</string> + <string name="game_env_proton_log">Imposta 1 per un log diagnostico di Proton. Il log può ridurre le prestazioni e occupare spazio.</string> + <string name="game_env_wined3d">Imposta 1 per usare WineD3D basato su OpenGL invece di DXVK, per test di compatibilità D3D9–11.</string> + <string name="game_env_no_ntsync">Imposta 1 per non usare ntsync (droiddeck-ntsync o un driver del kernel) in questo gioco; userà droiddeck-esync se esiste un pacchetto, altrimenti droiddeck-fsync.</string> + <string name="game_env_no_esync">Imposta 1 per disattivare droiddeck-esync in questo gioco; userà invece droiddeck-fsync, o droiddeck-ntsync se anche fsync è disattivato.</string> + <string name="game_env_no_fsync">Imposta 1 per non usare droiddeck-fsync in questo gioco; userà droiddeck-esync se esiste un pacchetto, altrimenti droiddeck-ntsync.</string> + <string name="game_env_fsync_first">Imposta 1 per eseguire questo gioco con droiddeck-fsync, o 0 per dare priorità a droiddeck-esync se c\'è un pacchetto adatto al suo Proton.</string> + <string name="game_env_xalia">Imposta 0 per disattivare l\'assistente di navigazione con gamepad di Proton in questo gioco.</string> + <string name="game_env_hud">Overlay DXVK per D3D9–11. fps mostra il frame rate; se vuota, l\'overlay è nascosto.</string> + <string name="game_env_dxvk_config">Impostazioni DXVK separate da punto e virgola. Le build recenti usano dxvk.maxFrameRate per limitare i frame; per giocare normalmente è meglio il limitatore di frame della sessione.</string> + <string name="game_env_vkd3d_limit">Limite di frame rate D3D12. 0 disattiva il limite.</string> + <string name="game_env_glthread">Threading OpenGL di Mesa. true può aiutare i giochi OpenGL limitati dalla CPU; provalo gioco per gioco.</string> + <string name="sync_backend_title">Sincronizzazione Wine</string> + <string name="sync_backend_hint">Come i giochi mantengono sincronizzati i loro thread. Si applica dalla prossima sessione.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">Preset FEX</string> + <string name="fex_next_launch">Si applica al prossimo avvio del gioco.</string> + <string name="game_settings_title">Giochi</string> + <string name="fex_default_label">Predefinito FEX</string> + <string name="fex_default_detail">Usa le impostazioni predefinite di FEX.</string> + <string name="fex_stability_label">Stabilità</string> + <string name="fex_stability_detail">Impostazioni più sicure e più lente.</string> + <string name="fex_compatibility_label">Compatibilità</string> + <string name="fex_compatibility_detail">TSO completo con multiblock.</string> + <string name="fex_intermediate_label">Intermedio</string> + <string name="fex_intermediate_detail">TSO bilanciato con precisione x87 ridotta.</string> + <string name="fex_performance_label">Prestazioni</string> + <string name="fex_performance_detail">TSO del tutto disattivato; multiblock.</string> + <string name="fex_performance_tso_label">Prestazioni + TSO</string> + <string name="fex_performance_tso_detail">Impostazioni Prestazioni con il TSO principale attivo.</string> + <string name="fex_extreme_label">Estremo</string> + <string name="fex_extreme_detail">Disattiva i controlli sul codice automodificante. Giochi con JIT e DRM potrebbero non funzionare.</string> + <string name="fex_extreme_tso_label">Estremo + TSO</string> + <string name="fex_extreme_tso_detail">Impostazioni Estremo con il TSO principale attivo.</string> + <string name="fex_extreme_gn_label">Estremo-gn</string> + <string name="fex_extreme_gn_detail">Prestazioni con suggerimenti TSC e metadati. Provalo prima di Estremo.</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">Prestazioni con controlli completi sul codice e bit dell\'hypervisor nascosto.</string> + <string name="game_env_custom_value">Valore personalizzato</string> + <string name="game_env_select_value">Seleziona valore</string> + <string name="user_apps_section">Emulatori e app</string> + <string name="user_apps_add">Aggiungi</string> + <string name="user_apps_add_detail">La tua app o il tuo gioco</string> + <string name="user_apps_kind_script">Script</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d%%</string> + <string name="user_apps_starting">Avvio…</string> + <string name="user_apps_added">Hai aggiunto %1$s</string> + <string name="user_apps_removed">Hai rimosso %1$s</string> + <string name="user_apps_back">Desktop</string> + <string name="user_apps_gone">Quell\'app non c\'è più. Sceglila di nuovo dal Desktop.</string> + <string name="user_apps_open">Apri %1$s</string> + <string name="user_apps_remove">Rimuovi</string> + <string name="user_apps_remove_confirm">Premi di nuovo per rimuovere</string> + <string name="user_apps_stop_session">Termina la sessione per rimuovere</string> + <string name="user_apps_script_linked">Esegue %1$s dalla sua cartella, collegata alla sessione nella posizione in cui si trova. Rimuovere l\'app non tocca quella cartella.</string> + <string name="user_apps_script_copied">Esegue %1$s da una copia della sua cartella dentro DroidDeck. Rimuovere l\'app elimina la copia, non l\'originale.</string> + <string name="user_apps_runtime_required">Installa prima il runtime Linux</string> + <string name="user_apps_file_gone">Il file non esiste più</string> + <string name="user_apps_no_space">Spazio libero insufficiente: per copiare la cartella servono %1$s</string> + <string name="user_apps_copying">Copia di %1$s</string> + <string name="user_apps_failed">Impossibile aggiungere l\'app</string> + <string name="user_apps_github_checking">Ricerca di una release di %1$s</string> + <string name="user_apps_github_unreachable">Impossibile leggere le release di %1$s</string> + <string name="user_apps_github_no_appimage">Nessuna release di %1$s ha un AppImage ARM64 o x86_64</string> + <string name="user_apps_downloading">Download di %1$s</string> + <string name="user_apps_download_failed">Download non riuscito</string> + <string name="user_apps_update_failed">Impossibile aggiornare l\'app</string> + <string name="user_apps_icon_failed">Impossibile leggere quell\'icona</string> + <string name="user_apps_saved">Modifiche a %1$s salvate</string> + <string name="user_apps_edit">Modifica</string> + <string name="user_apps_check_updates">Controlla aggiornamenti</string> + <string name="user_apps_checking_updates">Controllo aggiornamenti…</string> + <string name="user_apps_update_to">Aggiorna a %1$s</string> + <string name="user_apps_up_to_date">Ultima versione · %1$s</string> + <string name="user_apps_updated">Aggiornamento di %1$s completato</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">Aggiungi un\'app</string> + <string name="add_app_tab_script">Script</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">Un file .sh. Viene eseguito dalla sua cartella, a schermo intero in gamescope.</string> + <string name="add_app_appimage_hint">Un AppImage ARM64 (aarch64) o uno x86_64 da eseguire tramite FEX. Viene estratto nel runtime Linux una sola volta.</string> + <string name="add_app_github_hint">Installa l\'AppImage ARM64 dalla release più recente del repository, oppure quello x86_64 tramite FEX.</string> + <string name="app_fex_title">Usa FEX</string> + <string name="app_fex_auto">Auto</string> + <string name="app_fex_on">Sì</string> + <string name="app_fex_off">No</string> + <string name="app_fex_hint">FEX esegue programmi Linux x86 su questo dispositivo ARM64. Auto lo usa quando il programma è compilato per x86.</string> + <string name="app_fex_x86">%1$s tramite FEX</string> + <string name="app_fex_not_ready">Questo programma è compilato per PC %1$s e viene eseguito tramite FEX, che non è ancora configurato. Apri Steam una volta dopo aver eseguito l\'accesso: DroidDeck gli farà scaricare FEX e lo Steam Linux Runtime necessario, poi il programma si aprirà.</string> + <string name="session_end_program">Questo programma</string> + <string name="session_end_fex_missing">%1$s è compilato per PC x86 e viene eseguito tramite FEX, che non è ancora installato. Avvia Steam una volta dopo aver eseguito l\'accesso e lascia che scarichi FEX e lo Steam Linux Runtime, poi riavvia il programma.</string> + <string name="add_app_flatpak_hint">Un ID app di Flathub o la sua pagina su Flathub. Se manca, Flatpak viene prima configurato.</string> + <string name="add_app_file">File</string> + <string name="add_app_no_file">Nessun file scelto</string> + <string name="add_app_choose_file">Scegli file</string> + <string name="add_app_change_file">Cambia</string> + <string name="add_app_pick_script">Scegli uno script</string> + <string name="add_app_pick_appimage">Scegli un AppImage</string> + <string name="add_app_pick_icon">Scegli un\'icona</string> + <string name="add_app_repository">Repository</string> + <string name="add_app_repository_placeholder">proprietario/repo o un link github.com</string> + <string name="add_app_repository_invalid">Non è un repository GitHub</string> + <string name="add_app_flatpak_id">ID app</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">Non è un ID app di Flathub</string> + <string name="add_app_flatpak_bundle_hint">Oppure un file .flatpak, per un\'app che non è su Flathub.</string> + <string name="add_app_pick_flatpak">Scegli un file .flatpak</string> + <string name="add_app_source">Origine</string> + <string name="add_app_appearance">Sul tuo Desktop</string> + <string name="add_app_preview_name">Nuova app</string> + <string name="add_app_name">Nome</string> + <string name="add_app_name_placeholder">Dall\'app</string> + <string name="add_app_icon">Icona</string> + <string name="add_app_icon_default">Icona predefinita</string> + <string name="add_app_choose_icon">Scegli icona</string> + <string name="add_app_clear_icon">Cancella</string> + <string name="add_app_icon_suggestions">Dal repository</string> + <string name="add_app_icon_searching">Ricerca di icone…</string> + <string name="add_app_icon_reset">Predefinita</string> + <string name="edit_app_title">Modifica %1$s</string> + <string name="edit_app_save">Salva</string> + <string name="add_app_checking_folder">Controllo della cartella…</string> + <string name="add_app_folder_linked">La sua cartella è collegata alla sessione: %1$s</string> + <string name="add_app_folder_copied">La sua cartella contiene programmi Linux, che la memoria condivisa non può eseguire, quindi viene copiata in DroidDeck (%1$s).</string> + <string name="add_app_confirm">Aggiungi</string> + <string name="add_app_cancel">Annulla</string> + <string name="add_app_runtime_required">Installa prima il runtime Linux da Configurazione.</string> + <string name="add_app_busy">Attendi che %1$s finisca.</string> + <string name="steam_compat_label">Compatibile</string> + <string name="proton_compatible_note">Le build contrassegnate con “%1$s” nel menu Compatibilità di Steam includono le correzioni al prefisso di DroidDeck e avviano i giochi qui. Quella predefinita è Proton Experimental ARM64 (%1$s), oppure Proton 11.0 ARM64 (%1$s) quando è installata questa build più recente. Le voci Proton di Valve senza il contrassegno non possono avviare giochi su questo dispositivo.</string> + <string name="proton_installed_compatible">Installato: %1$s · %2$s</string> + <string name="common_cancel">Annulla</string> + <string name="common_auto">Auto</string> + <string name="common_never">Mai</string> + <string name="common_always">Sempre</string> + <string name="common_uninstall">Disinstalla</string> + <string name="common_check">Controlla</string> + <string name="common_update">Aggiorna</string> + <string name="common_checking">Controllo…</string> + <string name="common_working">Operazione in corso…</string> + <string name="common_forget">Dimentica</string> + <string name="common_add_ellipsis">Aggiungi…</string> + <string name="common_use">Usa</string> + <string name="common_applies_next_session">Si applica dalla prossima sessione.</string> + <string name="mode_steam_title">Sessione Steam</string> + <string name="mode_desktop_title">Sessione desktop</string> + <string name="mode_display">Schermo</string> + <string name="mode_resolution">Risoluzione</string> + <string name="mode_res_720">Fino a 720p</string> + <string name="mode_res_900">Fino a 900p</string> + <string name="mode_res_1080">Fino a 1080p</string> + <string name="mode_res_native">Nativa</string> + <string name="mode_res_default">%1$s (predefinita)</string> + <string name="mode_res_custom_value">Personalizzata · %1$d×%2$d</string> + <string name="mode_res_custom">Personalizzata…</string> + <string name="mode_res_note">A 720p i menu possono essere più reattivi.</string> + <string name="mode_ratio">Proporzioni schermo</string> + <string name="mode_ratio_custom">Definite dalla risoluzione personalizzata.</string> + <string name="mode_ratio_auto">Auto usa almeno 16:9.</string> + <string name="mode_fps">Limite frame</string> + <string name="mode_fps_note">Limita l\'intera sessione. Lo schermo passa a una frequenza di aggiornamento multipla del limite.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">Uscita HDR10</string> + <string name="mode_hdr_unavailable">Non disponibile: %1$s.</string> + <string name="mode_hdr_restart">Riavvia l\'app per applicare.</string> + <string name="mode_drivers">Driver</string> + <string name="mode_gpu_drivers">Driver GPU</string> + <string name="mode_gpu_drivers_hint">Condivisi da Steam e Desktop. Si impostano nella pagina Componenti.</string> + <string name="mode_touch_controls">Tocco e controlli</string> + <string name="mode_touch">Tocco</string> + <string name="mode_touch_touchpad">Touchpad</string> + <string name="mode_touch_direct">Diretto</string> + <string name="mode_touch_note">Auto: touchpad sul Desktop, diretto in Steam.</string> + <string name="mode_osc">Controlli su schermo</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">Auto mostra i controlli quando non è collegato nessun controller.</string> + <string name="osc_steam_touch">Touch di Steam (beta)</string> + <string name="osc_style_next_session">I controlli touch di Steam partono dalla prossima sessione Steam</string> + <string name="osc_steam_layout">Layout touch di Steam</string> + <string name="osc_steam_layout_hint">Sposta, ridimensiona o nascondi i controlli touch di questo gioco; vengono salvati su Steam</string> + <string name="mode_controller">Controller</string> + <string name="mode_controller_hint">Come Steam vede il tuo controller. Si applica dalla prossima sessione.</string> + <string name="mode_controller_deck">Controller di Steam Deck</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Controller Xbox 360</string> + <string name="mode_controller_note">Deck: tasto Accesso rapido e giroscopio. Steam Controller: lo stesso, con il rilevamento della presa sui tasti posteriori. DualShock 4 e DualSense: giroscopio, con i trackpad usati come touchpad. Xbox 360: apri Accesso rapido con Guide+A.</string> + <string name="mode_back">Indietro</string> + <string name="back_menu_then_qam">Menu → QAM</string> + <string name="back_qam_then_menu">QAM → Menu</string> + <string name="mode_session">Sessione</string> + <string name="mode_suspend">Comportamento in background</string> + <string name="mode_suspend_hint">Quando l\'app è nascosta o lo schermo si spegne.</string> + <string name="mode_suspend_manual">Manuale</string> + <string name="mode_suspend_native">Nativo</string> + <string name="mode_suspend_steam_note">Auto mette in pausa e riprende quando l\'app torna visibile. Nativo prepara prima Steam alla sospensione. Manuale attende Riprendi. Mai lascia tutto in esecuzione.</string> + <string name="mode_background_downloads">Continua i download</string> + <string name="mode_background_downloads_hint">Ritarda la sospensione in background mentre Steam sta scaricando.</string> + <string name="mode_suspend_note">Auto mette in pausa in background e riprende quando l\'app torna visibile. Manuale attende Riprendi. Mai lascia tutto in esecuzione.</string> + <string name="mode_startup">Avvio</string> + <string name="mode_steam_startup">Avvia Steam con DroidDeck</string> + <string name="mode_steam_startup_hint">Apre la sessione Steam all\'avvio di DroidDeck.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">Ricerca di release…</string> + <string name="mode_decky_ready">Pronto per installare %1$s.</string> + <string name="mode_decky_none">Nessuna build compatibile trovata. Riprova più tardi.</string> + <string name="mode_decky_installed">Installato · %1$s</string> + <string name="mode_decky_update">Aggiornamento disponibile · %1$s</string> + <string name="mode_decky_current">Ultima versione · %1$s</string> + <string name="mode_decky_loader">Loader</string> + <string name="mode_decky_install_latest">Installa l\'ultima versione</string> + <string name="mode_decky_on">Si avvia con Steam. Quando è attivo, altre app su questo dispositivo potrebbero controllare Steam.</string> + <string name="mode_decky_off">Disattivato. La porta di debug remoto di Steam resta chiusa.</string> + <string name="mode_decky_remove_title">Disinstallare Decky Loader?</string> + <string name="mode_decky_remove_text">Rimuove il loader. I tuoi plugin e le impostazioni vengono mantenuti.</string> + <string name="mode_client">Client</string> + <string name="mode_mangoapp">Overlay prestazioni</string> + <string name="mode_mangoapp_hint">Disattivalo se i giochi vanno in crash o diventano neri in modalità Deck. Si applica dalla prossima sessione.</string> + <string name="mode_steam_repair">Ripara il client Steam</string> + <string name="mode_steam_repair_hint">Scarica di nuovo il client Steam al prossimo avvio di Steam. Usalo se Steam resta sulla schermata di caricamento. Giochi, accesso e impostazioni vengono mantenuti.</string> + <string name="mode_steam_repair_queued">Il client Steam verrà scaricato di nuovo al prossimo avvio di Steam.</string> + <string name="mode_steam_repair_button">Ripara</string> + <string name="mode_added_art">Grafica</string> + <string name="mode_sgdb_key">Chiave API SteamGridDB</string> + <string name="mode_sgdb_key_yours">La tua chiave</string> + <string name="mode_sgdb_edit">Modifica…</string> + <string name="mode_added_art_hint">Completa la grafica mancante da Steam, poi da SteamGridDB. I tuoi cover.jpg, hero.jpg, logo.png o icon.png nella cartella del gioco hanno la priorità.</string> + <string name="summary_remove_game">Rimuovi %1$s</string> + <string name="mode_added_remove_confirm">Rimuovere?</string> + <string name="games_add">Aggiungi gioco</string> + <string name="games_add_title">Aggiungi un gioco:</string> + <string name="games_pick_exe_title">Scegli l\'exe:</string> + <string name="games_add_unreachable">Questa posizione non è disponibile nella sessione</string> + <string name="mode_added_exe_check">Controlla questo exe</string> + <string name="wincomp_title">Componenti Windows</string> + <string name="wincomp_pad_hint">LB / RB per passare da una sezione all\'altra.</string> + <string name="wincomp_card_none">Nessuno</string> + <string name="wincomp_applies">DLL di Windows non incluse in Proton, dall\'elenco dei componenti. Attiva quelle che servono a questo gioco: vanno nella cartella Windows del gioco, mai tra i suoi file, e restano con qualunque Proton venga usato.</string> + <string name="wincomp_offline">Impossibile raggiungere l\'elenco dei componenti. I componenti già scaricati si possono comunque attivare o disattivare.</string> + <string name="wincomp_recommended">Consigliati per questo gioco</string> + <string name="wincomp_checking">Verifica di cosa serve a questo gioco…</string> + <string name="wincomp_when_online">Disponibile quando sei online</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">Attivazione di %1$d componente…</item> + <item quantity="many">Attivazione di %1$d componenti…</item> + <item quantity="other">Attivazione di %1$d componenti…</item> + </plurals> + <string name="wincomp_auto_section">Attivi per questo gioco</string> + <string name="wincomp_auto_steam">Steam installa %1$s con %2$s (app %3$s)</string> + <string name="wincomp_auto_gog">Dall\'elenco di GOG per questo gioco</string> + <string name="wincomp_auto_epic">Dai prerequisiti di Epic per questo gioco</string> + <string name="wincomp_auto_amazon">Dai passaggi di installazione di Amazon per questo gioco</string> + <string name="wincomp_steam_by_name">Steam installa %1$s con l\'app %2$s, abbinata per nome</string> + <string name="wincomp_suggestions">Elencati anche</string> + <string name="wincomp_suggestion_reason">Elencato da %1$s; qui non è un componente</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">Il gioco include %1$s</string> + <string name="wincomp_found_shipped">Il gioco ha %1$s (spesso funziona così com\'è)</string> + <string name="wincomp_already_here">Già presente in questo gioco</string> + <string name="wincomp_no_recommendation">Nulla nella cartella del gioco indica un componente.</string> + <string name="wincomp_active">Installati per questo gioco (%1$d)</string> + <string name="wincomp_all">Tutti i componenti (%1$d)</string> + <string name="wincomp_installers">Richiedono un programma di installazione Windows (%1$d)</string> + <string name="wincomp_installers_note">Sono programmi di installazione a sé, senza un pacchetto da scomporre e ancora senza una registrazione di ciò che installano. Quasi tutti sono vecchi runtime già sostituiti dal Mono incluso in Proton; le registrazioni vengono aggiunte man mano che vengono create.</string> + <string name="wincomp_show">Mostra</string> + <string name="wincomp_hide">Nascondi</string> + <string name="wincomp_needs_installer">Esegue un proprio programma di installazione (ancora nessuna registrazione)</string> + <string name="wincomp_unsupported">Non ancora supportato qui</string> + <string name="wincomp_loading">Caricamento dei componenti…</string> + <string name="wincomp_failed">Impossibile installare: %1$s</string> + <string name="wincomp_downloaded">Scaricato</string> + <string name="wincomp_downloading">Download di %1$s</string> + <string name="wincomp_installing">Installazione: %1$s</string> + <string name="wincomp_reverting">Disattivazione…</string> + <string name="wincomp_download_size">%1$s da scaricare</string> + <string name="wincomp_next_launch">Ha effetto al prossimo avvio del gioco.</string> + <string name="mode_stretch169">Allarga i giochi 16:9</string> + <string name="mode_stretch169_hint">Sugli schermi più quadrati di 16:9, allarga i giochi a tutto schermo invece di mostrare le bande. Si applica dalla prossima sessione.</string> + <string name="mode_audio">Audio</string> + <string name="mode_directaudio">DirectAudio per i giochi</string> + <string name="mode_directaudio_hint">Bypassa PulseAudio per ridurre la latenza.</string> + <string name="mode_mic">Microfono</string> + <string name="mode_mic_hint">Per la chat vocale.</string> + <string name="mode_network">Rete</string> + <string name="mode_wifi_discovery">Rilevamento Wi-Fi</string> + <string name="mode_wifi_discovery_hint">Mostra in Steam i nomi delle reti Wi-Fi e le reti vicine.</string> + <string name="mode_wifi_location_off">La geolocalizzazione è disattivata.</string> + <string name="mode_wifi_permission_denied">Autorizzazione Posizione non concessa.</string> + <string name="mode_wifi_open_settings">Apri impostazioni</string> + <string name="mode_wifi_continue">Continua</string> + <string name="mode_wifi_explain_title">Attivare il rilevamento Wi-Fi?</string> + <string name="mode_wifi_explain_text">Android richiede l\'autorizzazione Posizione per leggere i nomi delle reti Wi-Fi e rilevare le reti vicine. DroidDeck usa questo accesso per mostrare quelle reti in Steam. Non tracciamo la tua posizione. L\'accesso a Internet funziona anche senza questa autorizzazione.</string> + <string name="mode_storage">Archiviazione giochi</string> + <string name="mode_storage_auto">Automatica - la scheda SD se presente, più le tue cartelle Games</string> + <string name="mode_storage_auto_none">Automatica - le tue cartelle Games, più la scheda SD se presente (ora assente)</string> + <string name="mode_storage_internal">Solo memoria interna</string> + <string name="mode_storage_folder">Cartella: %1$s</string> + <string name="mode_second_library">Seconda libreria</string> + <string name="mode_storage_note">I giochi che caricano risorse in streaming dalla SD o dalla memoria condivisa potrebbero scattare.</string> + <string name="mode_storage_diagnostics">Diagnostica archiviazione</string> + <string name="mode_storage_diagnostics_hint">Registra i risultati delle allocazioni e campioni dei tempi di archiviazione in Condividi log. Mentre è attiva può rallentare le installazioni. Si applica dalla prossima sessione.</string> + <string name="mode_choose_folder">Scegli una cartella…</string> + <string name="mode_custom_res_title">Risoluzione personalizzata</string> + <string name="mode_custom_res_text">Sostituisce il limite e le proporzioni. Una dimensione che non corrisponde allo schermo avrà delle bande.</string> + <string name="mode_width">Larghezza</string> + <string name="mode_height">Altezza</string> + <string name="mode_custom_res_range">Tra 320×240 e 3840×2160.</string> + <plurals name="mode_added_count"> + <item quantity="one">%1$d gioco</item> + <item quantity="many">%1$d giochi</item> + <item quantity="other">%1$d giochi</item> + </plurals> + <string name="common_ok">OK</string> + <string name="common_delete">Elimina</string> + <string name="common_restore">Ripristina</string> + <string name="common_download">Scarica</string> + <string name="common_delete_named">Elimina %1$s</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Driver di runtime</string> + <string name="comp_runtime_driver_hint">Usato da Steam, dai giochi e dal desktop. Si applica dalla prossima sessione.</string> + <string name="comp_import_turnip">Importa zip Turnip…</string> + <string name="comp_display_driver">Driver di visualizzazione</string> + <string name="comp_display_driver_hint">Disegna i frame sullo schermo in entrambe le modalità. Riavvia l\'app per applicare.</string> + <string name="comp_import_adrenotools">Importa uno zip AdrenoTools…</string> + <string name="comp_never">mai</string> + <string name="comp_nightlies_checked">Nightlies controllate: %1$s</string> + <string name="comp_title">Componenti</string> + <string name="comp_about">Informazioni sui componenti</string> + <string name="comp_check_drivers">Cerca nuovi driver</string> + <string name="comp_check_nightlies">Cerca nuovi pacchetti nelle Nightlies</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">Il Proton scelto qui esegue ogni gioco che non ne ha scelto uno proprio in Steam, e una modifica fatta in Steam appare anche qui. Steam sostituisce il Proton di Valve quando si aggiorna; gli originali vengono conservati per ogni build.</string> + <string name="comp_scope">Configurazione</string> + <string name="comp_profile_configure">Configura profilo</string> + <string name="comp_profile">Profilo</string> + <string name="comp_scope_general">Profilo predefinito</string> + <string name="comp_scope_general_detail">Usato automaticamente dai giochi senza personalizzazioni.</string> + <string name="comp_profile_game_detail">Impostazioni salvate, usate automaticamente all\'avvio di %1$s.</string> + <string name="comp_game_hint">Scegli solo le impostazioni che servono a questo gioco. Tutto il resto continua a usare il Profilo predefinito.</string> + <string name="comp_game_fex">Preset FEX</string> + <string name="comp_game_fex_inherited">In uso il preset del Profilo predefinito.</string> + <string name="comp_game_fex_override">Preset personalizzato salvato per questo gioco.</string> + <string name="comp_game_fex_custom">Variabili FEX personalizzate</string> + <string name="comp_game_use_general">Usa predefinito</string> + <string name="comp_game_compatibility">Compatibilità del gioco</string> + <string name="comp_game_environment_hint">Opzioni d\'ambiente di Proton, DXVK, VKD3D e sincronizzazione per questo gioco.</string> + <string name="comp_game_proton_inherited">In uso il Proton del Profilo predefinito.</string> + <string name="comp_game_proton_override">Proton personalizzato salvato per questo gioco tramite la mappatura di compatibilità di Steam.</string> + <string name="comp_no_proton_short">Nessun Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Usa il pacchetto del Profilo predefinito da %1$s, a meno che questo gioco non ne selezioni un altro.</string> + <string name="comp_game_component_general">Usa il pacchetto di componenti del Profilo predefinito.</string> + <string name="comp_game_component_override">Usa %1$s solo per questo gioco.</string> + <string name="comp_game_runtime_options">Opzioni del runtime</string> + <string name="comp_game_environment_other_hint">Altre opzioni d\'ambiente per gioco di Proton, DXVK, VKD3D-Proton e sincronizzazione.</string> + <string name="comp_game_saved_next">Il profilo di %1$s è salvato per il prossimo avvio del gioco.</string> + <string name="game_no_steam_title">Avvia senza Steam</string> + <string name="game_no_steam_detail">Per un gioco che si chiude subito: lo avvia senza la configurazione Steam di Proton. Le funzioni di Steam, come l\'overlay, sono disattivate per esso.</string> + <string name="game_no_steam_on">Attivo</string> + <string name="comp_game_proton_live">La scelta di Proton per %1$s è salvata e già disponibile in Steam.</string> + <string name="comp_game_proton_next">La scelta di Proton per %1$s è salvata. Riavvia Steam prima di avviare il gioco.</string> + <string name="comp_game_proton_not_runnable">%1$s non può usare il Proton selezionato su questo dispositivo.</string> + <string name="comp_game_load_failed">Impossibile caricare il profilo di %1$s: %2$s</string> + <string name="comp_game_save_failed">Impossibile salvare il profilo del gioco: %1$s</string> + <string name="comp_game_reset">Reimposta profilo del gioco</string> + <string name="comp_game_reset_title">Reimpostare il profilo di %1$s?</string> + <string name="comp_game_reset_body">Rimuovere le personalizzazioni di Proton, FEX, pacchetto di componenti, ambiente, filtro delle texture e componenti Windows di questo gioco? Userà il Profilo predefinito.</string> + <string name="comp_game_reset_done">%1$s ora usa il Profilo predefinito.</string> + <string name="comp_game_reset_failed">Impossibile reimpostare completamente il profilo del gioco: %1$s</string> + <string name="comp_default_live">%1$s ora esegue ogni gioco che non ha scelto un proprio Proton in Steam.</string> + <string name="comp_default_next">%1$s eseguirà ogni gioco che non ha scelto un proprio Proton, dal prossimo avvio di Steam.</string> + <string name="comp_default_not_runnable">%1$s non può avviare giochi qui, quindi non può essere il predefinito.</string> + <string name="comp_default_failed">Impossibile salvare il Proton predefinito: %1$s</string> + <string name="comp_swapped">%1$s · %2$d sostituiti</string> + <string name="comp_all_original">%1$s · tutti originali</string> + <string name="comp_game_running_suffix">%1$s · gioco in esecuzione</string> + <string name="comp_esync_pack">%1$s · pacchetto droiddeck-esync</string> + <string name="comp_esync_wanted">%1$s · ancora nessun pacchetto droiddeck-esync</string> + <string name="comp_esync_builtin">%1$s · esync integrato, nessun pacchetto necessario</string> + <string name="comp_gpu_drivers">Driver GPU</string> + <string name="comp_reapplied">%1$s · riapplicato all\'avvio %2$s</string> + <string name="comp_next_queued">Prossimo: %1$s · dopo la chiusura del gioco</string> + <string name="comp_in_use">%1$s in uso: %2$s</string> + <string name="comp_waits">Attende la chiusura del gioco</string> + <string name="comp_applies_next_game">Si applica al prossimo avvio di un gioco</string> + <string name="comp_import_zip">Importa .zip</string> + <string name="comp_reading">Lettura delle versioni di Proton…</string> + <string name="comp_no_proton">Nessun Proton ancora installato. Avvia Steam una volta per scaricare il suo Proton ARM64, oppure aggiungi GE-Proton, CachyOS o DroidDeck-Proton da Configurazione.</string> + <string name="comp_original">Originale · %1$s</string> + <string name="comp_this_build">%1$s · questa build</string> + <string name="comp_earlier_build">%1$s · build precedente</string> + <string name="comp_installed_count">Installati · %1$d</string> + <string name="comp_nothing_stored">Ancora nulla di salvato per %1$s. I file di questo Proton vengono salvati come suo originale.</string> + <string name="comp_waits_long">Un gioco è in esecuzione su questo Proton, quindi la modifica attende la sua chiusura.</string> + <string name="comp_applies_long">Si applica al prossimo avvio di un gioco.</string> + <string name="comp_swap_title">Sostituire %1$s?</string> + <string name="comp_swap_body">Inserire %1$s in %2$s? I suoi file originali restano salvati. %3$s</string> + <string name="comp_restore_title">Ripristinare %1$s?</string> + <string name="comp_restore_body">Rimettere l\'originale di %1$s in %2$s? %3$s</string> + <string name="comp_delete_package_body">Rimuove il pacchetto dall\'app. Puoi scaricarlo di nuovo.</string> + <string name="comp_delete_original_title">Eliminare questo originale?</string> + <string name="comp_delete_original_body">Questo originale di %1$s proviene da una build precedente di %2$s. L\'originale della build attuale viene sempre conservato.</string> + <string name="comp_nightlies_count">Nelle Nightlies · %1$d</string> + <string name="comp_refresh_to_list">Aggiorna per elencare i pacchetti nelle Nightlies.</string> + <string name="comp_nothing_new">Niente di nuovo dall\'ultimo controllo.</string> + <string name="comp_import_wcp">Importa .wcp</string> + <string name="comp_restore_original">Ripristina l\'originale di questo Proton</string> + <string name="comp_restore_original_body">Ripristinare il %2$s originale di %1$s?</string> + <string name="comp_swap">Sostituisci</string> + <string name="comp_about_text">FEX, DXVK e VKD3D-Proton per ogni Proton con cui Steam esegue i giochi.\n\nSeleziona una riga installata per usarla al posto di quella attuale. La modifica si applica al prossimo avvio di un gioco, o dopo la chiusura del gioco in esecuzione.\n\nLe tue scelte vengono ricontrollate prima di ogni avvio e ripristinate se qualcosa le ha modificate. I file originali di ogni Proton vengono conservati, così gli aggiornamenti di Steam non li perdono mai.</string> + <string name="comp_tag_original">Originale</string> + <string name="comp_tag_in_use">In uso</string> + <string name="comp_tag_new">Nuovo</string> + <string name="comp_tag_stored">Salvato</string> + <string name="perf_title">Prestazioni</string> + <string name="perf_lede">Core della CPU, correzioni della sessione e nome host. Si applica dalla prossima sessione.</string> + <string name="perf_client_cores_group">Core del client Steam</string> + <string name="perf_override">Sostituisci la scelta dei core di Steam</string> + <string name="perf_override_hint">Vincola Steam, il suo helper dell\'interfaccia e gamescope ai core qui sotto.</string> + <string name="perf_client_cores">Core del client</string> + <string name="perf_client_cores_on">Core per Steam.</string> + <string name="perf_client_cores_off">Attiva la sostituzione per sceglierli.</string> + <string name="perf_game_cores_group">Core dei giochi</string> + <string name="perf_game_cores">Core dei giochi</string> + <string name="perf_game_cores_hint">Core per i giochi. Con tutti selezionati, Android li gestisce liberamente.</string> + <string name="perf_client_ui">Interfaccia del client</string> + <string name="perf_glthread">GL multithread</string> + <string name="perf_glthread_hint">Può rendere più reattivi i menu di Steam.</string> + <string name="perf_zink">Zink: descrittori differiti</string> + <string name="perf_zink_hint">Consigliato per i driver senza descriptor buffer.</string> + <string name="perf_noglerror">Salta i controlli degli errori GL</string> + <string name="perf_noglerror_hint">Disattiva la convalida GL a ogni chiamata.</string> + <string name="perf_gsrealtime">gamescope: coda GPU in tempo reale</string> + <string name="perf_gsrealtime_hint">Dà priorità al lavoro GPU del compositor. Può rendere più regolare il frame pacing, a scapito dei giochi.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">Mantieni la GPU alla frequenza massima</string> + <string name="perf_gpuclock_hint">Solo Adreno. Meno scatti, più consumo di batteria e più calore.</string> + <string name="perf_fixes">Correzioni della sessione</string> + <string name="perf_sysmem">Turnip: rendering sysmem</string> + <string name="perf_sysmem_hint">Più veloce per Steam e la maggior parte dei giochi. Necessario su Adreno 710/720/722.</string> + <string name="perf_xalia">Salta l\'assistente xalia di Steam</string> + <string name="perf_fastsync">droiddeck-ntsync (sperimentale)</string> + <string name="perf_fastsync_hint">Un sostituto in userspace del driver ntsync di Linux, per i test. Quando è attivo, Proton lo usa al posto di droiddeck-esync e droiddeck-fsync. Funziona con Proton Experimental ARM64, GE-Proton e proton-cachyos. Si applica dalla prossima sessione.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">Prima droiddeck-fsync</string> + <string name="perf_fsync_hint">Esegue il fsync di Proton sui file Proton originali, con futex_waitv fornito in userspace, quindi non richiede pacchetti e continua a funzionare appena Steam aggiorna Proton. Quando è attivo, i giochi lo usano prima di droiddeck-esync, tranne i pochi che Proton esclude da fsync. Si applica dalla prossima sessione.</string> + <string name="perf_esync_hint">I thread Windows dei giochi si attendono a vicenda tramite eventfd e memoria condivisa invece di passare ogni volta da wineserver. Usa un pacchetto droiddeck-esync creato per quella precisa build di Proton; i pacchetti vengono scaricati automaticamente. Finché non ne esiste uno per una nuova build di Proton, viene usato droiddeck-fsync. Si applica dalla prossima sessione.</string> + <string name="perf_xalia_hint">Disattiva l\'assistente di navigazione con gamepad di Proton, che consuma CPU in ogni gioco.</string> + <string name="perf_seccomp">Esegui proot senza seccomp</string> + <string name="perf_seccomp_hint">Può risolvere errori di syscall mancanti su alcuni kernel. Può ridurre le prestazioni.</string> + <string name="perf_fastpath">Percorso veloce di proot</string> + <string name="perf_fastpath_needs_seccomp">Richiede il filtro seccomp di proot.</string> + <string name="perf_fastpath_hint">Ricerca dei file più veloce e meno scatti. Disattivalo se un programma non trova i suoi file.</string> + <string name="perf_identity">Identità della sessione</string> + <string name="perf_hostname">Nome host</string> + <string name="perf_hostname_hint">Il nome con cui Steam vede questo dispositivo. Se vuoto, torna a %1$s.</string> + <string name="perf_hostname_invalid">Solo lettere, cifre e trattini interni, fino a 63 caratteri.</string> + <string name="perf_phantom_warning">Android è impostato per terminare questa sessione</string> + <string name="driver_installed">Installati</string> + <string name="driver_available">Disponibili per il download</string> + <string name="driver_nothing_new">Niente di nuovo dall\'ultimo controllo.</string> + <string name="driver_restore_builtin">Ripristina driver integrati</string> + <string name="driver_delete_builtin">Integrato nell\'app: questa azione rimuove i suoi file estratti e lo nasconde. Ripristina driver integrati lo fa ricomparire.</string> + <string name="driver_delete_bundle">Metà di un pacchetto Android + Linux: vengono rimosse entrambe le metà. Un driver in uso torna a quello predefinito.</string> + <string name="driver_delete_plain">I suoi file vengono rimossi dall\'app.</string> + <string name="driver_delete_in_use">I suoi file vengono rimossi. È in uso, quindi subentra quello predefinito.</string> + <string name="drawer_page_display">Schermo</string> + <string name="drawer_page_effects">Effetti</string> + <string name="drawer_page_controls">Controlli</string> + <string name="drawer_page_components">Componenti</string> + <string name="drawer_page_controller">Controller</string> + <string name="drawer_page_settings">Impostazioni</string> + <string name="drawer_page_desc">Pagina %1$s</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Desktop</string> + <string name="drawer_steam_menu">Menu di Steam</string> + <string name="drawer_quick_access">Accesso rapido</string> + <string name="drawer_open_qam">Apri il menu di accesso rapido</string> + <string name="drawer_prev_page">Pagina precedente</string> + <string name="drawer_next_page">Pagina successiva</string> + <string name="drawer_hud">HUD prestazioni</string> + <string name="drawer_scaling">Modalità di ridimensionamento</string> + <string name="drawer_scaling_sharpness">Nitidezza del ridimensionamento</string> + <string name="drawer_effects">Effetti schermo</string> + <string name="drawer_effects_hint">In tempo reale. Usa un po\' di tempo GPU mentre un effetto è attivo.</string> + <string name="drawer_look">Preset</string> + <string name="drawer_look_custom">Personalizzato</string> + <string name="drawer_effects_advanced">Avanzate</string> + <string name="widgets_expanded">Espanso</string> + <string name="widgets_collapsed">Compresso</string> + <string name="drawer_cas">Nitidezza (CAS)</string> + <string name="drawer_cas_level">Livello di nitidezza</string> + <string name="drawer_fake_hdr">HDR simulato</string> + <string name="drawer_deband">Riduzione del banding</string> + <string name="drawer_deband_strength">Intensità riduzione banding</string> + <string name="drawer_brightness">Luminosità</string> + <string name="drawer_contrast">Contrasto</string> + <string name="drawer_gamma">Gamma</string> + <string name="drawer_saturation">Saturazione</string> + <string name="drawer_fxaa">FXAA (smussatura dei bordi)</string> + <string name="drawer_toon">Contorni cartoon</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Filtro delle texture</string> + <string name="drawer_texture_hint">Giochi DirectX 9–11, dal prossimo avvio.</string> + <string name="drawer_anisotropy">Filtro anisotropico</string> + <string name="drawer_texture_sharpness">Nitidezza delle texture</string> + <string name="drawer_texture_sharpness_note">Un mip bias negativo: texture più nitide, un po\' di sfarfallio. Auto si adatta a quanto la sessione viene ingrandita sullo schermo.</string> + <string name="drawer_touch_auto">Auto (%1$s)</string> + <string name="drawer_keyboard">Tastiera</string> + <string name="drawer_pc_keyboard">Tastiera PC</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">Secondo schermo</string> + <string name="drawer_second_screen_mode">Modalità</string> + <string name="drawer_second_screen_shows">Mostra</string> + <string name="drawer_android_apps">App Android</string> + <string name="drawer_launch_app">Avvia un\'app</string> + <string name="common_hide">Nascondi</string> + <string name="common_show">Mostra</string> + <string name="drawer_no_apps">Nessuna app avviabile</string> + <string name="drawer_session_behavior">Comportamento della sessione</string> + <string name="drawer_suspend_hint">Si applica subito e alle sessioni future in questa modalità.</string> + <string name="drawer_next_session">Prossima sessione</string> + <string name="drawer_support">Assistenza</string> + <string name="drawer_logs">Log della sessione</string> + <string name="drawer_logs_hint">Da allegare a una segnalazione di bug</string> + <string name="drawer_share_logs">Condividi log</string> + <string name="drawer_background">In background</string> + <string name="drawer_stop">Termina</string> + <string name="drawer_stop_title">Terminare la sessione?</string> + <string name="drawer_running_now">In esecuzione ora</string> + <string name="drawer_no_game">Nessun gioco in esecuzione su un Proton</string> + <string name="drawer_selected_profile">Profilo del gioco</string> + <string name="drawer_selected_profile_none">I dettagli del profilo non sono ancora disponibili.</string> + <string name="drawer_profile_refresh_failed_initial">Impossibile caricare i profili dei giochi. Nuovo tentativo automatico.</string> + <string name="drawer_profile_refresh_failed_stale">Impossibile aggiornare i profili dei giochi. Vengono mostrati gli ultimi dati disponibili; nuovo tentativo automatico.</string> + <string name="drawer_profile_inspect_hint">Visualizza e modifica le impostazioni salvate usate al prossimo avvio.</string> + <string name="drawer_profile_default">In uso il Profilo predefinito</string> + <string name="drawer_profile_custom">Profilo personalizzato</string> + <string name="drawer_component_inherited">In uso %1$s dal Profilo predefinito.</string> + <string name="drawer_game_components_note">Le modifiche vengono salvate nel profilo di %1$s e si applicano al prossimo avvio del gioco.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d personalizzazione del gioco</item> + <item quantity="many">%1$d personalizzazioni del gioco</item> + <item quantity="other">%1$d personalizzazioni del gioco</item> + </plurals> + <string name="drawer_no_proton">Nessun Proton ancora installato.</string> + <string name="drawer_game_running">gioco in esecuzione</string> + <string name="drawer_reapplied">riapplicato all\'avvio %1$s</string> + <string name="drawer_original">Originale</string> + <string name="drawer_components_note">Importa ed elimina nella pagina Componenti.</string> + <string name="setup_check_done">Fatto</string> + <string name="setup_check_warn">Richiede attenzione</string> + <string name="setup_check_busy">In corso</string> + <string name="setup_limit_adb_failed">Controlla l\'indirizzo IP e la porta di Debug wireless, oppure esegui di nuovo l\'accoppiamento se Android ha rimosso questo dispositivo.</string> + <string name="setup_install">Installa</string> + <string name="setup_manage">Gestisci</string> + <string name="setup_tab_overview">Panoramica</string> + <string name="setup_tab_controller">Controller</string> + <string name="setup_tab_session">Sessione</string> + <string name="setup_tab_launcher">Launcher</string> + <string name="setup_title">Configurazione</string> + <string name="setup_all_set">● Tutto pronto</string> + <string name="setup_n_ready">%1$d di %2$d pronti</string> + <string name="setup_system_check">Controllo del sistema</string> + <string name="setup_gpu_supported">Dispositivo supportato</string> + <string name="setup_gpu_untested">GPU non testata</string> + <string name="setup_gpu_unsupported">GPU non supportata</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">Steam richiede un driver Adreno; %1$s potrebbe mostrare una schermata nera</string> + <string name="setup_runtime">Runtime Linux</string> + <string name="setup_runtime_progress">%1$s · %2$d%%</string> + <string name="setup_runtime_missing">Non installato · circa 3 GB, si installa la prima volta che premi Gioca</string> + <string name="setup_installed">Installato</string> + <string name="setup_runtime_update">%1$s · aggiornamento disponibile</string> + <string name="setup_runtime_current">%1$s · aggiornato</string> + <string name="setup_limit">Limite processi figlio</string> + <string name="setup_limit_on">Attivo · Android potrebbe chiudere Steam. Disattivarlo richiede un minuto.</string> + <string name="setup_limit_unset">Non impostato · Android potrebbe chiudere Steam. Disattivarlo richiede un minuto.</string> + <string name="setup_limit_unknown">Impossibile verificare · disattivarlo richiede un minuto.</string> + <string name="setup_fix_it">Risolvi</string> + <string name="setup_details">Dettagli</string> + <string name="setup_dev_options">Opzioni sviluppatore</string> + <string name="setup_use_wireless">Usa Debug wireless</string> + <string name="setup_check_again">Controlla di nuovo</string> + <string name="setup_limit_turn_on">Attiva il limite</string> + <string name="setup_account">Account Steam</string> + <string name="setup_signed_in_offline">Accesso eseguito come %1$s · modalità offline</string> + <string name="setup_signed_in">Accesso eseguito come %1$s</string> + <string name="setup_sign_in">Premi Gioca e accedi a Steam</string> + <string name="setup_tools">Strumenti</string> + <string name="setup_controller">Controller</string> + <string name="setup_controller_unavailable">Le impostazioni del controller non sono disponibili.</string> + <string name="setup_session">Sessione</string> + <string name="setup_logs">Log della sessione</string> + <string name="setup_logs_hint">Salvati dopo ogni sessione</string> + <string name="setup_agent_commands">Strumenti di debug</string> + <string name="setup_latest_logs">Log dell\'ultima sessione</string> + <string name="setup_saved_logs">Log delle sessioni salvati</string> + <string name="setup_saved_logs_hint">Conserva i %1$d più recenti</string> + <string name="setup_clear_logs">Cancella log</string> + <string name="setup_offline">Modalità offline</string> + <string name="setup_sign_in_first">Accedi prima a Steam</string> + <string name="setup_launcher">Launcher</string> + <string name="setup_language">Lingua</string> + <string name="setup_language_hint">La usa anche Steam, dal suo prossimo avvio.</string> + <string name="setup_language_system">Predefinita di sistema (%1$s)</string> + <string name="setup_language_system_short">Predefinita di sistema</string> + <string name="setup_app_scale">Scala dell\'app</string> + <string name="setup_app_scale_hint">Testo e controlli di DroidDeck</string> + <string name="setup_app_scale_default">%1$d%% (predefinita)</string> + <string name="setup_theme">Tema</string> + <string name="setup_theme_hint">Aspetto del launcher</string> + <string name="setup_home">Usa come schermata Home</string> + <string name="setup_home_on">DroidDeck può essere l\'app Home</string> + <string name="setup_home_off">DroidDeck non viene mai proposta come app Home</string> + <string name="setup_orientation">Orientamento</string> + <string name="setup_orientation_auto">Orizzontale automatico</string> + <string name="setup_orientation_landscape">Orizzontale</string> + <string name="setup_orientation_reverse">Orizzontale invertito</string> + <string name="setup_fullscreen">Schermo intero</string> + <string name="setup_fullscreen_on">Nasconde la barra di stato e la barra di navigazione di Android</string> + <string name="setup_fullscreen_off">Mostra la barra di stato e la barra di navigazione di Android</string> + <string name="setup_default_home">App Home predefinita</string> + <string name="setup_choose_home">Scegli un\'app Home</string> + <string name="setup_choose">Scegli</string> + <string name="setup_linux_apps">App Linux (beta)</string> + <string name="setup_tool_files">File</string> + <string name="setup_tool_files_hint">Sfoglia e gestisci i file</string> + <string name="setup_tool_protons">Versioni di Proton</string> + <string name="setup_tool_protons_hint">Installa build ARM64 di Proton</string> + <string name="setup_tool_performance">Prestazioni</string> + <string name="setup_tool_performance_hint">Assegnazione dei core della CPU</string> + <string name="setup_tool_roms">Cartella ROM</string> + <string name="setup_tool_roms_hint">Dove sono salvati i giochi per emulatori</string> + <string name="setup_card_components">Componenti</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Controlli</string> + <string name="setup_card_controls_hint">Mappatura dei tasti</string> + <string name="store_tab_discover">Scopri</string> + <string name="store_tab_search">Cerca</string> + <string name="store_tab_installed">Installate</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Negozio</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak pronto</string> + <string name="store_needs_runtime">Installa prima il runtime Linux da Configurazione.</string> + <string name="store_setup_title">Configura Flatpak</string> + <string name="store_setup_text">Aggiunge Flatpak e lo collega a Flathub (circa 20 MB). La prima app scarica anche il suo runtime.</string> + <string name="store_setting_up">Configurazione…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Aggiornamenti</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d%%</string> + <string name="store_starting">Avvio…</string> + <string name="store_categories">Categorie</string> + <string name="store_unreachable">Impossibile raggiungere Flathub. Controlla la connessione e riprova.</string> + <string name="store_try_again">Riprova</string> + <string name="store_search_label">Cerca su Flathub</string> + <string name="store_search_placeholder">Firefox, giochi, emulatori…</string> + <string name="store_search">Cerca</string> + <string name="store_all">Tutte</string> + <string name="store_searching">Ricerca…</string> + <string name="store_search_hint">Cerca per nome o scegli una categoria. Vengono elencate solo app ARM64.</string> + <string name="store_no_results">Nessuna app ARM64 su Flathub corrisponde alla ricerca.</string> + <string name="store_results">Risultati</string> + <string name="store_check_updates">Controlla aggiornamenti</string> + <string name="store_update_all">Aggiorna tutte (%1$d)</string> + <string name="store_all_current">● Tutto è aggiornato</string> + <string name="store_no_apps">Ancora nessuna app. Trovane qualcuna in Scopri o Cerca.</string> + <string name="store_setup_first_note">Configura Flatpak per installare app.</string> + <string name="store_update_available_prefix">Aggiornamento disponibile · %1$s</string> + <string name="store_open">Apri</string> + <string name="store_details">Dettagli</string> + <string name="store_remove">Rimuovi</string> + <string name="store_press_again">Premi di nuovo per rimuovere</string> + <string name="store_installing">Installazione…</string> + <string name="store_install">Installa</string> + <string name="store_setup_first">Configura prima Flatpak</string> + <string name="store_download_size">%1$s da scaricare</string> + <string name="store_installed_chip">● Installata</string> + <string name="store_details_failed">Impossibile caricare la pagina Flathub di questa app.</string> + <string name="store_loading">Caricamento da Flathub…</string> + <string name="store_no_arm">Nessuna build ARM64 su Flathub, quindi non si può installare qui.</string> + <string name="store_screenshots">Screenshot</string> + <string name="store_about">Informazioni</string> + <string name="store_app_id">ID app</string> + <string name="store_license">Licenza</string> + <string name="store_runtime">Runtime</string> + <string name="store_installed_size">Dimensione installata</string> + <string name="store_installed_size_value">%1$s (più il suo runtime condiviso)</string> + <string name="store_website">Sito web</string> + <string name="store_verified">Verificata</string> + <string name="upd_title">Aggiornamenti</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Controllato %1$s</string> + <string name="upd_not_checked">Non ancora controllato</string> + <string name="upd_perm_title">Lascia che DroidDeck si aggiorni da sé</string> + <string name="upd_perm_text">Nella prossima schermata, attiva Consenti da questa fonte, poi torna qui. L\'aggiornamento continua da solo.</string> + <string name="upd_open_settings">Apri Impostazioni</string> + <string name="upd_not_now">Non ora</string> + <string name="upd_signed_status">Firma diversa</string> + <string name="upd_signed_headline">Impossibile aggiornare senza reinstallare</string> + <string name="upd_signed_detail">Questa copia non è firmata con la chiave di rilascio di DroidDeck, quindi gli aggiornamenti non possono installarsi sopra di essa. Disinstallala per passare all\'altra.</string> + <string name="upd_available">Aggiornamento disponibile</string> + <string name="upd_blocked_status">Impossibile installare</string> + <string name="upd_switch">Pronto per il passaggio</string> + <string name="upd_ahead_status">In anticipo su Stable</string> + <string name="upd_ahead_headline">Sei in anticipo su Stable</string> + <string name="upd_ahead_detail">Questa build è più recente dell\'ultima Stable. Passerai a Stable con la sua prossima versione.</string> + <string name="upd_gone_status">Test terminato</string> + <string name="upd_gone_headline">Questo test è terminato</string> + <string name="upd_gone_detail">La sua correzione è stata unita o scartata. Segui Preview per le correzioni più recenti.</string> + <string name="upd_current_status">Aggiornato</string> + <string name="upd_current_headline">Hai la versione più recente (%1$s)</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Aggiorna%1$s</string> + <string name="upd_install_button">Installa%1$s</string> + <string name="upd_install_anyway">Installa comunque Stable %1$s</string> + <string name="upd_follow_preview">Segui Preview</string> + <string name="upd_stop_session">Interrompi la sessione in corso per aggiornare.</string> + <string name="upd_less">Meno</string> + <string name="upd_more">Altro</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Nuova build Preview</string> + <string name="upd_new_test">Build di test della PR #%1$d</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">CANALE DI AGGIORNAMENTO</string> + <string name="upd_stable">Stable</string> + <string name="upd_stable_hint">Versioni testate, per la maggior parte delle persone</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Preview</string> + <string name="upd_preview_hint">Le correzioni più recenti, prima di Stable</string> + <string name="upd_preview_current">Aggiornato</string> + <string name="upd_tests">Build di test</string> + <string name="upd_tests_hint">Prova una correzione prima che venga rilasciata</string> + <string name="upd_tests_none">Nessuna al momento</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Non compilata per questa copia di DroidDeck</string> + <string name="upd_updated">Aggiornato %1$s</string> + <string name="upd_test_name">il test della PR #%1$d</string> + <string name="upd_while_ago">qualche tempo fa</string> + <string name="upd_just_now">proprio ora</string> + <plurals name="upd_tests_count"> + <item quantity="one">%1$d da provare</item> + <item quantity="many">%1$d da provare</item> + <item quantity="other">%1$d da provare</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="one">Modifiche rispetto alla tua build · %1$d build indietro</item> + <item quantity="many">Modifiche rispetto alla tua build · %1$d build indietro</item> + <item quantity="other">Modifiche rispetto alla tua build · %1$d build indietro</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="one">Modifiche recenti · %1$d build</item> + <item quantity="many">Modifiche recenti · %1$d build</item> + <item quantity="other">Modifiche recenti · %1$d build</item> + </plurals> + <string name="gate_title">Un\'impostazione di Android da cambiare</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Android arresta le app che avviano molti processi in background, e Steam ne avvia decine. Finché questo non cambia, i giochi si chiudono senza un errore.</string> + <string name="gate_fix_for_me">Risolvilo per me</string> + <string name="gate_toggle_steps">Attiva “Disattiva limitazioni dei processi figlio”, poi torna qui. Lascia attive le Opzioni sviluppatore, altrimenti l\'interruttore si disattiva di nuovo.</string> + <string name="gate_open_dev">Apri Opzioni sviluppatore</string> + <string name="gate_no_toggle">Questa versione di Android non ha un interruttore per questo, quindi DroidDeck lo cambia tramite il Debug wireless. Richiede circa un minuto, senza bisogno di un computer.</string> + <string name="gate_needs_wifi">Il Debug wireless richiede il Wi-Fi. Va bene qualsiasi rete; non serve l\'accesso a internet.</string> + <string name="gate_needs_notifications">Il codice di accoppiamento arriva in una notifica. Consenti le notifiche per DroidDeck, oppure usa Altri modi.</string> + <string name="gate_allow_notifications">Consenti notifiche</string> + <string name="gate_step1">Apri Impostazioni → Opzioni sviluppatore → Debug wireless e attivalo</string> + <string name="gate_step2">Tocca “Accoppia dispositivo con codice di accoppiamento”</string> + <string name="gate_step3">Digita il codice nella notifica di DroidDeck</string> + <string name="gate_waiting">In attesa del popup di accoppiamento…</string> + <string name="gate_code_needed">Popup trovato. Inserisci il codice nella notifica.</string> + <string name="gate_status">Stato</string> + <string name="gate_polling">Controllo ogni 2 secondi. Questa pagina si chiude quando è disattivato.</string> + <string name="gate_other_ways">Altri modi</string> + <string name="gate_other_toggle">Debug wireless, indirizzo manuale, ADB da computer</string> + <string name="gate_other_no_toggle">Indirizzo manuale, ADB da computer, Opzioni sviluppatore</string> + <string name="gate_use_wireless_instead">Usa invece il Debug wireless</string> + <string name="gate_some_roms">Alcune ROM aggiungono comunque l\'interruttore. Cerca “processi figlio” nelle Opzioni sviluppatore.</string> + <string name="gate_manual_hint">Se la notifica non funziona, inserisci tu l\'indirizzo e il codice del popup.</string> + <string name="gate_enter_manually">Inserisci l\'indirizzo manualmente</string> + <string name="gate_from_computer">Da un computer con ADB:</string> + <string name="gate_copy_command">Copia comando</string> + <string name="gate_choose_display">Scegli su quale schermo aprire le Impostazioni di Android.</string> + <string name="gate_main_screen">Schermo principale</string> + <string name="wadb_finding_port">Accoppiato. Ricerca della porta del Debug wireless…</string> + <string name="wadb_enter_port">Accoppiato. Inserisci la porta dalle Impostazioni.</string> + <string name="wadb_applying">Dispositivo trovato. Applicazione dell\'impostazione…</string> + <string name="wadb_apply_failed">Impossibile applicare automaticamente. Controlla l\'indirizzo qui sotto e riprova. %1$s</string> + <string name="wadb_title_wireless">Debug wireless</string> + <string name="wadb_title_done">Limite dei processi figlio aggiornato</string> + <string name="wadb_title">Cambia il limite dei processi figlio</string> + <string name="wadb_lede_compact0">Se l\'opzione non è presente, accoppia tramite il Debug wireless.</string> + <string name="wadb_lede_compact1">Accoppiato. Controlla l\'indirizzo IP e la porta nel Debug wireless.</string> + <string name="wadb_lede_done">Il limite dei processi figlio è stato aggiornato.</string> + <string name="wadb_lede0">Usa il passaggio 2 se le Opzioni sviluppatore non hanno un\'impostazione per i processi figlio. Apri Opzioni sviluppatore → Debug wireless → Accoppia dispositivo con codice di accoppiamento.</string> + <string name="wadb_lede1">Accoppiato. Usa l\'indirizzo qui sotto, oppure aggiornalo da Debug wireless → Indirizzo IP e porta.</string> + <string name="wadb_lede_done_long">Il limite dei processi figlio è stato aggiornato tramite il Debug wireless.</string> + <string name="wadb_confirmed_on">Android ha confermato che il limite è attivo.</string> + <string name="wadb_confirmed_off">Android ha confermato che il limite è disattivato.</string> + <string name="wadb_pair_group">2 · Accoppia il Debug wireless</string> + <string name="wadb_pair_address">Indirizzo IP e porta del popup di accoppiamento</string> + <string name="wadb_pin">Codice di accoppiamento (PIN)</string> + <string name="wadb_pin_placeholder">6 cifre</string> + <string name="wadb_pair">Accoppia</string> + <string name="wadb_apply_group">2 · Applica tramite il Debug wireless</string> + <string name="wadb_connect_address">Indirizzo IP e porta di connessione</string> + <string name="wadb_pair_again">Accoppia di nuovo</string> + <string name="wadb_turn_off">Disattiva il limite</string> + <string name="wadb_other_ways">Altri modi per cambiarlo</string> + <string name="wadb_dev_try_first">1 · Opzioni sviluppatore (da provare prima)</string> + <string name="wadb_restrict_on">Attiva “Limita processi figlio”.</string> + <string name="wadb_restrict_off">Disattiva “Limita processi figlio”.</string> + <string name="wadb_computer_last">3 · ADB da computer (ultima risorsa)</string> + <string name="wadb_only_if">Solo se il Debug wireless non è disponibile.</string> + <string name="wadb_computer_group">3 · ADB da computer (ultima risorsa)</string> + <string name="ctrl_osc_hint">Quando i controlli touch appaiono in una sessione</string> + <string name="ctrl_color">Colore</string> + <string name="ctrl_color_hint">Tonalità dei tasti sullo schermo</string> + <string name="ctrl_custom">Personalizzato</string> + <string name="ctrl_opacity">Opacità</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">Dimensione dei tasti</string> + <string name="ctrl_size_hint" formatted="false">100% è la dimensione standard</string> + <string name="ctrl_stick_click">Clic levetta</string> + <string name="ctrl_stick_click_hint">Tocca due volte e tieni premuta una levetta per L3 o R3</string> + <string name="ctrl_adaptive">Levette adattive</string> + <string name="ctrl_rumble">Vibrazione di gioco</string> + <string name="ctrl_steam_button">Tasto Steam</string> + <string name="ctrl_qam_button">Tasto QAM</string> + <string name="ctrl_keyboard_button">Tasto tastiera</string> + <string name="ctrl_select_steam">Premi Select per aprire il menu Steam</string> + <string name="ctrl_select_steam_hint">Invece di View/Select.</string> + <string name="setup_animations">Animazioni</string> + <string name="ctrl_adaptive_hint">Le levette appaiono dove tocchi e si nascondono al rilascio</string> + <string name="ctrl_layout">Layout</string> + <string name="ctrl_layout_custom">Posizioni personalizzate salvate</string> + <string name="ctrl_layout_default">Adattato a questo schermo</string> + <string name="ctrl_edit">Modifica</string> + <string name="ctrl_reset">Ripristina</string> + <string name="ctrl_mapping">Mappatura dei tasti</string> + <string name="ctrl_mapping_default">Ogni tasto invia il proprio input</string> + <string name="ctrl_mapping_count">%1$d di %2$d tasti rimappati</string> + <string name="ctrl_configure">Configura</string> + <string name="ctrl_reset_all">Ripristina controller</string> + <string name="ctrl_reset_all_hint">Ripristina tutte le impostazioni del controller e il layout</string> + <string name="ctrl_mapping_lede">Cosa invia al gioco ciascun tasto sullo schermo. Nascosto rimuove il tasto.</string> + <string name="ctrl_onscreen_buttons">Tasti sullo schermo</string> + <string name="ctrl_defaults">Predefiniti</string> + <string name="ctrl_reset_mapping">Ripristina mappatura</string> + <string name="content_android_apps">App Android</string> + <string name="content_apps_count">%1$d app</string> + <string name="content_no_android_apps">Nessuna app Android avviabile trovata.</string> + <string name="content_desktop_installed">● Desktop installato</string> + <string name="content_desktop_first_open">Si installa alla prima apertura</string> + <string name="content_available">Disponibile per l\'installazione</string> + <string name="content_not_installed">Non installato.</string> + <string name="content_not_installed_suffix">%1$s · non installato</string> + <string name="content_open_named">Apri %1$s</string> + <string name="content_games">Giochi</string> + <string name="content_choose_roms">Scegli una cartella ROM.</string> + <string name="content_retroarch_roms">Sfoglia fino a /root/ROMs in RetroArch.</string> + <string name="content_add_roms">Aggiungi i giochi %1$s a ROMs/%2$s.</string> + <string name="content_install_named">Installa %1$s</string> + <string name="content_stop_to_install">Interrompi la sessione per installare</string> + <string name="content_runtime_required">Runtime richiesto</string> + <string name="content_loading_details">Caricamento dei dettagli di installazione…</string> + <string name="content_details_unavailable">I dettagli di installazione non sono disponibili. Riprova più tardi.</string> + <string name="content_needs_runtime">Installa prima il runtime Linux da Configurazione.</string> + <string name="content_stop_first">Interrompi la sessione in corso per installare le app desktop.</string> + <string name="content_install_into">Installa %1$s nel desktop Linux.</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">Nasconderlo lo rimuove da Desktop. I suoi file restano nel runtime Linux.</string> + <string name="content_rom_gone">Quel gioco non è più nella cartella ROM.</string> + <string name="content_desktop_system">Desktop · %1$s</string> + <string name="content_launch_in">Avvia in %1$s</string> + <string name="content_also_in">Anche in %1$s</string> + <string name="content_page_gone">Quella pagina non esiste più. Scegli una sezione a sinistra.</string> + <string name="content_open_desktop">Apri desktop</string> + <string name="content_install_open_desktop">Installa e apri desktop</string> + <string name="desktop_remove_title">Disinstalla desktop</string> + <string name="desktop_remove_text">Rimuove il desktop e le sue app incluse. Mantiene giochi, app aggiunte, impostazioni e librerie condivise.</string> + <string name="desktop_removing">Rimozione del desktop</string> + <string name="desktop_removed">Desktop rimosso</string> + <string name="desktop_cleaning_legacy">Rimozione dei vecchi file LXQt</string> + <string name="desktop_removal_unknown">Aggiorna DroidDeck prima di rimuovere questa versione del desktop.</string> + <string name="content_linux_desktop">Desktop Linux</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox e i tuoi emulatori</string> + <string name="games_empty">I giochi che installi in Steam compaiono qui dopo la sessione.</string> + <string name="games_play_steam">Gioca su Steam</string> + <string name="games_launch_settings">Impostazioni di avvio</string> + <string name="games_launch">Avvia</string> + <string name="games_files">File di gioco</string> + <string name="games_prefix">Prefisso Proton</string> + <string name="games_saves_looking">Ricerca dei salvataggi…</string> + <string name="games_saves_none">Nessuna cartella di salvataggio trovata per nome. L\'esportazione prende l\'intera cartella utente.</string> + <string name="games_saves_folders">%1$d cartelle di salvataggio</string> + <string name="games_saves_summary">%1$s · %2$d file · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">Gestisci salvataggi</string> + <string name="games_saves">Salvataggi di gioco</string> + <string name="games_import_saves">Importa salvataggi…</string> + <string name="games_export_gamehub">Esporta come zip GameHub…</string> + <string name="games_export_winlator">Esporta come zip Winlator…</string> + <string name="games_open_save_folder">Apri cartella di salvataggio</string> + <string name="games_never_played">Mai giocato</string> + <string name="games_runtime_busy_percent">Runtime occupato · %1$d%%</string> + <string name="games_runtime_busy">Runtime occupato</string> + <string name="games_runtime_first_play">Il runtime si installa al primo Gioca</string> + <string name="games_runtime_update">Aggiornamento del runtime disponibile</string> + <string name="games_runtime_ready">● Runtime pronto</string> + <string name="games_last_played">Giocato l\'ultima volta %1$s</string> + <string name="games_internal">Memoria interna</string> + <string name="games_added">Gioco aggiunto</string> + <string name="games_custom">Gioco personalizzato</string> + <string name="games_store_storage">Archiviazione dei giochi</string> + <string name="gpu_auto_hint">La coppia migliore per questa GPU, mantenuta aggiornata</string> + <string name="gpu_manual_hint">Scegli tu una coppia</string> + <string name="gpu_checking">Ricerca dei driver…</string> + <string name="gpu_none_listed">Ancora nessun driver elencato. Aggiorna per cercarli.</string> + <string name="gpu_hide_other">Nascondi i driver per altre GPU</string> + <string name="gpu_show_other">Mostra i driver per altre GPU</string> + <string name="gpu_advanced">Avanzate</string> + <string name="gpu_set_with_display">Impostato insieme al driver video: %1$s include entrambi.</string> + <string name="gpu_set_with_runtime">Impostato insieme al driver del runtime: %1$s include entrambi.</string> + <string name="gpu_runtime_hint">Usato dai giochi e dal desktop.</string> + <string name="gpu_runtime_default">Runtime predefinito</string> + <string name="gpu_display_hint">Disegna i fotogrammi sullo schermo. Si applica dopo il riavvio di DroidDeck.</string> + <string name="gpu_auto_by_gpu">Auto - scelto in base alla GPU</string> + <string name="gpu_manage">Scarica, importa o elimina…</string> + <string name="gpu_in_use_caps">IN USO</string> + <string name="gpu_builtin">I driver integrati in DroidDeck</string> + <string name="gpu_matched_pair">Driver del runtime e video impostati insieme come coppia abbinata.</string> + <string name="gpu_recommended_for">Consigliato per questa GPU: %1$s.</string> + <string name="gpu_refresh_hint">Aggiorna per cercare driver per questa GPU.</string> + <string name="gpu_recommended_caps">CONSIGLIATO</string> + <string name="gpu_unavailable">Non disponibile</string> + <string name="load_runtime">Runtime Linux</string> + <string name="load_runtime_detail">Preparazione del runtime Linux</string> + <string name="load_session">Sessione</string> + <string name="load_session_detail">Avvio di schermo e audio</string> + <string name="load_steam">Client Steam</string> + <string name="load_steam_detail">Controllo di Steam e dei suoi strumenti di compatibilità</string> + <string name="load_opening_steam">Apertura di Steam</string> + <string name="load_opening_steam_detail">In attesa del primo frame di Steam</string> + <string name="load_desktop">Desktop</string> + <string name="load_desktop_detail">Preparazione del desktop</string> + <string name="load_opening">Apertura</string> + <string name="load_opening_detail">In attesa del primo frame</string> + <string name="load_starting_steam">Avvio di Steam</string> + <string name="load_ended">La sessione è terminata</string> + <string name="load_step">Passaggio %1$d di %2$d</string> + <string name="load_steam_paused">Steam è in pausa</string> + <string name="deck_tab_grips">Impugnature</string> + <string name="deck_tab_left">Pad sinistro</string> + <string name="deck_tab_right">Pad destro</string> + <string name="deck_tab_both">Entrambi</string> + <string name="deck_tab_gamepad">Gamepad</string> + <string name="deck_surface_stick">Levetta</string> + <string name="deck_surface_trackpad">Trackpad</string> + <string name="deck_surface_switch">Alterna tra levetta e trackpad</string> + <string name="deck_more_controls">Altri controlli Deck</string> + <string name="deck_panels">PANNELLI</string> + <string name="deck_close">Chiudi i controlli sul secondo schermo</string> + <string name="deck_open_steam_menu">Apri il menu Steam</string> + <string name="deck_left_upper">Sinistra · in alto</string> + <string name="deck_left_lower">Sinistra · in basso</string> + <string name="deck_right_upper">Destra · in alto</string> + <string name="deck_right_lower">Destra · in basso</string> + <string name="deck_click">Clic</string> + <string name="deck_right_trackpad">Trackpad destro</string> + <string name="deck_left_trackpad">Trackpad sinistro</string> + <string name="second_trackpad">Trackpad</string> + <string name="second_show_keyboard">Mostra tastiera</string> + <string name="second_hide_keyboard">Nascondi tastiera</string> + <string name="second_terminal">Terminale Linux</string> + <string name="second_terminal_not_ready">La sessione Linux non è ancora pronta per un terminale.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Giochi</string> + <string name="rail_desktop">Desktop</string> + <string name="rail_store">Negozio</string> + <string name="rail_components">Componenti</string> + <string name="rail_apps">App</string> + <string name="rail_setup">Configurazione</string> + <string name="rail_updates">Aggiornamenti</string> + <string name="rail_percent">%1$d%%</string> + <string name="rail_resume_named">Riprendi %1$s</string> + <string name="roms_text">Appare come /root/ROMs in ogni sessione. La memoria interna si trova in /root/Storage.</string> + <string name="roms_none">Nessuna cartella scelta</string> + <string name="roms_applies">Si applica dalla prossima sessione. Le schede SD funzionano.</string> + <string name="roms_choose">Scegli cartella</string> + <string name="roms_change">Cambia cartella</string> + <string name="proton_lede">Installa una build ARM64 di Proton, poi sceglila per ogni gioco in Steam → Proprietà → Compatibilità.</string> + <string name="proton_stop_first">Interrompi la sessione in corso per installare o rimuovere uno strumento di compatibilità.</string> + <string name="proton_available">Build disponibili</string> + <string name="proton_installed">Installata: %1$s</string> + <string name="proton_queued">In attesa da una richiesta precedente</string> + <string name="proton_not_installed">Non installata</string> + <string name="proton_update_available">Aggiornamento disponibile → %1$s</string> + <string name="proton_remove_older">Rimuovi le vecchie versioni di %1$s</string> + <string name="proton_remove_older_body">Rimuove %1$s. I giochi impostati su queste versioni devono passare a un altro Proton.</string> + <string name="proton_remove_body">I giochi impostati su questa versione devono passare a un altro Proton.</string> + <string name="proton_install_now">Installa ora</string> + <string name="proton_cancel_queued">Togli dalla coda</string> + <string name="pckb_android">Tastiera Android</string> + <string name="pckb_hide">Nascondi ✕</string> + <string name="main_launch_named">Avvia %1$s</string> + <string name="main_choose_screen">Scegli uno schermo. Secondario: %1$s.</string> + <string name="main_secondary_gone">Lo schermo secondario non è più disponibile.</string> + <string name="main_primary">Schermo principale</string> + <string name="main_secondary">Schermo secondario</string> + <string name="widgets_prev_tab">Scheda precedente</string> + <string name="widgets_next_tab">Scheda successiva</string> + <string name="widgets_on">Attivo</string> + <string name="widgets_all_n">Tutti i %1$d</string> + <string name="widgets_none">Nessuno</string> + <string name="widgets_n_of_m">%1$d di %2$d</string> + <string name="widgets_off">Disattivato</string> + <string name="screen_bottom">Schermo inferiore</string> + <string name="game_env_applies">Si applica al prossimo avvio del gioco. Il profilo di un gioco ha la precedenza su quello condiviso.</string> + <string name="game_env_other">Un altro gioco tramite ID app…</string> + <string name="game_env_done">Fine</string> + <string name="game_env_default">Predefinito</string> + <string name="game_env_off">Disattivato</string> + <string name="game_env_change">Cambia…</string> + <string name="game_env_change_title">Cambia variabile</string> + <string name="game_env_restore_default">Ripristina predefinito</string> + <string name="game_env_eyebrow">Giochi Proton</string> + <string name="pip_title">Picture in picture</string> + <string name="pip_auto">Passa automaticamente in PiP</string> + <string name="unfolded_controls">Controlli sullo schermo aperto</string> + <string name="unfolded_controls_hint">Da aperto, il gioco sta sopra i trackpad e le impugnature posteriori di Steam Deck. Da chiuso, il gioco è a schermo intero.</string> + <string name="pip_suspended">Sospeso</string> + <string name="pip_not_ready">Il picture in picture è disponibile quando la sessione è pronta.</string> + <string name="pip_permission">Consenti il picture in picture per DroidDeck nelle impostazioni di Android.</string> + <string name="pip_unavailable">Android non è riuscito ad aprire il picture in picture.</string> + <string name="game_shortcut">Scorciatoia</string> + <string name="game_shortcut_add">Aggiungi alla schermata Home</string> + <string name="game_shortcut_choose">Scegli gioco</string> + <string name="game_shortcut_scanning">Ricerca dei giochi installati…</string> + <string name="game_link_copy">Copia link di avvio</string> + <string name="game_link_copied">Link di avvio copiato</string> + <string name="game_shortcut_unsupported">Questo launcher non può aggiungere scorciatoie. Usa Copia link di avvio.</string> + <string name="game_link_invalid">Link di avvio del gioco non valido.</string> + <string name="game_link_missing">Questo gioco non è più nella tua libreria DroidDeck.</string> + <string name="game_link_session_busy">Interrompi la sessione attuale prima di avviare un gioco Steam.</string> + <string name="game_link_failed">Impossibile inviare il gioco a Steam. Prova ad avviarlo di nuovo.</string> + <string name="setup_retry_removal">Riprova rimozione</string> + <string name="runtime_removing">Rimozione</string> + <string name="runtime_removal_incomplete">Rimozione incompleta</string> + <string name="game_file_export">Esporta file del gioco</string> + <string name="game_file_sync">Sincronizza tutti i file dei giochi nella cartella</string> + <string name="game_file_stop_sync">Interrompi sincronizzazione dei file dei giochi</string> + <string name="game_file_folder">Scegli una cartella per i file dei giochi del frontend</string> + <string name="game_file_exported">File dei giochi salvati in %1$s</string> + <string name="game_frontend_files">File del frontend</string> + + <string name="main_wait_runtime">Attendi la fine dell\'operazione sul runtime</string> + <string name="main_wait_proton_install">Attendi la fine dell\'installazione dello strumento di compatibilità</string> + <string name="main_wait_desktop_install">Attendi la fine dell\'installazione dell\'app desktop</string> + <string name="main_wait_install_game">Attendi la fine dell\'installazione prima di avviare un gioco</string> + <string name="main_saves_close_session">Chiudi prima la sessione Steam, così il gioco non potrà sovrascrivere i dati importati</string> + <string name="main_saves_importing">Importazione in %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="one">%1$d file importato (%2$s) in %3$s</item> + <item quantity="many">%1$d file importati (%2$s) in %3$s</item> + <item quantity="other">%1$d file importati (%2$s) in %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">%1$d file importato (%2$s) in %3$s. Backup dei vecchi salvataggi in Download/DroidDeck/Saves/backups</item> + <item quantity="many">%1$d file importati (%2$s) in %3$s. Backup dei vecchi salvataggi in Download/DroidDeck/Saves/backups</item> + <item quantity="other">%1$d file importati (%2$s) in %3$s. Backup dei vecchi salvataggi in Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Scegli uno zip di salvataggi per %1$s</string> + <string name="main_saves_exporting">Esportazione di %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="one">%1$d file esportato come %2$s: %3$s</item> + <item quantity="many">%1$d file esportati come %2$s: %3$s</item> + <item quantity="other">%1$d file esportati come %2$s: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Scegli dove salvare %1$s (%2$s)</string> + <string name="main_action_failed">Operazione non riuscita (%1$s): %2$s</string> + <string name="main_no_logs_yet">Ancora nessun log di sessione: avvia prima una sessione.</string> + <string name="main_no_logs_to_clear">Nessun log di sessione da cancellare.</string> + <plurals name="main_logs_cleared"> + <item quantity="one">%1$d cartella di log di sessione cancellata.</item> + <item quantity="many">%1$d cartelle di log di sessione cancellate.</item> + <item quantity="other">%1$d cartelle di log di sessione cancellate.</item> + </plurals> + <string name="main_wadb_pair_failed">Accoppiamento per il debug wireless non riuscito</string> + <string name="main_wadb_command_failed">Comando di debug wireless non riuscito</string> + <string name="main_wadb_unavailable">Il debug wireless non è disponibile</string> + <string name="main_adb_copied">Comando ADB copiato</string> + <string name="main_pick_roms">Scegli la cartella delle ROM</string> + <string name="main_non_adreno_title">Non è una GPU Adreno</string> + <string name="main_non_adreno_text">Turnip supporta le GPU Adreno. Su %1$s, Steam potrebbe mostrare una schermata nera. Download: %2$.0f MB.</string> + <string name="main_install_anyway">Installa comunque</string> + <string name="main_remove_runtime_title">Rimuovi runtime Linux</string> + <string name="main_remove_runtime_text">Verranno eliminati il runtime, il client Steam al suo interno e tutti i giochi installati lì.</string> + <string name="main_applied">Applicato: %1$s</string> + <string name="android_app_open_failed_primary">Impossibile aprire %1$s sullo schermo principale</string> + <string name="android_app_open_failed_on">Impossibile aprire %1$s su %2$s</string> + <string name="android_app_open_failed_display">Impossibile aprire %1$s sullo schermo %2$d</string> + <string name="comp_busy_importing">Importazione</string> + <string name="comp_busy_swapping">Sostituzione</string> + <string name="comp_busy_restoring">Ripristino</string> + <string name="comp_busy_cancelling">Annullamento</string> + <string name="comp_busy_deleting">Eliminazione</string> + <string name="comp_cannot_read">impossibile leggere il file</string> + <string name="comp_imported">Importato %1$s</string> + <string name="comp_swap_cancelled">La sostituzione in attesa è stata annullata.</string> + <string name="comp_pick_wcp">Scegli un pacchetto di componenti (-linux .wcp)</string> + <string name="comp_nightlies_unreachable">Impossibile raggiungere le build Nightly</string> + <string name="comp_stored">Salvato %1$s</string> + <string name="comp_download_failed">Download non riuscito: %1$s</string> + <string name="drivers_pick_linux">Scegli un driver per il runtime Linux (zip -Linux o pacchetto Android + Linux)</string> + <string name="drivers_pick_android">Scegli un driver video (zip AdrenoTools o pacchetto Android + Linux)</string> + <string name="drivers_pick_any">Scegli uno zip di driver o un pacchetto Android + Linux</string> + <string name="main_pkg_problem">%1$s: %2$s</string> + <string name="main_removing_named">Rimozione di %1$s…</string> + <string name="main_forgetting_named">Rimozione di %1$s dall\'elenco…</string> + <string name="main_pick_game_storage">Scegli la cartella di archiviazione dei giochi</string> + <string name="main_pick_decky_zip">Scegli uno ZIP di plugin Decky</string> + <string name="main_storage_not_usable">Non utilizzabile: %1$s</string> + <string name="main_running_program">Programma</string> + <string name="main_removing_runtime">Rimozione del runtime Linux</string> + <string name="main_settings_main_screen">Impossibile aprire Impostazioni sullo schermo inferiore; aperte sullo schermo principale.</string> + <string name="main_open_dev_options">Apri Opzioni sviluppatore in Impostazioni</string> + + <string name="upd_unknown_apps_failed">Impossibile aprire l\'impostazione Android “Installa app sconosciute”</string> + <string name="upd_couldnt_check">Impossibile controllare: %1$s</string> + <string name="upd_no_download">Questa build non ha un download per questa copia di DroidDeck</string> + <string name="upd_stop_first">Interrompi prima la sessione in corso, poi aggiorna</string> + <string name="upd_stage_downloading">Download</string> + <string name="upd_stage_installing">Installazione</string> + <string name="upd_stage_confirm">In attesa della tua conferma</string> + <string name="upd_failed">Aggiornamento non riuscito</string> + <string name="upd_not_offered">Android non ha proposto di installare l\'aggiornamento</string> + + <string name="decky_install_failed">Installazione di Decky non riuscita: %1$s</string> + <string name="decky_plugin_starting">Avvio dell\'importazione del plugin…</string> + <string name="decky_plugin_failed">Importazione del plugin non riuscita: %1$s</string> + <string name="decky_plugin_installed">Plugin installato. Riavvia Steam per caricarlo.</string> + + <string name="drivers_not_checked">Non ancora controllato - tocca aggiorna per cercare nuovi driver</string> + <string name="drivers_bundle_sets_display">Android + Linux: imposta anche il driver video</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: imposta anche il driver del runtime</string> + <string name="drivers_runtime_builtin">il Turnip integrato nel runtime</string> + <string name="drivers_system_vulkan">Vulkan di sistema: nessuna build inclusa per questa GPU</string> + <string name="drivers_bundled">%1$s (incluso)</string> + <string name="drivers_manual">Manuale</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">Al momento ne è pubblicata solo una metà</string> + <string name="drivers_pair_size">%1$.0f MB per entrambi</string> + <string name="drivers_none_to_set">Nessun driver da impostare: %1$s</string> + <string name="drivers_checking_latest">Ricerca dei driver più recenti…</string> + <string name="drivers_up_to_date_check_failed">Aggiornato (impossibile controllare: %1$s)</string> + <string name="drivers_recommended_unpublished">Al momento i driver consigliati non sono pubblicati</string> + <string name="drivers_downloading_pair">Download di %1$s %2$s…</string> + <string name="drivers_updated_to">Aggiornato a %1$s %2$s.</string> + <string name="drivers_updated_to_restart">Aggiornato a %1$s %2$s. Il driver video si applica dopo il riavvio di DroidDeck.</string> + <string name="drivers_switched_to">Passato a %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Passato a %1$s %2$s. Il driver video si applica dopo il riavvio di DroidDeck.</string> + <string name="drivers_using">In uso: %1$s %2$s.</string> + <string name="drivers_using_restart">In uso: %1$s %2$s. Il driver video si applica dopo il riavvio di DroidDeck.</string> + <string name="drivers_not_a_driver">Non è uno zip di driver: non è un driver AdrenoTools, un Turnip -Linux né un pacchetto Android + Linux</string> + <string name="drivers_import_failed">Importazione non riuscita: %1$s</string> + <string name="drivers_imported_unnamed">Driver importato</string> + <string name="drivers_using_bundle">%1$s in uso sia come driver del runtime sia come driver video.</string> + <string name="drivers_using_bundle_restart">%1$s in uso sia come driver del runtime sia come driver video. Il driver video si applica dopo il riavvio di DroidDeck.</string> + <string name="drivers_deleted_bundle">Eliminato %1$s (entrambi i driver)</string> + <string name="drivers_deleted">Eliminato %1$s</string> + <string name="drivers_release_status">Ultimi: %1$s · controllato %2$s</string> + <string name="drivers_release_status_failed">Ultimi: %1$s · %2$s non raggiungibile · controllato %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">%1$d min fa</item> + <item quantity="many">%1$d min fa</item> + <item quantity="other">%1$d min fa</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">%1$d h fa</item> + <item quantity="many">%1$d h fa</item> + <item quantity="other">%1$d h fa</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">%1$d giorno fa</item> + <item quantity="many">%1$d giorni fa</item> + <item quantity="other">%1$d giorni fa</item> + </plurals> + <string name="drivers_checking_sources">Controllo di Banners-Turnip, WinNative e DroidDeck…</string> + <string name="drivers_check_failed">controllo non riuscito</string> + <string name="drivers_installed_pick_either">Installato %1$s - sceglilo in uno dei due elenchi per impostare entrambi i driver</string> + <string name="drivers_installed_pick">Installato %1$s - sceglilo nel menu</string> + + <string name="session_wait_removal">Attendi la fine della rimozione del runtime</string> + <string name="session_title_game">Gioco</string> + <string name="session_touch_auto_touchpad">touchpad</string> + <string name="session_touch_auto_direct">diretto</string> + <string name="session_swap_failed">Sostituzione non riuscita: %1$s</string> + <string name="session_profile_save_failed">Salvataggio del profilo non riuscito: %1$s</string> + <string name="session_desktop_paused">Il desktop è in pausa</string> + <string name="session_named_paused">%1$s è in pausa</string> + <string name="session_program_paused">Il programma è in pausa</string> + <string name="session_starting_desktop">Avvio del desktop</string> + <string name="session_starting_named">Avvio di %1$s</string> + <string name="session_starting_program">Avvio del programma</string> + <string name="session_no_logs">Nessun log per questa sessione.</string> + <string name="session_logs_archive_failed">Impossibile creare l\'archivio dei log della sessione.</string> + <string name="session_logs_share_failed">Impossibile condividere i log della sessione.</string> + <string name="session_starting_session">Avvio della sessione…</string> + <string name="session_runtime_downloading">download del runtime Linux</string> + <string name="session_runtime_downloading_mb">download del runtime Linux · %1$d di %2$d MB</string> + <string name="session_runtime_checking">verifica del runtime Linux</string> + <string name="session_runtime_unpacking">estrazione del runtime Linux</string> + <string name="session_desktop_downloading">download del desktop</string> + <string name="session_desktop_downloading_mb">download del desktop · %1$d di %2$d MB</string> + <string name="session_desktop_checking">verifica del desktop</string> + <string name="session_desktop_unpacking">estrazione del desktop</string> + <string name="session_proton_downloading">download di Proton Experimental (ARM64)</string> + <string name="session_proton_downloading_mb">download di Proton Experimental (ARM64) · %1$d di %2$d MB</string> + <string name="session_proton_checking">verifica di Proton Experimental (ARM64)</string> + <string name="session_proton_unpacking">estrazione di Proton Experimental (ARM64)</string> + <string name="session_runtime_catalog_unreachable">Impossibile raggiungere il catalogo dei runtime. Controlla la connessione e premi di nuovo Gioca.</string> + <string name="session_runtime_install_failed">Il runtime Linux non è stato installato. Controlla il runtime in Configurazione e premi di nuovo Gioca.</string> + <string name="session_desktop_install_failed">Il desktop non è stato installato (%1$s). Controlla la connessione e premi di nuovo Desktop.</string> + <string name="session_ended_named">%1$s si è interrotto in modo imprevisto. Condividi i log in una segnalazione di bug o riprova.</string> + <string name="session_ended_program">Il programma si è interrotto in modo imprevisto. Condividi i log in una segnalazione di bug o riprova.</string> + <string name="session_ended_desktop">Il desktop si è interrotto in modo imprevisto. Condividi i log in una segnalazione di bug o riprova.</string> + <string name="session_ended_detail">Stato di uscita %1$d · %2$s</string> + <string name="session_ended_no_log">nessun log</string> + <plurals name="session_hint_enosys"> + <item quantity="one">Il log mostra %1$d errore “Function not implemented”: l\'accelerazione seccomp di proot fa fallire un helper su questo dispositivo. Prova %2$s → “%3$s” (o “%4$s”) e riavvia.</item> + <item quantity="many">Il log mostra %1$d errori “Function not implemented”: l\'accelerazione seccomp di proot fa fallire un helper su questo dispositivo. Prova %2$s → “%3$s” (o “%4$s”) e riavvia.</item> + <item quantity="other">Il log mostra %1$d errori “Function not implemented”: l\'accelerazione seccomp di proot fa fallire un helper su questo dispositivo. Prova %2$s → “%3$s” (o “%4$s”) e riavvia.</item> + </plurals> + + <string name="mode_decky_plugins">Plugin</string> + <string name="mode_decky_plugins_hint">Installa uno ZIP di plugin per la prossima sessione Steam.</string> + <string name="mode_decky_plugin_downloading">Download…</string> + <string name="mode_decky_plugin_install_zip">Installa da ZIP</string> + <string name="store_cat_games">Giochi</string> + <string name="store_cat_audio_video">Audio e video</string> + <string name="store_cat_graphics">Grafica</string> + <string name="store_cat_internet">Internet</string> + <string name="store_cat_office">Ufficio</string> + <string name="store_cat_developer">Sviluppo</string> + <string name="store_cat_education">Istruzione</string> + <string name="store_cat_science">Scienza</string> + <string name="store_cat_system">Sistema</string> + <string name="store_cat_utilities">Utilità</string> + <string name="store_section_popular">Popolari</string> + <string name="store_section_trending">Di tendenza</string> + <string name="store_section_new">Novità</string> + <string name="store_section_updated">Aggiornate</string> + <string name="store_section_loading">caricamento…</string> + <string name="store_flatpak_setup">Configurazione di Flatpak</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Impossibile cercare aggiornamenti su Flathub</string> + <string name="content_rom_installed">installato</string> + <string name="content_rom_folder">cartella</string> + <string name="theme_graphite">Grafite</string> + <string name="theme_graphite_detail">Superfici color grafite, con il blu della sfera del logo per tutto ciò che è selezionato, attivo o premuto.</string> + <string name="theme_paper">Carta su nero</string> + <string name="theme_paper_detail">Il bianco è il colore principale, il blu dell\'icona fa da segnale per linee, anelli e punti.</string> + <string name="theme_icon_blue">Blu icona</string> + <string name="theme_icon_blue_detail">Fondo nero, testo bianco, il blu per tutto ciò che è selezionato, attivo o premuto.</string> + <string name="theme_electric">Blu elettrico</string> + <string name="theme_electric_detail">Il blu più brillante, su superfici che tendono al blu navy invece che al grigio.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Canc</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">Fine</string> + <string name="pckb_key_pgup">PgSu</string> + <string name="pckb_key_pgdn">PgGiù</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Bloc Maiusc</string> + <string name="pckb_key_enter">Invio ⏎</string> + <string name="pckb_key_shift">⇧ Maiusc</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Spazio</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <string name="game_sync_err_runtime">Installa prima il runtime</string> + <string name="game_sync_err_steam_library">La libreria di Steam non è disponibile</string> + <string name="game_sync_err_previous_sd">La libreria SD precedente non è disponibile</string> + <string name="game_sync_err_game_storage">L\'archivio dei giochi non è disponibile</string> + <string name="game_sync_err_added_games">La cartella dei giochi aggiunti non è disponibile</string> + <string name="game_sync_err_manifests">I manifest di Steam non sono disponibili</string> + <string name="content_system_many">Molti sistemi</string> + <string name="fm_drive_internal">Interna</string> + <string name="fm_drive_linux">Sistema Linux</string> + <string name="fm_drive_sd">Scheda SD</string> + <string name="fm_drive_storage">Memoria</string> + <string name="fm_select">Seleziona</string> + <string name="fm_properties">Proprietà</string> + <string name="fm_favorite_add">Aggiungi ai preferiti</string> + <string name="fm_favorite_remove">Rimuovi dai preferiti</string> + <string name="fm_rename">Rinomina</string> + <string name="fm_copy">Copia</string> + <string name="fm_cut">Taglia</string> + <string name="fm_actions">Azioni</string> + <string name="fm_favorites">Preferiti</string> + <string name="fm_pin_current">Fissa la cartella attuale</string> + <string name="fm_no_favorites">Ancora nessun preferito - fissa una cartella dal suo menu ⋮ per tornarci in un attimo.</string> + <string name="fm_unpin_favorite">Rimuovi dai preferiti</string> + <string name="fm_size_zero">0 byte</string> + <string name="fm_unit_bytes">byte</string> + <string name="fm_prop_location">Posizione</string> + <string name="fm_prop_type">Tipo</string> + <string name="fm_prop_size">Dimensioni</string> + <string name="fm_prop_modified">Modificato</string> + <string name="fm_type_folder">Cartella</string> + <string name="fm_type_file">File</string> + <string name="fm_type_ext_file">File %1$s</string> + <string name="fm_read_only">Sola lettura</string> + <string name="fm_read_only_hint">Impedisce ai giochi (e a Wine) di sovrascrivere o eliminare questo file.</string> + <string name="fm_read_only_failed">Impossibile cambiare Sola lettura</string> + <string name="fm_hidden">Nascosto</string> + <string name="fm_hidden_unsupported">Questa memoria non può salvare l\'attributo nascosto.</string> + <string name="fm_hidden_hint">Contrassegna il file come nascosto in Windows (attributo nascosto di Wine).</string> + <string name="fm_hidden_failed">Impossibile cambiare Nascosto su questa memoria</string> + <string name="fm_done">Fine</string> + <string name="fm_op_deleting">Eliminazione...</string> + <string name="fm_delete_failed">Eliminazione non riuscita</string> + <string name="fm_op_moving">Spostamento...</string> + <string name="fm_op_copying">Copia in corso...</string> + <string name="fm_op_moving_item">Spostamento - %1$s</string> + <string name="fm_op_copying_item">Copia - %1$s</string> + <string name="fm_op_moving_batch">Spostamento %1$d/%2$d - %3$s</string> + <string name="fm_op_copying_batch">Copia %1$d/%2$d - %3$s</string> + <string name="fm_paste_failed">%1$d completati, %2$d non riusciti</string> + <string name="fm_paste_skipped">%1$d completati, %2$d saltati</string> + <plurals name="fm_paste_moved"> + <item quantity="one">%1$d elemento spostato</item> + <item quantity="many">%1$d elementi spostati</item> + <item quantity="other">%1$d elementi spostati</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">%1$d elemento copiato</item> + <item quantity="many">%1$d elementi copiati</item> + <item quantity="other">%1$d elementi copiati</item> + </plurals> + <string name="fm_already_exists">“%1$s” esiste già</string> + <string name="fm_op_renaming">Rinomina in corso...</string> + <string name="fm_rename_failed">Impossibile rinominare</string> + <string name="fm_op_creating_folder">Creazione della cartella...</string> + <string name="fm_create_folder_failed">Impossibile creare la cartella</string> + <string name="fm_new_folder">Nuova cartella</string> + <string name="fm_folder_name">Nome cartella</string> + <string name="fm_create">Crea</string> + <string name="fm_new_name">Nuovo nome</string> + <string name="fm_delete_title">Eliminare?</string> + <string name="fm_delete_text">Eliminare definitivamente “%1$s”?</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">Eliminare %1$d elemento?</item> + <item quantity="many">Eliminare %1$d elementi?</item> + <item quantity="other">Eliminare %1$d elementi?</item> + </plurals> + <string name="fm_cannot_undo">L\'operazione non può essere annullata.</string> + <plurals name="fm_and_more"> + <item quantity="one">…e %1$d altro</item> + <item quantity="many">…e altri %1$d</item> + <item quantity="other">…e altri %1$d</item> + </plurals> + <string name="fm_op_deleting_batch">Eliminazione %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">Impossibile eliminare %1$d elemento</item> + <item quantity="many">Impossibile eliminare %1$d elementi</item> + <item quantity="other">Impossibile eliminare %1$d elementi</item> + </plurals> + <string name="fm_conflict_merge_hint">Unisci aggiunge e sostituisce i file nella cartella esistente.</string> + <string name="fm_conflict_overwrite_hint">Sovrascrivi sostituisce il file esistente.</string> + <string name="fm_conflict_apply_all">Applica a tutti i conflitti</string> + <string name="fm_conflict_merge">Unisci</string> + <string name="fm_conflict_overwrite">Sovrascrivi</string> + <string name="fm_conflict_keep_both">Mantieni entrambi</string> + <string name="fm_conflict_skip">Salta</string> + <string name="fm_select_this_folder">Seleziona questa cartella</string> + <string name="fm_back">Indietro</string> + <string name="fm_drive_not_readable">%1$s: montata ma al momento non leggibile</string> + <string name="fm_list_view">Visualizzazione elenco</string> + <string name="fm_grid_view">Visualizzazione griglia</string> + <string name="fm_search">Cerca</string> + <string name="fm_sort">Ordina</string> + <string name="fm_sort_name">Nome</string> + <string name="fm_sort_date">Data di modifica</string> + <string name="fm_rows_comfortable">Righe ampie</string> + <string name="fm_rows_compact">Righe compatte</string> + <string name="fm_hide_hidden">Nascondi file nascosti</string> + <string name="fm_show_hidden">Mostra file nascosti</string> + <string name="fm_hide_favorites">Nascondi preferiti</string> + <string name="fm_show_favorites">Mostra preferiti</string> + <string name="fm_filter_folder">Filtra questa cartella</string> + <string name="fm_free_space">%1$s liberi</string> + <string name="fm_selected_count">%1$d selezionati</string> + <string name="fm_select_none">Nessuno</string> + <string name="fm_select_all">Tutti</string> + <string name="fm_paste">Incolla</string> + <plurals name="fm_items"> + <item quantity="one">%1$d elemento</item> + <item quantity="many">%1$d elementi</item> + <item quantity="other">%1$d elementi</item> + </plurals> + <string name="fm_paste_here">Incolla %1$s qui</string> + <string name="fm_paste_move_here">Incolla %1$s qui (sposta)</string> + <string name="fm_rail_title">File</string> + <string name="fm_rail_storage">UNITÀ</string> + <string name="fm_rail_quick">ACCESSO RAPIDO</string> + <string name="fm_rail_favorites">PREFERITI</string> + <string name="fm_rail_downloads">Download</string> + <string name="fm_rail_downloads_short">Download</string> + <string name="fm_rail_roms">ROM</string> + <string name="fm_rail_session_logs">Log di sessione</string> + <string name="fm_rail_pictures">Immagini</string> + <string name="fm_favorite_added">“%1$s” aggiunta ai preferiti</string> + <string name="fm_favorite_removed">“%1$s” rimossa dai preferiti</string> + <string name="fm_empty_directory">Cartella vuota</string> + <string name="fm_up_one_level">Livello superiore</string> + <string name="fm_choose_folder">Scegli una cartella</string> + <string name="fm_choose_file">Scegli un file</string> + <string name="ctrl_tint_steam_blue">Blu Steam</string> + <string name="ctrl_tint_sky">Cielo</string> + <string name="ctrl_tint_white">Bianco</string> + <string name="ctrl_tint_violet">Viola</string> + <string name="ctrl_tint_green">Verde</string> + <string name="ctrl_tint_amber">Ambra</string> + <string name="ctrl_tint_red">Rosso</string> + <string name="ctrl_tint_pink">Rosa</string> + <string name="ctrl_button_a">Pulsante A</string> + <string name="ctrl_button_b">Pulsante B</string> + <string name="ctrl_button_x">Pulsante X</string> + <string name="ctrl_button_y">Pulsante Y</string> + <string name="ctrl_button_lb">Dorsale sinistro</string> + <string name="ctrl_button_rb">Dorsale destro</string> + <string name="ctrl_button_lt">Grilletto sinistro</string> + <string name="ctrl_button_rt">Grilletto destro</string> + <string name="ctrl_button_view">Pulsante Visualizza</string> + <string name="ctrl_button_menu">Pulsante Menu</string> + <string name="ctrl_target_view">Visualizza</string> + <string name="ctrl_target_menu">Menu</string> + <string name="ctrl_target_up">Croce su</string> + <string name="ctrl_target_down">Croce giù</string> + <string name="ctrl_target_left">Croce sinistra</string> + <string name="ctrl_target_right">Croce destra</string> + <string name="ctrl_target_hidden">Nascosto</string> + <string name="ctrl_editor_drag">Trascina per spostare</string> + <string name="ctrl_editor_snap">Aggancia</string> + <string name="ctrl_editor_save">Salva</string> + <string name="second_mode_none">Nessuno</string> + <string name="second_mode_keyboard_trackpad">Tastiera + trackpad</string> + <string name="second_mode_terminal">Terminale</string> + <string name="second_mode_deck_controls">Impugnature Deck + trackpad</string> + + <string name="rtinst_removing">Rimozione del runtime Linux</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">Rimozione del runtime Linux · %1$d elemento</item> + <item quantity="many">Rimozione del runtime Linux · %1$d elementi</item> + <item quantity="other">Rimozione del runtime Linux · %1$d elementi</item> + </plurals> + <string name="rtinst_removed">Runtime Linux rimosso</string> + <string name="rtinst_removal_failed">Impossibile completare la rimozione del runtime Linux. Riprova la rimozione in Configurazione.</string> + <string name="rtinst_removing_leftovers">Rimozione dei residui del runtime</string> + <string name="rtinst_downloading">Download</string> + <string name="rtinst_verifying">Verifica</string> + <string name="rtinst_extracting">Estrazione</string> + <string name="rtinst_channel_description">Visibile durante l\'installazione o la rimozione del runtime Linux</string> + <string name="rtinst_title_installing">Installazione del runtime Linux</string> + <string name="rtinst_title_removing">Rimozione del runtime Linux</string> + + <string name="deskpkg_runtime_missing">Il runtime Linux non è installato</string> + <string name="deskpkg_no_checksum">Il catalogo non ha un checksum per %1$s</string> + <string name="deskpkg_checksum_mismatch">Checksum non corrispondente - non è stato modificato nulla</string> + <string name="deskpkg_installing">Installazione di %1$s</string> + <string name="deskpkg_place_failed">Impossibile posizionare l\'AppImage</string> + <string name="deskpkg_extract_failed">Estrazione non riuscita</string> + <string name="deskpkg_install_failed">Installazione non riuscita</string> + + <string name="flatpakmgr_setup_failed">Configurazione di Flatpak non riuscita (uscita %1$d)</string> + <string name="flatpakmgr_setup_unfinished">La configurazione di Flatpak non è stata completata</string> + <string name="flatpakmgr_busy">È in corso un\'altra operazione del Negozio</string> + <string name="flatpakmgr_not_ready">Configura prima Flatpak</string> + <string name="flatpakmgr_starting">Avvio</string> + <string name="flatpakmgr_exit_status">Flatpak è terminato con stato %1$d</string> + <string name="flatpakmgr_installing">Installazione di %1$s</string> + <string name="flatpakmgr_updating">Aggiornamento di %1$s</string> + <string name="flatpakmgr_removing">Rimozione di %1$s</string> + <string name="flatpakmgr_installing_step">Installazione di %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_updating_step">Aggiornamento di %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_removing_step">Rimozione di %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_ref_translations">traduzioni</string> + <string name="flatpakmgr_ref_graphics_drivers">driver grafici</string> + <string name="flatpakmgr_ref_media_codecs">codec multimediali</string> + <string name="flatpakmgr_ref_runtime">runtime %1$s</string> + <string name="flatpakmgr_ref_runtime_branch">runtime %1$s %2$s</string> + + <string name="appimg_too_small">Il file è troppo piccolo per essere un AppImage</string> + <string name="appimg_unreadable">Impossibile leggere il file: %1$s</string> + <string name="appimg_not_linux">Questo non è un AppImage (non è un programma Linux)</string> + <string name="appimg_wrong_arch">Questo AppImage non è compilato né per ARM64 (aarch64) né per PC x86</string> + <string name="appimg_not_appimage">Questo è un programma Linux, ma non un AppImage</string> + <string name="appimg_wrong_type">Si possono importare solo AppImage di tipo 2 (questo è di tipo %1$d)</string> + <string name="appimg_no_space_import">Spazio libero insufficiente: l\'importazione richiede circa %1$s</string> + <string name="appimg_no_space_update">Spazio libero insufficiente: l\'aggiornamento richiede circa %1$s</string> + <string name="appimg_import_failed">Importazione non riuscita</string> + <string name="appimg_gone">Quell\'AppImage non c\'è più</string> + <string name="appimg_update_failed">Aggiornamento non riuscito</string> + <string name="appimg_extracting">Estrazione di %1$s</string> + <string name="appimg_extract_failed">Impossibile estrarre l\'AppImage</string> + <string name="appimg_extract_failed_detail">Impossibile estrarre l\'AppImage: %1$s</string> + + <string name="deckymgr_stop_session_loader">Interrompi la sessione attiva prima di installare Decky Loader</string> + <string name="deckymgr_download_failed">Download di Decky Loader non riuscito</string> + <string name="deckymgr_size_mismatch">La dimensione di Decky Loader non corrisponde ai metadati della release</string> + <string name="deckymgr_checksum_download_failed">Impossibile scaricare il checksum della release</string> + <string name="deckymgr_checksum_missing">Checksum della release mancante</string> + <string name="deckymgr_checksum_invalid">Checksum della release non valido</string> + <string name="deckymgr_checksum_mismatch">Checksum di Decky Loader non corrispondente</string> + <string name="deckymgr_not_elf">Il file della release non è un eseguibile Linux a 64 bit compatibile</string> + <string name="deckymgr_session_started_loader">È stata avviata una sessione durante il download; interrompila prima di installare Decky Loader</string> + <string name="deckymgr_loader_not_executable">Impossibile rendere eseguibile PluginLoader</string> + <string name="deckymgr_cef_failed">Impossibile attivare il debug remoto CEF di Steam</string> + <string name="deckymgr_stage_failed">Impossibile preparare PluginLoader</string> + <string name="deckymgr_loader_preserve_failed">Impossibile conservare il PluginLoader esistente</string> + <string name="deckymgr_loader_activate_failed">Impossibile attivare il nuovo PluginLoader</string> + <string name="deckymgr_stop_session_plugin">Interrompi la sessione attiva prima di installare un plugin</string> + <string name="deckymgr_loader_required">Installa Decky Loader prima di importare un plugin</string> + <string name="deckymgr_zip_unreadable">Impossibile leggere il file ZIP selezionato</string> + <string name="deckymgr_plugins_dir_failed">Impossibile creare la cartella dei plugin Decky</string> + <string name="deckymgr_extracting_zip">Estrazione dello ZIP del plugin</string> + <string name="deckymgr_zip_one_folder">Lo ZIP del plugin deve contenere una sola cartella di plugin al primo livello</string> + <string name="deckymgr_zip_missing_files">Nello ZIP del plugin mancano plugin.json, package.json o dist/index.js</string> + <string name="deckymgr_plugin_name_missing">Nome del plugin mancante</string> + <string name="deckymgr_session_started_plugin">È stata avviata una sessione durante l\'installazione; interrompila prima di installare un plugin</string> + <string name="deckymgr_plugin_installed">Plugin installato · riavvia Steam per caricarlo</string> + <string name="deckymgr_plugin_install_failed">Installazione dello ZIP del plugin non riuscita</string> + <string name="deckymgr_zip_too_many">Lo ZIP del plugin contiene troppi elementi</string> + <string name="deckymgr_zip_invalid_path">Lo ZIP del plugin contiene un percorso non valido</string> + <string name="deckymgr_zip_duplicate_paths">Lo ZIP del plugin contiene percorsi duplicati</string> + <string name="deckymgr_zip_unsafe_path">Lo ZIP del plugin contiene un percorso non sicuro</string> + <string name="deckymgr_folder_failed">Impossibile creare la cartella del plugin</string> + <string name="deckymgr_zip_too_large">Una volta estratto, lo ZIP del plugin supera il limite di 1 GiB</string> + <string name="deckymgr_bundled_not_executable">Impossibile rendere eseguibile il binario incluso nel plugin</string> + <string name="deckymgr_remote_entry_invalid">Voce remote_binary non valida</string> + <string name="deckymgr_remote_name_invalid">Nome del binario remoto non valido</string> + <string name="deckymgr_remote_url_https">L\'URL del binario remoto deve usare HTTPS</string> + <string name="deckymgr_remote_hash_invalid">Il binario remoto %1$s ha un hash SHA-256 non valido</string> + <string name="deckymgr_bin_dir_failed">Impossibile creare la cartella bin del plugin</string> + <string name="deckymgr_downloading_binary">Download del binario del plugin · %1$s</string> + <string name="deckymgr_binary_download_failed">Impossibile scaricare il binario del plugin %1$s</string> + <string name="deckymgr_binary_checksum_mismatch">Checksum del binario del plugin non corrispondente · %1$s</string> + <string name="deckymgr_binary_not_executable">Impossibile rendere eseguibile il binario del plugin · %1$s</string> + <string name="deckymgr_binary_replace_failed">Impossibile sostituire il binario del plugin · %1$s</string> + <string name="deckymgr_binary_install_failed">Impossibile installare il binario del plugin · %1$s</string> + <string name="deckymgr_folder_taken">La cartella del plugin appartiene a un altro plugin</string> + <string name="deckymgr_preserve_failed">Impossibile conservare il plugin esistente</string> + <string name="deckymgr_activate_failed">Impossibile attivare il plugin importato</string> + <string name="deckymgr_settings_failed">Impossibile aggiornare le impostazioni dei plugin Decky</string> + <string name="deckymgr_restore_failed">Impossibile ripristinare il plugin esistente</string> + + <string name="adbfix_save_failed">Impossibile salvare l\'identità del debug wireless</string> + <string name="adbfix_command_failed">Comando ADB non riuscito (%1$d)</string> + <string name="adbfix_not_confirmed">Android non ha confermato la modifica del limite dei processi secondari: %1$s</string> + <string name="adbfix_no_pairing">Nessun accoppiamento del debug wireless salvato</string> + <string name="adbfix_enter_address">Inserisci l\'indirizzo IP e la porta attuali del debug wireless</string> + + <string name="adbpair_timed_out">Tempo scaduto in attesa del popup di accoppiamento. Riprova da DroidDeck.</string> + <string name="adbpair_discovery_failed_code">Impossibile cercare il popup di accoppiamento (%1$d). Controlla che il Wi-Fi sia attivo.</string> + <string name="adbpair_discovery_failed">Impossibile cercare il popup di accoppiamento: %1$s</string> + <string name="adbpair_six_digits">Inserisci tutte e 6 le cifre del codice</string> + <string name="adbpair_pairing">Accoppiamento…</string> + <string name="adbpair_code_rejected">Il codice non ha funzionato. Riapri il popup di accoppiamento e inserisci il nuovo codice.</string> + <string name="adbpair_connecting">Accoppiato. Connessione…</string> + <string name="adbpair_port_not_found">Accoppiato, ma la porta del debug wireless non è stata trovata. Lascia attivo il debug wireless e riprova.</string> + <string name="adbpair_applying">Applicazione dell\'impostazione…</string> + <string name="adbpair_command_failed">Comando di debug wireless non riuscito</string> + <string name="adbpair_channel_name">Configurazione del debug wireless</string> + <string name="adbpair_channel_description">Chiede il codice di accoppiamento del debug wireless mentre Impostazioni è aperto</string> + <string name="adbpair_title_pair">Accoppia il debug wireless</string> + <string name="adbpair_waiting_text">Attiva Debug wireless, poi tocca “Accoppia dispositivo con codice di accoppiamento”.</string> + <string name="adbpair_waiting_long">Attiva Debug wireless, poi tocca “Accoppia dispositivo con codice di accoppiamento”. Resta in Impostazioni: il codice va inserito in questa notifica.</string> + <string name="adbpair_code_label">Codice di accoppiamento a 6 cifre</string> + <string name="adbpair_code_hint">Tocca Inserisci codice e digita le 6 cifre mostrate in Impostazioni.</string> + <string name="adbpair_title_code">Inserisci il codice di accoppiamento Wi-Fi</string> + <string name="adbpair_title_retry">Riprova con il codice</string> + <string name="adbpair_enter_code">Inserisci codice</string> + <string name="adbpair_title_working">Configurazione di Steam</string> + <string name="adbpair_title_done">Steam è pronto</string> + <string name="adbpair_done_text">Tocca per tornare a DroidDeck. Ora puoi disattivare il debug wireless.</string> + <string name="adbpair_title_failed">Configurazione del debug wireless interrotta</string> + + <string name="selfinst_http">Il download ha risposto HTTP %1$d</string> + <string name="selfinst_size_mismatch">La dimensione del download non corrispondeva all\'APK pubblicato ed è stato scartato - riprova</string> + <string name="selfinst_checksum_mismatch">Il download non corrispondeva al suo checksum ed è stato scartato - riprova</string> + <string name="selfinst_wrong_package">L\'aggiornamento è per il pacchetto Android %1$s, non %2$s</string> + <string name="selfinst_metadata_signer">I metadati dell\'aggiornamento non indicano la chiave di firma delle release di DroidDeck</string> + <string name="selfinst_unreadable">Android non è riuscito a leggere l\'APK scaricato</string> + <string name="selfinst_apk_wrong_package">L\'APK scaricato è per %1$s, non %2$s</string> + <string name="selfinst_version_mismatch">L\'APK scaricato indica versionCode %1$d, non il %2$d pubblicato</string> + <string name="selfinst_downgrade">Android non installa il versionCode %1$d sopra il %2$d installato</string> + <string name="selfinst_no_signing">L\'APK scaricato non ha informazioni di firma</string> + <string name="selfinst_wrong_signer">L\'APK scaricato non è firmato con la chiave di release di DroidDeck</string> + <string name="appupd_http">Il catalogo degli aggiornamenti ha risposto HTTP %1$d</string> + + <string name="loadstate_starting">Avvio della sessione…</string> + <string name="loadstate_elapsed">%1$d:%2$02d trascorsi · ancora in corso</string> + <string name="loadstate_steam_update">Download dell\'aggiornamento del client Steam · %1$d%%</string> + <string name="loadstate_steam_client">Download del client Steam · %1$s</string> + + <string name="pextra_busy">È già in corso l\'installazione di uno strumento di compatibilità</string> + <string name="pextra_stop_session">Interrompi la sessione attiva prima di installare strumenti di compatibilità</string> + <string name="pextra_no_release">Impossibile trovare una release ARM64 per %1$s</string> + <string name="pextra_no_space">Per installare %1$s servono almeno 4 GB liberi più la dimensione del download</string> + <string name="pextra_downloading">Download di %1$s %2$s</string> + <string name="pextra_retrying">Nuovo tentativo di download · tentativo %1$d di %2$d</string> + <string name="pextra_interrupted">Download interrotto</string> + <string name="pextra_download_failed">Download non riuscito; il file parziale viene conservato per riprendere al prossimo tentativo</string> + <string name="pextra_too_big">Il download ha superato la dimensione della release ed è stato eliminato</string> + <string name="pextra_incomplete">Download incompleto; il file parziale viene conservato per riprendere al prossimo tentativo</string> + <string name="pextra_verifying">Verifica del download</string> + <string name="pextra_checksum_unreadable">Impossibile leggere il checksum della release; riprova</string> + <string name="pextra_no_checksum">%1$s %2$s non pubblica alcun checksum; non è stato installato nulla</string> + <string name="pextra_checksum_mismatch">Checksum non corrispondente; il download è stato eliminato</string> + <string name="pextra_session_started">È stata avviata una sessione durante il download; interrompila prima di installare strumenti di compatibilità</string> + <string name="pextra_installing">Installazione di %1$s</string> + <string name="pextra_install_failed">Installazione di %1$s non riuscita (uscita %2$d)</string> + <string name="pextra_could_not_install">Impossibile installare %1$s</string> + <string name="esync_fetching">Recupero del pacchetto droiddeck-esync</string> + + <string name="cmgr_fex_linux_helpers">con helper Linux</string> + <string name="cmgr_fex_dlls_only">Solo DLL</string> + <string name="cmgr_original_proton_updated">Originale (Proton aggiornato dopo l\'ultima sostituzione)</string> + <string name="cmgr_changed_outside">Modificato fuori da Componenti</string> + <string name="cmgr_not_present">non presente</string> + <string name="cmgr_original_version">Originale %1$s</string> + <string name="cmgr_original_from">Originale da %1$s</string> + <string name="cmgr_swap_queued">%1$s %2$s entrerà in %3$s quando si chiuderà il gioco che lo sta usando.</string> + <string name="cmgr_swap_done">%1$s %2$s ora è in %3$s. Si applica al prossimo avvio di un gioco.</string> + <string name="cmgr_original_missing">Quel pacchetto originale non è salvato</string> + <string name="cmgr_restore_queued">%1$s di %2$s tornerà all\'originale quando si chiuderà il gioco che lo sta usando.</string> + <string name="cmgr_restore_done">%1$s di %2$s ripristinato dall\'originale di %3$s.</string> + <string name="cmgr_package_in_use">%1$s è in uso in %2$s. Prima passa quel Proton a un\'altra versione.</string> + <string name="cmgr_package_not_stored">%1$s non è salvato</string> + <string name="cmgr_deleted">%1$s eliminato.</string> + <string name="cmgr_original_kept">L\'originale della build installata viene conservato: serve per tornare indietro.</string> + <string name="cmgr_not_stored">Non salvato</string> + <string name="cmgr_deleted_original">Originale %1$s di %2$s eliminato.</string> + <string name="cmgr_nightlies_only">I download provengono solo dalle release Nightly</string> + <string name="cmgr_list_predates_checksums">Questo elenco di pacchetti è anteriore ai checksum - aggiornalo e riprova</string> + <string name="cmgr_checksum_mismatch">Checksum non corrispondente</string> + + <string name="sprefs_upscaler_linear">Lineare</string> + <string name="sprefs_upscaler_nearest">Più vicino</string> + <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (qualità)</string> + <string name="sprefs_upscaler_sharpen">Solo nitidezza</string> + <string name="sprefs_lod_bias_auto">Auto (segue il ridimensionamento)</string> + + <string name="look_off_desc">Nessun effetto applicato.</string> + <string name="look_game_clarity">Chiarezza di gioco</string> + <string name="look_game_clarity_desc">Nitidezza con un tocco di contrasto - quello per tutti i giorni.</string> + <string name="look_vivid">Vivace</string> + <string name="look_vivid_desc">Colori molto accentuati. Deband attivo, così i cieli non fanno strisce.</string> + <string name="look_cinematic">Cinematografico</string> + <string name="look_cinematic_desc">Contrasto più profondo, colori più freddi, bordi smussati.</string> + <string name="look_competitive">Competitivo</string> + <string name="look_competitive_desc">Ombre schiarite e nitidezza elevata - avvista gli avversari per primo.</string> + <string name="look_adaptive_sharpen">Nitidezza adattiva</string> + <string name="look_adaptive_sharpen_desc">Solo nitidezza, colori invariati.</string> + <string name="look_filmic">Filmico</string> + <string name="look_filmic_desc">Curva da pellicola, leggermente attenuata.</string> + <string name="look_arcade">Arcade</string> + <string name="look_arcade_desc">Forte e incisivo.</string> + <string name="look_retro_crt">CRT rétro</string> + <string name="look_retro_crt_desc">I passaggi tubo CRT e segnale analogico insieme.</string> + <string name="look_upscale_sharp">Upscaling nitido</string> + <string name="look_upscale_sharp_desc">Per giocare sotto la risoluzione del pannello.</string> + <string name="look_pixel_clean">Pixel puliti</string> + <string name="look_pixel_clean_desc">Pixel nitidi per il 2D e i titoli più vecchi.</string> + <string name="look_anime_edge">Contorni anime</string> + <string name="look_anime_edge_desc">Contorni da cartone animato più nitidezza.</string> + + <string name="gpuinfo_not_adreno">Non è una GPU Adreno</string> + <string name="gpuinfo_supported">Supportata</string> + <string name="gpuinfo_experimental">Sperimentale: i suoi driver sono build di test</string> + <string name="gpuinfo_below_tested">Inferiore all\'hardware testato (Adreno 725 e successive): potrebbe non funzionare</string> + <string name="gpuinfo_unsupported">Non supportata: DroidDeck richiede una GPU Adreno (Snapdragon)</string> + + <string name="turnip_rate_limited">Raggiunto il limite di richieste di GitHub - riprova più tardi</string> + <string name="turnip_github_http">GitHub ha risposto HTTP %1$d</string> + <string name="turnip_download_http">il download ha risposto HTTP %1$d</string> + <string name="turnip_list_predates_checksums">Questo elenco di driver è anteriore ai checksum - aggiornalo e riprova</string> + <string name="turnip_checksum_mismatch">Checksum non corrispondente - il download è stato scartato</string> + <string name="turnip_not_adrenotools">Non è un driver AdrenoTools: %1$s</string> + <string name="turnip_reject_no_meta">manca meta.json</string> + <string name="turnip_reject_meta_unreadable">meta.json non è leggibile (%1$s)</string> + <string name="turnip_reject_linux_driver">questo è un driver per il runtime Linux (importalo in “Driver del runtime Linux”)</string> + <string name="turnip_reject_wayland">questo è un driver per giochi Wayland, che a questa app non serve</string> + <string name="turnip_reject_no_library_name">meta.json non indica alcun libraryName, quindi non c\'era nulla da passare ad AdrenoTools</string> + <string name="turnip_reject_library_missing">meta.json indica %1$s, che non è nello zip</string> + <string name="turnip_reject_not_aarch64">%1$s non è una libreria condivisa ELF AArch64 a 64 bit</string> + <string name="turnip_reject_glibc">%1$s è collegato a glibc: è un Turnip “-Linux”, che va in “Driver del runtime Linux”</string> + <string name="lvd_no_library">Nessun libvulkan_freedreno*.so in questo zip. Uno zip Turnip Android (AdrenoTools) o -Wayland non è un driver per il runtime Linux.</string> + <string name="lvd_not_aarch64">%1$s non è una libreria condivisa ELF AArch64 a 64 bit.</string> + <string name="lvd_not_glibc">%1$s non è un driver glibc: è collegato alla libc di Android. Il runtime Linux richiede uno zip “-Linux”; un Turnip normale o “-Wayland” non può essere caricato in alcun modo dal client Steam.</string> + <string name="bundle_no_manifest">Non è un pacchetto di driver: manca un manifest.json che indichi un driver Android e uno Linux</string> + <string name="bundle_entry_outside">Non è un pacchetto di driver: %1$s punta al di fuori di esso</string> + <string name="bundle_too_large">Non è un pacchetto di driver: una volta estratto supera %1$d MB</string> + + <string name="gstore_sd_card">Scheda SD</string> + <string name="gstore_folder">Cartella</string> + + <string name="logshare_chooser">Condividi log di sessione</string> + + <string name="hdr_needs_android_10">richiede Android 10 o successivo (livelli di visualizzazione con spazio colore)</string> + <string name="hdr_none">questo schermo non segnala supporto HDR</string> + <string name="hdr_not_hdr10">questo schermo supporta %1$s, non HDR10</string> + + <string name="gpu_this_gpu">questa GPU</string> + <string name="phantom_title_on">Limite dei processi secondari attivo</string> + <string name="phantom_title_unset">Limite dei processi secondari non impostato</string> + <string name="phantom_title_unreadable">Impossibile verificare il limite dei processi secondari</string> + <string name="phantom_title_off">Limite dei processi secondari disattivato</string> + <string name="phantom_title_not_required">Limite dei processi secondari non necessario</string> + <string name="phantom_instructions_fix">%1$s Se non è possibile, collega questo dispositivo a un computer con ADB ed esegui il comando qui sotto. Poi torna qui e ricontrolla.</string> + <string name="phantom_instructions_off">Le sessioni Steam possono avviarsi.</string> + <string name="phantom_instructions_not_applicable">Questa versione di Android non usa questo limite.</string> + <string name="phantom_fix_toggle">Attiva “Disattiva limitazioni processi secondari” in Opzioni sviluppatore e poi lascia attive le Opzioni sviluppatore.</string> + <string name="phantom_fix_no_toggle">Questa versione di Android non ha un interruttore per farlo. Usa il debug wireless per cambiarlo da questo dispositivo.</string> + <string name="upd_block_no_apk">Questa build non ha un download per questa copia di DroidDeck.</string> + <string name="upd_block_version_code">Android non la installerà sopra questa build perché il suo versionCode %1$d è inferiore al %2$d installato.</string> + <string name="upd_fail_downgrade">Android non installa una versione precedente sopra una più recente. Resta su questa build finché il canale non si aggiorna.</string> + <string name="upd_fail_signature">Questa copia di DroidDeck ha una firma diversa (una build locale?), quindi Android non può aggiornarla direttamente.</string> + <string name="upd_fail_storage">Non c\'è abbastanza spazio libero per l\'aggiornamento.</string> + <string name="upd_fail_aborted">L\'aggiornamento è stato annullato.</string> + <string name="upd_fail_with_message">L\'aggiornamento non è stato installato: %1$s</string> + <string name="upd_fail_generic">L\'aggiornamento non è stato installato.</string> + <string name="saves_layout_zip">zip %1$s</string> + <string name="saves_zip">zip</string> + <string name="saves_not_launched">%1$s non è ancora stato avviato</string> + <string name="saves_none_found">Nessun file di salvataggio trovato per %1$s</string> + <string name="saves_not_save_zip">%1$s non ha una cartella drive_c, quindi non è uno zip di salvataggi GameHub o Winlator</string> + <string name="gstore_cannot_create">impossibile creare cartelle in %1$s</string> + <string name="gstore_cannot_write">impossibile scrivere in %1$s</string> + <string name="gstore_cannot_write_detail">impossibile scrivere in %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">Cartella non disponibile</string> + <string name="gfiles_name_taken">Un altro file usa già %1$s</string> + <string name="gfiles_art_unavailable">Cartella delle illustrazioni non disponibile</string> + <string name="gfiles_unrelated_inventory">La cartella ha un inventario di sincronizzazione non correlato</string> + <string name="gfiles_remove_failed">Impossibile rimuovere %1$s</string> + <string name="gfiles_outside_folder">Il file esce dalla cartella di esportazione</string> + <string name="cmgr_fex_unnamed">FEX (build senza nome di versione)</string> + <string name="cmgr_no_profile">nessun profile.json in %1$s</string> + <string name="cmgr_not_component_package">%1$s non è un pacchetto di componenti Linux (tipo %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s liberi</string> + <string name="mode_storage_auto_short">Automatico</string> + <string name="appupd_catalog_rejected">Il catalogo degli aggiornamenti non ha superato i controlli (%1$s)</string> + <string name="main_adb_clip_label">Impostazione processi secondari di DroidDeck</string> + <string name="setup_signed_in_offline_unnamed">Accesso eseguito · modalità offline</string> + <string name="setup_signed_in_unnamed">Accesso eseguito</string> + <string name="driver_tag_bundled">INCLUSO</string> + <string name="driver_tag_downloaded">SCARICATO</string> + <string name="driver_tag_imported">IMPORTATO</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Schermo della sessione</string> + <string name="display_resolution">Risoluzione</string> + <string name="display_game_fit">Adattamento giochi 16:9</string> + <string name="display_game_fit_hint">Come i giochi 16:9 si adattano a questo schermo. Dalla prossima sessione.</string> + <string name="display_game_fit_unavailable">Scegli Adatta allo schermo o una dimensione personalizzata più stretta di 16:9.</string> + <string name="display_preserve">Mantieni proporzioni</string> + <string name="display_stretch">Estendi allo schermo</string> + <string name="display_image_scaling">Ridimensionamento immagine</string> + <string name="display_filter">Filtro di ridimensionamento</string> + <string name="display_filter_hint">Condiviso da Steam e Desktop.</string> + <string name="display_filter_live">Si applica subito. Condiviso da Steam e Desktop.</string> + <string name="display_filter_note">FSR, GSR e NIS si applicano quando l\'immagine della sessione viene ingrandita. Solo nitidezza funziona anche senza ingrandimento.</string> + <string name="display_sharpness">Nitidezza del filtro</string> + <string name="display_frame_rate">Frequenza fotogrammi</string> + <string name="display_look_filter">%1$s Imposta il ridimensionamento su %2$s.</string> + <string name="display_match_screen">Adatta allo schermo (%1$d×%2$d)</string> + <string name="display_follow_screen">Segui lo schermo (si ridimensiona quando lo pieghi o lo apri)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Mostra dettagli dispositivo</string> + <string name="device_info_hide">Nascondi dettagli dispositivo</string> + <string name="device_info_loading">Lettura del dispositivo…</string> + <string name="device_info_copy">Copia</string> + <string name="device_info_copy_hint">Copia questi dettagli in una segnalazione di bug.</string> + <string name="device_info_copied">Copiato.</string> + <string name="force_ssbs_title">Forza SSBS</string> + <string name="force_ssbs_hint">Mantiene attiva un\'impostazione di velocità della CPU che Wine continua a disattivare. Può rendere più veloci i giochi Windows su Snapdragon 8 Elite; poche differenze sui chip più vecchi. Si applica al prossimo avvio del gioco.</string> + + <string name="rail_stores">Negozi</string> + <string name="source_steam">Steam</string> + <string name="source_added">Personalizzati</string> + <string name="setup_stores">Negozi</string> + <string name="setup_stores_open_on">Apri un negozio su</string> + <string name="setup_stores_speed">Livello di velocità di download</string> + <string name="setup_stores_engine">Motore dei negozi</string> + <string name="setup_stores_engine_chip_ready">● Motore pronto</string> + <string name="setup_stores_engine_chip_missing">Non compilato</string> + <string name="stores_tier_balanced">Bilanciato</string> + <string name="stores_tier_fast">Veloce</string> + <string name="stores_tier_max">Massimo</string> + <string name="stores_title">Negozi</string> + <string name="stores_settings">Impostazioni negozi</string> + <string name="stores_settings_eyebrow">Impostazioni</string> + <string name="stores_chip_signed_out">(accesso non eseguito)</string> + <string name="stores_tab_store">Negozio</string> + <string name="stores_tab_installed">Installati</string> + <string name="stores_tab_library">Libreria</string> + <string name="stores_tab_all">Tutti</string> + <string name="stores_tab_downloads">Download</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Accedi</string> + <string name="stores_signout_named">%1$s · esci</string> + <string name="stores_signout_title">Uscire da %1$s?</string> + <string name="stores_signout_note">I giochi installati restano. La libreria verrà recuperata di nuovo quando accederai.</string> + <string name="stores_signout_stay">Resta connesso</string> + <string name="stores_signout_go">Esci</string> + <string name="stores_search_catalog">Cerca nel catalogo di %1$s…</string> + <string name="stores_search_installed">Cerca tra i giochi installati…</string> + <string name="stores_search_library">Cerca nella tua libreria…</string> + <string name="stores_search_open">Cerca</string> + <string name="stores_installed_empty">Niente di installato</string> + <string name="stores_library_loading">Caricamento</string> + <string name="stores_nothing_matches">Nessun gioco</string> + <string name="stores_shelves_loading">Caricamento del negozio…</string> + <string name="stores_amazon_no_catalog">Nessun catalogo</string> + <string name="stores_shelf_new">Novità</string> + <string name="stores_shelf_deals">Offerte</string> + <string name="stores_shelf_free">Giochi gratuiti</string> + <string name="stores_shelf_trending">Di tendenza</string> + <string name="stores_shelf_library">La tua libreria</string> + <string name="stores_play">Gioca</string> + <string name="stores_install">Installa</string> + <string name="stores_resume_install">Riprendi installazione</string> + <string name="stores_dl_interrupted">Interrotto</string> + <string name="stores_install_size">Installa · %1$s</string> + <string name="stores_get_free">Ottieni gratis</string> + <string name="stores_view_on">Visualizza su %1$s</string> + <string name="stores_buy_price">Acquista · %1$s</string> + <string name="stores_installed_chip">Installato</string> + <string name="stores_in_library">In libreria</string> + <string name="stores_free">Gratis</string> + <string name="stores_resume">Riprendi</string> + <string name="stores_refresh">Aggiorna</string> + <string name="stores_show_mature">Mostra contenuti per adulti</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Offline</string> + <string name="epic_card_none">Disattivato</string> + <string name="epic_eos">Accesso Epic (EOS)</string> + <string name="epic_offline">Avvia offline</string> + <string name="epic_resolve">Risolvi l\'accesso Epic</string> + <string name="store_cloud_saves">Salvataggi nel cloud</string> + <string name="cloud_upload">Carica nel cloud</string> + <string name="cloud_download">Scarica dal cloud</string> + <string name="cloud_none">Nessun salvataggio nel cloud</string> + <string name="cloud_conflict">Conflitto cloud</string> + <string name="cloud_conflict_files">Conflitto cloud · %1$d file</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Mantieni cloud</string> + <string name="cloud_keep_local">Mantieni locale</string> + <string name="cloud_checking">Controllo del cloud…</string> + <string name="cloud_last_sync">Ultima sincronizzazione %1$s</string> + <string name="cloud_never">Non ancora sincronizzato</string> + <string name="cloud_done">%1$d file</string> + <string name="stores_eyebrow_installed">installato</string> + <string name="stores_eyebrow_library">nella tua libreria</string> + <string name="stores_eyebrow_store">negozio</string> + <string name="achievements_title">Obiettivi</string> + <string name="achievements_count">%1$d di %2$d sbloccati</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Sbloccati · %1$d</string> + <string name="achievements_group_locked">Bloccati · %1$d</string> + <string name="achievements_group_hidden">Nascosti · %1$d</string> + <string name="achievements_hidden">Obiettivo nascosto</string> + <string name="achievements_hidden_detail">I dettagli vengono mostrati una volta sbloccato.</string> + <string name="achievements_all">Tutti gli obiettivi sbloccati</string> + <string name="achievements_reading">Caricamento…</string> + <string name="achievements_signed_out">Accedi a %1$s per vedere i tuoi obiettivi.</string> + <string name="achievements_unreachable">Impossibile raggiungere %1$s. Riprova quando sei online.</string> + <string name="achievements_none">%1$s non elenca obiettivi per questo gioco.</string> + <string name="achievements_show_all">Mostra tutti i %1$d</string> + <string name="achievements_dialog_title">Obiettivi · %1$s</string> + <string name="stores_in_steam">✓ In Steam</string> + <string name="stores_uninstall">Disinstalla</string> + <string name="stores_uninstalling">Disinstallazione…</string> + <string name="stores_uninstall_title">Disinstallare %1$s?</string> + <string name="stores_uninstall_body_size">Libera %1$s e lo rimuove da Steam.</string> + <string name="stores_uninstall_body">Rimuove i suoi file e il suo collegamento in Steam.</string> + <string name="stores_launch_not_registered">Steam rileverà il gioco al prossimo avvio; avvialo da lì, oppure avvia Steam ora.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Scelta di Steam per ogni gioco</string> + <string name="stores_card_audio">Audio</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Componenti</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Motore</string> + <string name="stores_card_engine_value">%1$s · nativo</string> + <string name="stores_card_engine_java">%1$s · integrato</string> + <string name="stores_how_it_runs">Come funziona</string> + <string name="stores_kv_fetched">Scaricato da</string> + <string name="stores_kv_fetched_value">%1$s, con il tuo account</string> + <string name="stores_kv_installed_to">Installato in</string> + <string name="stores_kv_installed_to_value">Archivio dei giochi (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Avviato da</string> + <string name="stores_kv_launched_by_value">Steam e Proton, come i giochi Steam</string> + <string name="stores_kv_launched_by_epic">Steam e Proton · accesso Epic a ogni avvio, overlay EOS disattivato</string> + <string name="stores_kv_launched_by_amazon">Steam e Proton · variabili di Amazon Games Services impostate</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">aggiunto a Steam all\'installazione (interfaccia Deck + Steam Input)</string> + <string name="stores_downloads_empty">Nessun download</string> + <string name="stores_dl_paused">In pausa</string> + <string name="stores_dl_queued">In coda</string> + <string name="stores_dl_queued_at">In coda · #%1$d</string> + <string name="stores_dl_cancelled">Annullato</string> + <string name="stores_dl_failed">Non riuscito</string> + <string name="stores_dl_failed_reason">Non riuscito · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Pausa</string> + <string name="stores_dl_resume">Riprendi</string> + <string name="stores_dl_cancel">Annulla</string> + <string name="stores_dl_retry">Riprova</string> + <string name="stores_dl_clear">Cancella</string> + <string name="stores_dl_clear_all">Cancella completati</string> + <string name="stores_stage_manifest">Manifest</string> + <string name="stores_stage_download">Download</string> + <string name="stores_stage_verify">Verifica</string> + <string name="stores_stage_install">Installazione</string> + <string name="stores_stage_done">Completato</string> + <string name="stores_stage_checking">Controllo</string> + <string name="stores_stage_downloading">Download</string> + <string name="stores_dl_cancel_confirm">Eliminare il download?</string> + <string name="stores_dl_failed_kept">Non riuscito · %1$s conservati</string> + <string name="stores_stage_verifying">Verifica</string> + <string name="stores_stage_installing">Installazione</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d file</string> + <string name="stores_download_manager">Gestione download</string> + <string name="stores_parallel">Download simultanei</string> + <string name="stores_install_where">Installa in</string> + <string name="stores_install_title">Installa %1$s:</string> + <string name="stores_target_internal">Interna</string> + <string name="stores_target_free">%1$s liberi</string> + <string name="stores_notification_channel">Download dei negozi</string> + <string name="stores_notification_channel_hint">Mostra l\'avanzamento dei download dei giochi e li mantiene attivi in background</string> + <string name="stores_notification_one">Download in corso</string> + <string name="stores_notification_many">%1$d download</string> + <string name="stores_notification_waiting">Preparazione…</string> + <string name="stores_notification_done_channel">Download dei negozi completati</string> + <string name="stores_notification_installed">%1$s installato</string> + <string name="stores_notification_failed">Download di %1$s non riuscito</string> + <string name="stores_notification_delete">Elimina</string> + <string name="stores_notification_keep">Mantieni</string> + <string name="stores_login_title">Accedi a %1$s</string> + <string name="stores_login_provider_title">Accedi con il tuo provider</string> + <string name="stores_login_reload">Ricarica</string> + <string name="stores_login_close">Chiudi</string> + <string name="stores_login_retry">Riprova</string> + <string name="stores_login_finishing">Completamento dell\'accesso…</string> + <string name="stores_login_error_network">Impossibile caricare la pagina di accesso: %1$s</string> + <string name="stores_login_error_idp_blocked">Questo provider non consente l\'accesso da una pagina incorporata. Accedi invece con la password del negozio.</string> + <string name="stores_login_error_http">La pagina di accesso ha risposto con un errore (HTTP %1$d).</string> + <string name="stores_login_error_ssl">La connessione alla pagina di accesso non è sicura.</string> + <string name="stores_login_error_verification">Impossibile verificare l\'accesso. Riprova.</string> + <string name="stores_login_error_generic">Accesso non riuscito. Riprova.</string> + <string name="common_close">Chiudi</string> + <string name="common_back">Indietro</string> + <string name="common_edit">Modifica</string> + <string name="games_add_all">Aggiungi tutti i giochi in questa cartella</string> + <string name="games_added_none">Niente di nuovo in questa cartella</string> + <string name="edit_name">Nome</string> + <string name="edit_target">Destinazione</string> + <string name="edit_start_in">Avvia in</string> + <string name="edit_launch_options">Opzioni di avvio</string> + <string name="edit_artwork">Illustrazioni</string> + <string name="edit_other_exe">Altro exe…</string> + <string name="edit_remove">Rimuovi</string> + <string name="art_cover">Copertina</string> + <string name="art_background">Sfondo</string> + <string name="art_logo">Logo</string> + <string name="art_icon">Icona</string> + <string name="art_src_folder">Cartella del gioco</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">Il tuo file</string> + <string name="art_src_exe">Dall\'exe</string> + <string name="art_src_auto">Automatico</string> + <string name="art_pick_image">Scegli immagine…</string> + <string name="art_pick_title">Scegli un\'immagine: %1$s</string> + <string name="art_reset">Ripristina automatico</string> + <plurals name="games_added_n"> + <item quantity="one">%1$d gioco aggiunto</item> + <item quantity="many">%1$d giochi aggiunti</item> + <item quantity="other">%1$d giochi aggiunti</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="one">%1$d già in Giochi</item> + <item quantity="many">%1$d già in Giochi</item> + <item quantity="other">%1$d già in Giochi</item> + </plurals> + <string name="hop_back">B · Torna a %1$s</string> + <string name="hop_from_games">il gioco</string> + <string name="hop_from_play">Impostazioni di gioco</string> + <string name="sheet_graphics_drivers">Driver grafici</string> + <string name="comp_this_proton">Questo Proton</string> + <string name="comp_every_proton">Ogni Proton</string> + <string name="comp_for_every_proton">Per ogni Proton</string> + <string name="comp_available_count">Disponibili · %1$d</string> + <string name="comp_checked">Controllato %1$s</string> + <string name="comp_check_all">Cerca nuovi componenti e driver</string> + <string name="comp_get">Ottieni</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton e FEX qui sotto sono di questo Proton</string> + <string name="comp_device_tested">%1$s · testato</string> + <string name="comp_device_untested">%1$s · non testato</string> + <string name="comp_device_unsupported">%1$s · non supportato</string> + <string name="gpu_auto_keeps">Mantiene aggiornata la coppia consigliata</string> + <string name="gpu_bar_auto">Auto · %1$s</string> + <string name="gpu_separately">Driver del runtime e video separati</string> + <string name="gpu_use_title">Usare %1$s?</string> + <string name="proton_remove_title">Rimuovere %1$s?</string> + <string name="setup_tab_diagnostics">Diagnostica</string> + <string name="setup_ready">Pronto per giocare</string> + <string name="setup_not_ready">Non ancora pronto per giocare</string> + <string name="setup_ready_runtime">runtime %1$s</string> + <string name="setup_ready_limit_off">limite processi disattivato</string> + <string name="setup_ready_signed_in">accesso come %1$s</string> + <string name="setup_needs_you">Quando serve il tuo intervento</string> + <string name="setup_group_look">Aspetto</string> + <string name="setup_group_device">Questo dispositivo</string> + <string name="setup_group_art">Illustrazioni della libreria</string> + <string name="flathub_eyebrow">APP LINUX</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">App per il desktop Linux, da aprire in Desktop.</string> + <string name="flathub_setup">Configura Flathub</string> + <string name="flathub_remove_title">Rimuovere %1$s?</string> + <string name="flathub_remove_note">Le sue impostazioni e i suoi file vengono conservati.</string> + <string name="flathub_keep">Mantieni</string> + <string name="desktop_size">Spazio su disco</string> + <string name="desktop_uninstall_title">Disinstallare il desktop?</string> + <string name="desktop_all_systems">Tutti</string> + <string name="gog_achievements">Obiettivi GOG</string> + <string name="gog_achievements_hint">Sincronizza gli obiettivi per i giochi GOG supportati. Si applica al prossimo avvio.</string> + <string name="gog_achievements_off">Obiettivi disattivati</string> + <string name="gog_achievements_on">Obiettivi attivati</string> + <string name="gog_achievements_signin">Accedi di nuovo a GOG</string> + <string name="gog_achievements_uploaded">Obiettivi caricati</string> + <string name="gog_achievements_pending">Caricamento obiettivo in sospeso</string> + <string name="gog_achievements_enable_store">Attiva gli obiettivi GOG nelle impostazioni Store</string> + <string name="gog_achievements_unavailable">Obiettivi non disponibili; controlla i log della sessione</string> + <string name="achievement_unlocked">Obiettivo sbloccato</string> + <string name="touch_paste">Incolla</string> + <string name="touch_dpad">Croce direzionale</string> + <string name="touch_left_stick">Stick sinistro</string> + <string name="touch_right_stick">Stick destro</string> + <string name="touch_menu">Menu</string> + <string name="touch_keyboard">Tastiera</string> + <string name="touch_dpad_up">Croce direzionale su</string> + <string name="touch_dpad_down">Croce direzionale giù</string> + <string name="touch_dpad_left">Croce direzionale sinistra</string> + <string name="touch_dpad_right">Croce direzionale destra</string> + <string name="touch_title">Controlli touch</string> + <string name="touch_controller">Controller touch</string> + <string name="touch_mouse">Mouse</string> + <string name="touch_on">Attivo</string> + <string name="touch_off">Disattivo</string> + <string name="touch_speed_slow">Lenta</string> + <string name="touch_speed_normal">Normale</string> + <string name="touch_speed_fast">Rapida</string> + <string name="touch_edit_layout">Modifica layout</string> + <string name="touch_droiddeck_menu">Menu di DroidDeck</string> + <string name="touch_drag_hint">Trascina i controlli sullo schermo</string> + <string name="touch_arrange">Disponi</string> + <string name="touch_discard">Annulla</string> + <string name="touch_saving">Salvataggio…</string> + <string name="touch_icon">Icona</string> + <string name="touch_narrower">L−</string> + <string name="touch_wider">L+</string> + <string name="touch_shorter">A−</string> + <string name="touch_taller">A+</string> + <string name="touch_hide">Nascondi</string> + <string name="touch_color_opacity">Colore e opacità…</string> + <string name="touch_copy_layout">Copia questo layout</string> + <string name="touch_layout_copied">Layout copiato; incollalo nel layout touch di qualsiasi gioco</string> + <string name="touch_paste_layout">Incolla layout copiato</string> + <string name="touch_reset_layout">Ripristina quello predefinito di Steam</string> + <string name="touch_no_binding">Questo controllo non ha un\'associazione in Steam per assegnargli un\'icona</string> + <string name="touch_icon_for">Icona per %1$s</string> + <string name="touch_layout_color">Colore del layout touch</string> + <string name="touch_done">Fine</string> + <string name="touch_save_failed">Impossibile salvare il layout touch</string> + <string name="touch_saved">Layout touch salvato su Steam</string> + <string name="touch_app_id">App %1$d</string> + <string name="touch_label_hint">Etichetta (opzionale)</string> + <string name="touch_icons_missing">Le icone di Steam non sono state trovate</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d icona da Steam</item> + <item quantity="many">%1$d icone da Steam</item> + <item quantity="other">%1$d icone da Steam</item> + </plurals> + <string name="touch_icon_color">Colore dell\'icona</string> + <string name="touch_button_color">Colore del tasto</string> + <string name="touch_no_icon">Nessuna icona</string> + + <string name="stores_sync_sign_in">Verifica dell\'accesso…</string> + <string name="stores_sync_library">Recupero della libreria…</string> + <string name="stores_sync_game_list">Recupero dell\'elenco giochi…</string> + <string name="stores_sync_details">Caricamento dei dettagli dei giochi… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">Sincronizzazione di %1$d gioco…</item> + <item quantity="many">Sincronizzazione di %1$d giochi…</item> + <item quantity="other">Sincronizzazione di %1$d giochi…</item> + </plurals> + <string name="stores_sync_progress">Sincronizzazione… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Recupero delle copertine…</string> + <string name="stores_sync_failed">Sincronizzazione non riuscita</string> + <string name="stores_sync_no_games">Nessun gioco trovato nella tua libreria %1$s</string> + <string name="stores_sync_amazon_no_answer">Amazon Games non ha risposto per la tua libreria: aggiorna, oppure accedi di nuovo</string> + <string name="stores_sync_gog_unreachable">Impossibile contattare GOG</string> + <string name="stores_sync_gog_unreadable">Impossibile leggere la tua libreria GOG</string> + <string name="stores_registering">Registrazione su Steam…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Modalità Cardboard</string> + <string name="cardboard_mode_hint">Immagini per l\'occhio sinistro e destro, per un visore VR per telefono.</string> + <string name="cardboard_source">Immagine</string> + <string name="cardboard_source_screen">Schermo virtuale</string> + <string name="cardboard_source_sbs">3D affiancato</string> + <string name="cardboard_viewer">Visore</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Senza lenti</string> + <string name="cardboard_ipd">Distanza tra gli occhi</string> + <string name="cardboard_screen_width">Larghezza schermo</string> + <string name="cardboard_screen_distance">Distanza schermo</string> + <string name="cardboard_head_tracking">Tracciamento della testa</string> + <string name="cardboard_no_sensor">Questo dispositivo non ha un sensore di rotazione.</string> + <string name="cardboard_recenter">Ricentra vista</string> + <string name="cardboard_recenter_button">Ricentra</string> + <string name="cardboard_steamvr">SteamVR (sperimentale)</string> + <string name="cardboard_steamvr_install">Installa</string> + <string name="cardboard_steamvr_install_hint">Scarica SteamVR ARM64 di Valve (571 MB).</string> + <string name="cardboard_steamvr_installing">Download ed estrazione di SteamVR…</string> + <string name="cardboard_steamvr_start">Avvia</string> + <string name="cardboard_steamvr_ready_hint">Avvia SteamVR usando la tua testa come visore; l\'immagine passa al 3D affiancato.</string> + <string name="cardboard_steamvr_stop">Arresta</string> + <string name="cardboard_steamvr_running_hint">SteamVR è in esecuzione.</string> + <string name="cardboard_steamvr_unavailable">Non disponibile in questa sessione.</string> + <string name="cardboard_steamvr_failed">SteamVR non ha risposto; controlla il log della sessione.</string> + <string name="drawer_component_downloads">Scarica componenti</string> + <string name="drawer_component_downloads_hint">I download sono condivisi con la pagina Componenti. Scegli sopra una versione scaricata per usarla al prossimo avvio del gioco.</string> + <string name="drawer_component_choose_download">Scegli una versione da scaricare</string> + <string name="stores_fact_developer">Sviluppatore</string> + <string name="stores_fact_publisher">Editore</string> + <string name="stores_fact_released">Uscita</string> + <string name="stores_hltb_main">Principale</string> + <string name="stores_hltb_extras">+ Extra</string> + <string name="stores_hltb_completionist">Completista</string> + <string name="stores_hltb_hours">%1$s h</string> + <string name="stores_hltb_minutes">%1$d min</string> +</resources> diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index dd4d1f3c8..d503db202 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -15,8 +15,10 @@ <string name="session_channel">セッション</string> <string name="session_channel_description">Steamまたはデスクトップのセッション中に表示</string> <string name="session_notification">Steamが実行中です</string> + <string name="session_notification_downloads">Steamのダウンロードが進行中</string> <string name="session_notification_paused">セッションが一時停止中 - 準備ができたらタップして再開</string> <string name="resume_session">再開</string> + <string name="suspend_now">今すぐ一時停止</string> <string name="stop_session">セッションを終了</string> <string name="frame_gen_title">フレーム生成</string> <string name="frame_gen_hint">使用するフレーム生成モードを選択してください</string> @@ -182,6 +184,8 @@ <string name="add_app_flatpak_id">アプリID</string> <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> <string name="add_app_flatpak_invalid">有効なFlathubアプリIDではありません</string> + <string name="add_app_flatpak_bundle_hint">Flathubにないアプリの場合は、.flatpakファイルも使えます。</string> + <string name="add_app_pick_flatpak">.flatpakファイルを選択</string> <string name="add_app_source">ソース</string> <string name="add_app_appearance">デスクトップに表示</string> <string name="add_app_preview_name">新しいアプリ</string> @@ -251,6 +255,10 @@ <string name="mode_osc">画面コントローラー</string> <string name="mode_osc_qam">Steam + QAM</string> <string name="mode_osc_note">自動では、コントローラーが接続されていないときに画面コントローラーを表示します。</string> + <string name="osc_steam_touch">Steamタッチ(ベータ版)</string> + <string name="osc_style_next_session">Steamのタッチコントロールは次回のSteamセッションから有効になります</string> + <string name="osc_steam_layout">Steamタッチのレイアウト</string> + <string name="osc_steam_layout_hint">このゲームのタッチコントロールを移動、サイズ変更、非表示にします。Steamに保存されます</string> <string name="mode_controller">コントローラー</string> <string name="mode_controller_hint">Steamがコントローラーを認識する方法。次回のセッションで有効になります。</string> <string name="mode_controller_deck">Steam Deckコントローラー</string> @@ -268,6 +276,8 @@ <string name="mode_suspend_manual">手動</string> <string name="mode_suspend_native">ネイティブ</string> <string name="mode_suspend_steam_note">自動では一時停止し、表示すると再開します。ネイティブでは先にSteamをスリープに備えさせます。手動では「再開」をタップするまで待機します。「しない」では実行し続けます。</string> + <string name="mode_background_downloads">ダウンロードを継続</string> + <string name="mode_background_downloads_hint">Steamがダウンロード中は、バックグラウンドでの一時停止を遅らせます。</string> <string name="mode_suspend_note">自動ではバックグラウンドで一時停止し、表示すると再開します。手動では「再開」をタップするまで待機します。「しない」では実行し続けます。</string> <string name="mode_startup">起動</string> <string name="mode_steam_startup">DroidDeck起動時にSteamを起動</string> @@ -331,6 +341,7 @@ <string name="wincomp_found_shipped">ゲームに%1$sがあります(そのままで動くことが多いです)</string> <string name="wincomp_already_here">このゲームに導入済み</string> <string name="wincomp_no_recommendation">ゲームのフォルダーには、必要なコンポーネントを示すものがありません。</string> + <string name="wincomp_active">このゲームにインストール済み(%1$d)</string> <string name="wincomp_all">すべてのコンポーネント(%1$d)</string> <string name="wincomp_installers">Windowsインストーラーが必要(%1$d)</string> <string name="wincomp_installers_note">これらはプレフィックス内で実際のインストーラーを実行します(.NET、PhysX、XNA、Visual C++のフルインストーラーなど)。DroidDeckはまだ対応していませんが、次に対応予定です。</string> @@ -341,6 +352,9 @@ <string name="wincomp_loading">コンポーネントを読み込み中…</string> <string name="wincomp_failed">インストールできませんでした: %1$s</string> <string name="wincomp_downloaded">ダウンロード済み</string> + <string name="wincomp_downloading">%1$sをダウンロード中</string> + <string name="wincomp_installing">インストール中:%1$s</string> + <string name="wincomp_reverting">オフにしています…</string> <string name="wincomp_download_size">ダウンロード %1$s</string> <string name="wincomp_next_launch">次にゲームを起動したときに反映されます。</string> <string name="mode_stretch169">16:9ゲームを引き伸ばす</string> @@ -397,6 +411,44 @@ <string name="comp_check_nightlies">Nightliesの新しいパッケージを確認</string> <string name="comp_proton">Proton</string> <string name="comp_proton_note">ここで選択したProtonは、Steamで個別のProtonが選択されていないすべてのゲームを実行します。Steamで変更した場合もここに反映されます。Steamのアップデート時にValveのProtonは置き換えられますが、元のファイルはビルドごとに保持されます。</string> + <string name="comp_scope">構成</string> + <string name="comp_profile_configure">プロファイルを設定</string> + <string name="comp_profile">プロファイル</string> + <string name="comp_scope_general">デフォルトプロファイル</string> + <string name="comp_scope_general_detail">個別設定のないゲームで自動的に使用されます。</string> + <string name="comp_profile_game_detail">%1$sの起動時に自動的に使用される保存済みの設定です。</string> + <string name="comp_game_hint">このゲームに必要な設定だけを選んでください。それ以外はデフォルトプロファイルを引き続き使用します。</string> + <string name="comp_game_fex">FEXプリセット</string> + <string name="comp_game_fex_inherited">デフォルトプロファイルのプリセットを使用中。</string> + <string name="comp_game_fex_override">このゲーム用のカスタムプリセットを保存済み。</string> + <string name="comp_game_fex_custom">カスタムFEX変数</string> + <string name="comp_game_use_general">デフォルトを使用</string> + <string name="comp_game_compatibility">ゲームの互換性</string> + <string name="comp_game_environment_hint">このゲームのProton、DXVK、VKD3D、同期の環境オプション。</string> + <string name="comp_game_proton_inherited">デフォルトプロファイルのProtonを使用中。</string> + <string name="comp_game_proton_override">Steamの互換性マッピングを通じて、このゲーム用のカスタムProtonを保存済み。</string> + <string name="comp_no_proton_short">Protonなし</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">このゲームで別のパッケージを選択しない限り、%1$sのデフォルトプロファイルのパッケージを使用します。</string> + <string name="comp_game_component_general">デフォルトプロファイルのコンポーネントパッケージを使用します。</string> + <string name="comp_game_component_override">このゲームでのみ%1$sを使用します。</string> + <string name="comp_game_runtime_options">ランタイムオプション</string> + <string name="comp_game_environment_other_hint">その他のゲームごとのProton、DXVK、VKD3D-Proton、同期の環境オプション。</string> + <string name="comp_game_saved_next">%1$sのプロファイルを保存しました。次回のゲーム起動時に有効になります。</string> + <string name="game_no_steam_title">Steamなしで起動</string> + <string name="game_no_steam_detail">すぐに終了してしまうゲーム用:ProtonのSteamセットアップなしで起動します。オーバーレイなどのSteam機能はこのゲームでは無効になります。</string> + <string name="game_no_steam_on">オン</string> + <string name="comp_game_proton_live">%1$sのProtonの選択を保存しました。Steamですぐに使用できます。</string> + <string name="comp_game_proton_next">%1$sのProtonの選択を保存しました。ゲームを起動する前にSteamを再起動してください。</string> + <string name="comp_game_proton_not_runnable">%1$sは、選択したProtonをこのデバイスで使用できません。</string> + <string name="comp_game_load_failed">%1$sのプロファイルを読み込めませんでした:%2$s</string> + <string name="comp_game_save_failed">ゲームプロファイルを保存できませんでした:%1$s</string> + <string name="comp_game_reset">ゲームプロファイルをリセット</string> + <string name="comp_game_reset_title">%1$sのプロファイルをリセットしますか?</string> + <string name="comp_game_reset_body">このゲームのProton、FEX、コンポーネントパッケージ、環境、テクスチャフィルタリング、Windowsコンポーネントの個別設定を削除しますか?デフォルトプロファイルが使用されます。</string> + <string name="comp_game_reset_done">%1$sはデフォルトプロファイルを使用するようになりました。</string> + <string name="comp_game_reset_failed">ゲームプロファイルを完全にはリセットできませんでした:%1$s</string> <string name="comp_default_live">Steamで個別のProtonが選択されていないすべてのゲームを、現在%1$sで実行します。</string> <string name="comp_default_next">次回Steamの起動時から、個別のProtonが選択されていないすべてのゲームを%1$sで実行します。</string> <string name="comp_default_not_runnable">%1$sはここでゲームを起動できないため、デフォルトにできません。</string> @@ -560,6 +612,18 @@ <string name="drawer_stop_title">セッションを停止しますか?</string> <string name="drawer_running_now">現在実行中</string> <string name="drawer_no_game">現在Protonで実行中のゲームはありません</string> + <string name="drawer_selected_profile">ゲームプロファイル</string> + <string name="drawer_selected_profile_none">プロファイルの詳細はまだ利用できません。</string> + <string name="drawer_profile_refresh_failed_initial">ゲームプロファイルを読み込めませんでした。自動的に再試行します。</string> + <string name="drawer_profile_refresh_failed_stale">ゲームプロファイルを更新できませんでした。最後に取得したデータを表示しています。自動的に再試行します。</string> + <string name="drawer_profile_inspect_hint">次回の起動時に使用される保存済みの設定を表示・編集します。</string> + <string name="drawer_profile_default">デフォルトプロファイルを使用中</string> + <string name="drawer_profile_custom">カスタムプロファイル</string> + <string name="drawer_component_inherited">デフォルトプロファイルの%1$sを使用中。</string> + <string name="drawer_game_components_note">変更は%1$sのプロファイルに保存され、次回のゲーム起動時に有効になります。</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="other">%1$d件のゲーム個別設定</item> + </plurals> <string name="drawer_no_proton">Protonがインストールされていません。</string> <string name="drawer_game_running">ゲーム実行中</string> <string name="drawer_reapplied">%1$sの起動時に再適用しました</string> @@ -616,6 +680,14 @@ <string name="setup_saved_logs">保存されたセッションログ</string> <string name="setup_saved_logs_hint">最新の%1$d件を保持</string> <string name="setup_clear_logs">ログを消去</string> + <string name="setup_reset_settings">すべての設定をリセット</string> + <string name="setup_reset_settings_hint">すべての設定をデフォルトに戻します</string> + <string name="setup_reset_settings_button">リセット</string> + <string name="setup_reset_settings_title">すべての設定をリセットしますか?</string> + <string name="setup_reset_settings_body">画面、パフォーマンス、コントローラー、テーマ、言語、ランチャーの設定がデフォルトに戻ります。ゲーム、Steamのサインイン、ストアのアカウント、ダウンロードしたドライバー、ストレージフォルダーは保持されます。</string> + <string name="setup_reset_settings_profiles">ゲームプロファイルもリセットする(各ゲームのProton、FEX、コンポーネント、環境、およびデフォルトプロファイルの環境)</string> + <string name="setup_reset_settings_session_running">設定をリセットする前にセッションを停止してください。</string> + <string name="setup_reset_settings_partial">設定はリセットされましたが、一部をリセットできませんでした。詳細はログを確認してください。</string> <string name="setup_offline">オフラインモード</string> <string name="setup_sign_in_first">先にSteamにログインしてください</string> <string name="setup_launcher">ランチャー</string> @@ -631,6 +703,10 @@ <string name="setup_home">ホーム画面として使用</string> <string name="setup_home_on">DroidDeckをホームアプリとして使用できます</string> <string name="setup_home_off">DroidDeckはホームアプリとして提供されません</string> + <string name="setup_orientation">画面の向き</string> + <string name="setup_orientation_auto">横向き(自動回転)</string> + <string name="setup_orientation_landscape">横向き</string> + <string name="setup_orientation_reverse">横向き(反転)</string> <string name="setup_fullscreen">全画面</string> <string name="setup_fullscreen_on">Androidのステータスバーとナビゲーションバーを非表示</string> <string name="setup_fullscreen_off">Androidのステータスバーとナビゲーションバーを表示</string> @@ -888,6 +964,12 @@ <string name="content_page_gone">このページは存在しません。左側からセクションを選択してください。</string> <string name="content_open_desktop">デスクトップを開く</string> <string name="content_install_open_desktop">インストールしてデスクトップを開く</string> + <string name="desktop_remove_title">デスクトップをアンインストール</string> + <string name="desktop_remove_text">デスクトップと同梱のアプリを削除します。ゲーム、追加したアプリ、設定、共有ライブラリは保持されます。</string> + <string name="desktop_removing">デスクトップを削除中</string> + <string name="desktop_removed">デスクトップを削除しました</string> + <string name="desktop_cleaning_legacy">古いLXQtファイルを削除中</string> + <string name="desktop_removal_unknown">このバージョンのデスクトップを削除する前に、DroidDeckを更新してください。</string> <string name="content_linux_desktop">Linuxデスクトップ</string> <string name="content_linux_desktop_hint">KDE Plasma、Firefox、エミュレーター</string> <string name="games_empty">Steamでインストールされたゲームは、セッション終了後にここに表示されます。</string> @@ -917,6 +999,8 @@ <string name="games_last_played">最終プレイ %1$s</string> <string name="games_internal">内部ストレージ</string> <string name="games_added">追加されたゲーム</string> + <string name="games_custom">カスタムゲーム</string> + <string name="games_store_storage">ゲームストレージ</string> <string name="gpu_auto_hint">このGPUに最適なドライバー構成を選択し、最新の状態に保ちます</string> <string name="gpu_manual_hint">ドライバー構成を自分で選択する</string> <string name="gpu_checking">ドライバーを確認中…</string> @@ -958,6 +1042,12 @@ <string name="deck_tab_left">左トラックパッド</string> <string name="deck_tab_right">右トラックパッド</string> <string name="deck_tab_both">両方</string> + <string name="deck_tab_gamepad">ゲームパッド</string> + <string name="deck_surface_stick">スティック</string> + <string name="deck_surface_trackpad">トラックパッド</string> + <string name="deck_surface_switch">スティックとトラックパッドを切り替え</string> + <string name="deck_more_controls">その他のDeckコントロール</string> + <string name="deck_panels">パネル</string> <string name="deck_close">セカンドスクリーンコントロールを閉じる</string> <string name="deck_open_steam_menu">Steamメニューを開く</string> <string name="deck_left_upper">左 · 上</string> @@ -993,6 +1083,10 @@ <string name="proton_installed">%1$s インストール済み</string> <string name="proton_queued">前回のリクエストによる保留中の項目</string> <string name="proton_not_installed">未インストール</string> + <string name="proton_update_available">アップデートあり → %1$s</string> + <string name="proton_remove_older">古い%1$sのバージョンを削除</string> + <string name="proton_remove_older_body">%1$sを削除します。これらのバージョンに設定されているゲームは、別のProtonに切り替える必要があります。</string> + <string name="proton_remove_body">このバージョンに設定されているゲームは、別のProtonに切り替える必要があります。</string> <string name="proton_install_now">今すぐインストール</string> <string name="proton_cancel_queued">キューをキャンセル</string> <string name="pckb_android">Androidキーボード</string> @@ -1021,6 +1115,8 @@ <string name="game_env_eyebrow">Protonゲーム</string> <string name="pip_title">ピクチャーインピクチャー</string> <string name="pip_auto">自動でピクチャーインピクチャー</string> + <string name="unfolded_controls">開いた状態の画面のコントロール</string> + <string name="unfolded_controls_hint">開いた状態では、ゲームがSteam Deckのトラックパッドと背面グリップの上に表示されます。折りたたんだ状態では、ゲームが全画面で表示されます。</string> <string name="pip_suspended">一時停止中</string> <string name="pip_not_ready">セッションの準備ができたらピクチャーインピクチャーを使用できます。</string> <string name="pip_permission">Android設定でDroidDeckのピクチャーインピクチャーを許可してください。</string> @@ -1046,19 +1142,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> @@ -1066,19 +1155,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> @@ -1091,78 +1172,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> @@ -1170,19 +1228,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> @@ -1197,11 +1249,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> @@ -1214,39 +1262,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> @@ -1261,33 +1295,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> @@ -1300,28 +1321,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> @@ -1330,8 +1337,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> @@ -1349,27 +1354,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> @@ -1387,8 +1382,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> @@ -1396,8 +1389,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> @@ -1411,19 +1402,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> @@ -1431,8 +1414,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> @@ -1443,20 +1424,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> @@ -1470,8 +1445,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> @@ -1486,51 +1459,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> @@ -1539,8 +1492,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> @@ -1551,34 +1502,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> @@ -1593,75 +1531,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> @@ -1697,27 +1607,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> @@ -1725,24 +1621,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> @@ -1756,31 +1642,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> @@ -1788,19 +1665,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> @@ -1817,55 +1686,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> @@ -1890,29 +1742,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> @@ -1923,115 +1769,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> @@ -2049,6 +1841,7 @@ <string name="display_frame_rate">フレームレート</string> <string name="display_look_filter">%1$s スケーリングを%2$sに設定します。</string> <string name="display_match_screen">画面に合わせる(%1$d×%2$d)</string> + <string name="display_follow_screen">画面に追従(開閉時にサイズを変更)</string> <string name="mode_tab_steam">Steam</string> <string name="device_info_show">デバイスの詳細を表示</string> <string name="device_info_hide">デバイスの詳細を非表示</string> @@ -2058,6 +1851,186 @@ <string name="device_info_copied">コピーしました。</string> <string name="force_ssbs_title">SSBSを強制</string> <string name="force_ssbs_hint">Wineがオフにし続けるCPUの速度設定をオンに保ちます。Snapdragon 8 EliteではWindowsゲームが速くなることがあります。古いチップではほとんど変化がありません。次のゲーム起動時に反映されます。</string> + + <string name="rail_stores">ストア</string> + <string name="source_steam">Steam</string> + <string name="source_added">カスタム</string> + <string name="setup_stores">ストア</string> + <string name="setup_stores_open_on">ストアの初期表示</string> + <string name="setup_stores_speed">ダウンロード速度レベル</string> + <string name="setup_stores_engine">ストアエンジン</string> + <string name="setup_stores_engine_chip_ready">● エンジン準備完了</string> + <string name="setup_stores_engine_chip_missing">未ビルド</string> + <string name="stores_tier_balanced">バランス</string> + <string name="stores_tier_fast">高速</string> + <string name="stores_tier_max">最大</string> + <string name="stores_title">ストア</string> + <string name="stores_settings">ストア設定</string> + <string name="stores_settings_eyebrow">設定</string> + <string name="stores_chip_signed_out">(未ログイン)</string> + <string name="stores_tab_store">ストア</string> + <string name="stores_tab_installed">インストール済み</string> + <string name="stores_tab_library">ライブラリ</string> + <string name="stores_tab_all">すべて</string> + <string name="stores_tab_downloads">ダウンロード</string> + <string name="stores_tab_count">%1$s(%2$d)</string> + <string name="stores_signin">ログイン</string> + <string name="stores_signout_named">%1$s · ログアウト</string> + <string name="stores_signout_title">%1$sからログアウトしますか?</string> + <string name="stores_signout_note">インストール済みのゲームは残ります。再度ログインすると、ライブラリをもう一度取得します。</string> + <string name="stores_signout_stay">ログインしたままにする</string> + <string name="stores_signout_go">ログアウト</string> + <string name="stores_search_catalog">%1$sのカタログを検索…</string> + <string name="stores_search_installed">インストール済みのゲームを検索…</string> + <string name="stores_search_library">ライブラリを検索…</string> + <string name="stores_search_open">検索</string> + <string name="stores_installed_empty">何もインストールされていません</string> + <string name="stores_library_loading">読み込み中</string> + <string name="stores_nothing_matches">ゲームがありません</string> + <string name="stores_shelves_loading">ストアを読み込み中…</string> + <string name="stores_amazon_no_catalog">カタログなし</string> + <string name="stores_shelf_new">新着</string> + <string name="stores_shelf_deals">セール</string> + <string name="stores_shelf_free">無料ゲーム</string> + <string name="stores_shelf_trending">トレンド</string> + <string name="stores_shelf_library">マイライブラリ</string> + <string name="stores_play">プレイ</string> + <string name="stores_install">インストール</string> + <string name="stores_resume_install">インストールを再開</string> + <string name="stores_dl_interrupted">中断</string> + <string name="stores_install_size">インストール · %1$s</string> + <string name="stores_get_free">無料で入手</string> + <string name="stores_view_on">%1$sで見る</string> + <string name="stores_buy_price">購入 · %1$s</string> + <string name="stores_installed_chip">インストール済み</string> + <string name="stores_in_library">ライブラリにあり</string> + <string name="stores_free">無料</string> + <string name="stores_resume">再開</string> + <string name="stores_refresh">更新</string> + <string name="stores_show_mature">成人向けコンテンツを表示</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">オフライン</string> + <string name="epic_card_none">オフ</string> + <string name="epic_eos">Epicログイン(EOS)</string> + <string name="epic_offline">オフラインで起動</string> + <string name="epic_resolve">Epicログインの問題を解決</string> + <string name="store_cloud_saves">クラウドセーブ</string> + <string name="cloud_upload">クラウドにアップロード</string> + <string name="cloud_download">クラウドからダウンロード</string> + <string name="cloud_none">クラウドセーブなし</string> + <string name="cloud_conflict">クラウドの競合</string> + <string name="cloud_conflict_files">クラウドの競合 · %1$d個のファイル</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">クラウドを残す</string> + <string name="cloud_keep_local">ローカルを残す</string> + <string name="cloud_checking">クラウドを確認中…</string> + <string name="cloud_last_sync">最終同期 %1$s</string> + <string name="cloud_never">未同期</string> + <string name="cloud_done">%1$d個のファイル</string> + <string name="stores_eyebrow_installed">インストール済み</string> + <string name="stores_eyebrow_library">ライブラリ内</string> + <string name="stores_eyebrow_store">ストア</string> + <string name="achievements_title">実績</string> + <string name="achievements_count">%2$d 個中 %1$d 個を解除</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">解除済み · %1$d</string> + <string name="achievements_group_locked">未解除 · %1$d</string> + <string name="achievements_group_hidden">隠し実績 · %1$d</string> + <string name="achievements_hidden">隠し実績</string> + <string name="achievements_hidden_detail">解除すると詳細が表示されます。</string> + <string name="achievements_all">すべての実績を解除</string> + <string name="achievements_reading">読み込み中…</string> + <string name="achievements_signed_out">実績を表示するには %1$s にサインインしてください。</string> + <string name="achievements_unreachable">%1$s に接続できませんでした。オンラインになってから再度確認してください。</string> + <string name="achievements_none">%1$s にはこのゲームの実績がありません。</string> + <string name="achievements_show_all">すべて表示(%1$d)</string> + <string name="achievements_dialog_title">実績 · %1$s</string> + <string name="stores_in_steam">✓ Steamに追加済み</string> + <string name="stores_uninstall">アンインストール</string> + <string name="stores_uninstalling">アンインストール中…</string> + <string name="stores_uninstall_title">%1$sをアンインストールしますか?</string> + <string name="stores_uninstall_body_size">%1$sの空き容量を確保し、Steamから削除します。</string> + <string name="stores_uninstall_body">ファイルとSteamのショートカットを削除します。</string> + <string name="stores_launch_not_registered">Steamは次回の起動時にこのゲームを認識します。Steamから起動するか、今すぐSteamを起動してください。</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">ゲームごとにSteamが選択</string> + <string name="stores_card_audio">オーディオ</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">コンポーネント</string> + <string name="stores_card_components_value">FEX、DXVK、VKD3D</string> + <string name="stores_card_engine">エンジン</string> + <string name="stores_card_engine_value">%1$s · ネイティブ</string> + <string name="stores_card_engine_java">%1$s · 内蔵</string> + <string name="stores_how_it_runs">実行の仕組み</string> + <string name="stores_kv_fetched">取得元</string> + <string name="stores_kv_fetched_value">%1$s(あなたのアカウントで取得)</string> + <string name="stores_kv_installed_to">インストール先</string> + <string name="stores_kv_installed_to_value">ゲームストレージ(Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">起動方法</string> + <string name="stores_kv_launched_by_value">SteamとProton(Steamゲームと同じ経路)</string> + <string name="stores_kv_launched_by_epic">SteamとProton · 起動ごとにEpicログイン、EOSオーバーレイはオフ</string> + <string name="stores_kv_launched_by_amazon">SteamとProton · Amazon Games Servicesの変数を設定</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">インストール時にSteamに追加(Deck UI + Steam Input)</string> + <string name="stores_downloads_empty">ダウンロードはありません</string> + <string name="stores_dl_paused">一時停止中</string> + <string name="stores_dl_queued">待機中</string> + <string name="stores_dl_queued_at">待機中 · #%1$d</string> + <string name="stores_dl_cancelled">キャンセル済み</string> + <string name="stores_dl_failed">失敗</string> + <string name="stores_dl_failed_reason">失敗 · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">一時停止</string> + <string name="stores_dl_resume">再開</string> + <string name="stores_dl_cancel">キャンセル</string> + <string name="stores_dl_retry">再試行</string> + <string name="stores_dl_clear">消去</string> + <string name="stores_dl_clear_all">完了分を消去</string> + <string name="stores_stage_manifest">マニフェスト</string> + <string name="stores_stage_download">ダウンロード</string> + <string name="stores_stage_verify">検証</string> + <string name="stores_stage_install">インストール</string> + <string name="stores_stage_done">完了</string> + <string name="stores_stage_checking">確認中</string> + <string name="stores_stage_downloading">ダウンロード中</string> + <string name="stores_dl_cancel_confirm">ダウンロードを削除しますか?</string> + <string name="stores_dl_failed_kept">失敗 · %1$sを保持</string> + <string name="stores_stage_verifying">検証中</string> + <string name="stores_stage_installing">インストール中</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">ファイル %1$d/%2$d</string> + <string name="stores_download_manager">ダウンロードマネージャー</string> + <string name="stores_parallel">同時ダウンロード数</string> + <string name="stores_install_where">インストール先</string> + <string name="stores_install_title">%1$sをインストール:</string> + <string name="stores_target_internal">内部ストレージ</string> + <string name="stores_target_free">空き %1$s</string> + <string name="stores_notification_channel">ストアのダウンロード</string> + <string name="stores_notification_channel_hint">ゲームのダウンロードの進行状況を表示し、バックグラウンドでダウンロードを継続します</string> + <string name="stores_notification_one">ダウンロード中</string> + <string name="stores_notification_many">%1$d件のダウンロード</string> + <string name="stores_notification_waiting">準備中…</string> + <string name="stores_notification_done_channel">完了したストアのダウンロード</string> + <string name="stores_notification_installed">%1$sをインストールしました</string> + <string name="stores_notification_failed">%1$sのダウンロードに失敗しました</string> + <string name="stores_notification_delete">削除</string> + <string name="stores_notification_keep">保持</string> + <string name="stores_login_title">%1$sにログイン</string> + <string name="stores_login_provider_title">プロバイダーでログイン</string> + <string name="stores_login_reload">再読み込み</string> + <string name="stores_login_close">閉じる</string> + <string name="stores_login_retry">再試行</string> + <string name="stores_login_finishing">ログインを完了しています…</string> + <string name="stores_login_error_network">ログインページを読み込めませんでした:%1$s</string> + <string name="stores_login_error_idp_blocked">このプロバイダーは埋め込みページからのログインを拒否しています。代わりにストアのパスワードでログインしてください。</string> + <string name="stores_login_error_http">ログインページがエラーを返しました(HTTP %1$d)。</string> + <string name="stores_login_error_ssl">ログインページへの接続は安全ではありません。</string> + <string name="stores_login_error_verification">ログインを確認できませんでした。もう一度お試しください。</string> + <string name="stores_login_error_generic">ログインに失敗しました。もう一度お試しください。</string> <string name="common_close">閉じる</string> <string name="common_back">戻る</string> <string name="common_edit">編集</string> @@ -2089,21 +2062,122 @@ <plurals name="games_already_n"> <item quantity="other">%1$d 本はすでにゲームにあります</item> </plurals> - <string name="achievements_title">実績</string> - <string name="achievements_count">%2$d 個中 %1$d 個を解除</string> - <string name="achievements_count_short">%1$d / %2$d</string> - <string name="achievements_group_unlocked">解除済み · %1$d</string> - <string name="achievements_group_locked">未解除 · %1$d</string> - <string name="achievements_group_hidden">隠し実績 · %1$d</string> - <string name="achievements_hidden">隠し実績</string> - <string name="achievements_hidden_detail">解除すると詳細が表示されます。</string> - <string name="achievements_all">すべての実績を解除</string> - <string name="achievements_reading">読み込み中…</string> - <string name="achievements_signed_out">実績を表示するには %1$s にサインインしてください。</string> - <string name="achievements_unreachable">%1$s に接続できませんでした。オンラインになってから再度確認してください。</string> - <string name="achievements_none">%1$s にはこのゲームの実績がありません。</string> - <string name="achievements_show_all">すべて表示(%1$d)</string> - <string name="achievements_dialog_title">実績 · %1$s</string> + <string name="hop_back">B · %1$sに戻る</string> + <string name="hop_from_games">ゲーム</string> + <string name="hop_from_play">プレイ設定</string> + <string name="sheet_graphics_drivers">グラフィックドライバー</string> + <string name="comp_this_proton">このProton</string> + <string name="comp_every_proton">すべてのProton</string> + <string name="comp_for_every_proton">すべてのProtonに適用</string> + <string name="comp_available_count">利用可能 · %1$d</string> + <string name="comp_checked">確認日時 %1$s</string> + <string name="comp_check_all">新しいコンポーネントとドライバーを確認</string> + <string name="comp_get">入手</string> + <string name="comp_proton_bar">%1$s · 以下のDXVK、VKD3D-Proton、FEXはこのProtonのものです</string> + <string name="comp_device_tested">%1$s · テスト済み</string> + <string name="comp_device_untested">%1$s · 未テスト</string> + <string name="comp_device_unsupported">%1$s · サポート外</string> + <string name="gpu_auto_keeps">推奨の組み合わせを最新に保ちます</string> + <string name="gpu_bar_auto">自動 · %1$s</string> + <string name="gpu_separately">ランタイムドライバーとディスプレイドライバーを個別に設定</string> + <string name="gpu_use_title">%1$sを使用しますか?</string> + <string name="proton_remove_title">%1$sを削除しますか?</string> + <string name="setup_tab_diagnostics">診断</string> + <string name="setup_ready">プレイの準備完了</string> + <string name="setup_not_ready">まだプレイの準備ができていません</string> + <string name="setup_ready_runtime">実行環境 %1$s</string> + <string name="setup_ready_limit_off">プロセス制限オフ</string> + <string name="setup_ready_signed_in">%1$sとしてログイン中</string> + <string name="setup_needs_you">対応が必要なとき</string> + <string name="setup_group_look">外観</string> + <string name="setup_group_device">このデバイス</string> + <string name="setup_group_art">ライブラリのアートワーク</string> + <string name="flathub_eyebrow">Linuxアプリ</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Linuxデスクトップ用のアプリ。「デスクトップ」から開きます。</string> + <string name="flathub_setup">Flathubをセットアップ</string> + <string name="flathub_remove_title">%1$sを削除しますか?</string> + <string name="flathub_remove_note">設定とファイルは保持されます。</string> + <string name="flathub_keep">保持</string> + <string name="desktop_size">ディスク上のサイズ</string> + <string name="desktop_uninstall_title">デスクトップをアンインストールしますか?</string> + <string name="desktop_all_systems">すべて</string> + <string name="gog_achievements">GOGの実績</string> + <string name="gog_achievements_hint">対応するGOGゲームの実績を同期します。次回の起動から適用されます。</string> + <string name="gog_achievements_off">実績オフ</string> + <string name="gog_achievements_on">実績オン</string> + <string name="gog_achievements_signin">GOGに再度サインインしてください</string> + <string name="gog_achievements_uploaded">実績をアップロードしました</string> + <string name="gog_achievements_pending">実績のアップロード待ち</string> + <string name="gog_achievements_enable_store">ストア設定でGOGの実績を有効にしてください</string> + <string name="gog_achievements_unavailable">実績を利用できません。セッションログを確認してください</string> + <string name="achievement_unlocked">実績を解除しました</string> + <string name="touch_paste">貼り付け</string> + <string name="touch_dpad">十字キー</string> + <string name="touch_left_stick">左スティック</string> + <string name="touch_right_stick">右スティック</string> + <string name="touch_menu">メニュー</string> + <string name="touch_keyboard">キーボード</string> + <string name="touch_dpad_up">十字キー上</string> + <string name="touch_dpad_down">十字キー下</string> + <string name="touch_dpad_left">十字キー左</string> + <string name="touch_dpad_right">十字キー右</string> + <string name="touch_title">タッチ操作</string> + <string name="touch_controller">タッチコントローラー</string> + <string name="touch_mouse">マウス</string> + <string name="touch_on">オン</string> + <string name="touch_off">オフ</string> + <string name="touch_speed_slow">遅い</string> + <string name="touch_speed_normal">普通</string> + <string name="touch_speed_fast">速い</string> + <string name="touch_edit_layout">レイアウトを編集</string> + <string name="touch_droiddeck_menu">DroidDeckメニュー</string> + <string name="touch_drag_hint">操作ボタンを画面にドラッグしてください</string> + <string name="touch_arrange">配置</string> + <string name="touch_discard">破棄</string> + <string name="touch_saving">保存中…</string> + <string name="touch_icon">アイコン</string> + <string name="touch_narrower">幅−</string> + <string name="touch_wider">幅+</string> + <string name="touch_shorter">高−</string> + <string name="touch_taller">高+</string> + <string name="touch_hide">非表示</string> + <string name="touch_color_opacity">色と不透明度…</string> + <string name="touch_copy_layout">このレイアウトをコピー</string> + <string name="touch_layout_copied">レイアウトをコピーしました。任意のゲームのタッチレイアウトに貼り付けられます</string> + <string name="touch_paste_layout">コピーしたレイアウトを貼り付け</string> + <string name="touch_reset_layout">Steamの初期設定に戻す</string> + <string name="touch_no_binding">この操作にはSteamでアイコンを設定できる割り当てがありません</string> + <string name="touch_icon_for">%1$sのアイコン</string> + <string name="touch_layout_color">タッチレイアウトの色</string> + <string name="touch_done">完了</string> + <string name="touch_save_failed">タッチレイアウトを保存できませんでした</string> + <string name="touch_saved">タッチレイアウトをSteamに保存しました</string> + <string name="touch_app_id">アプリ%1$d</string> + <string name="touch_label_hint">ラベル(任意)</string> + <string name="touch_icons_missing">Steamのアイコンが見つかりませんでした</string> + <plurals name="touch_icons_found"> + <item quantity="other">Steamのアイコン%1$d個</item> + </plurals> + <string name="touch_icon_color">アイコンの色</string> + <string name="touch_button_color">ボタンの色</string> + <string name="touch_no_icon">アイコンなし</string> + + <string name="stores_sync_sign_in">サインインを確認中…</string> + <string name="stores_sync_library">ライブラリを取得中…</string> + <string name="stores_sync_game_list">ゲームリストを取得中…</string> + <string name="stores_sync_details">ゲーム情報を読み込み中… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="other">%1$d個のゲームを同期中…</item> + </plurals> + <string name="stores_sync_progress">同期中… %1$d/%2$d</string> + <string name="stores_sync_cover_art">カバーアートを取得中…</string> + <string name="stores_sync_failed">同期に失敗しました</string> + <string name="stores_sync_no_games">%1$sのライブラリにゲームが見つかりません</string> + <string name="stores_sync_amazon_no_answer">Amazon Gamesからライブラリの応答がありません。更新するか、再度サインインしてください</string> + <string name="stores_sync_gog_unreachable">GOGに接続できませんでした</string> + <string name="stores_sync_gog_unreadable">GOGのライブラリを読み込めませんでした</string> + <string name="stores_registering">Steamに登録中…</string> <string name="drawer_page_glasses">グラス</string> <string name="glasses_open">VITURE グラス</string> <string name="glasses_control">グラスの制御</string> @@ -2152,4 +2226,34 @@ <string name="stores_hltb_completionist">完全制覇</string> <string name="stores_hltb_hours">%1$s時間</string> <string name="stores_hltb_minutes">%1$d分</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Cardboardモード</string> + <string name="cardboard_mode_hint">スマホ用VRビューアー向けの左右の目の映像です。</string> + <string name="cardboard_source">映像</string> + <string name="cardboard_source_screen">仮想スクリーン</string> + <string name="cardboard_source_sbs">サイドバイサイド3D</string> + <string name="cardboard_viewer">ビューアー</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard(2014)</string> + <string name="cardboard_viewer_none">レンズなし</string> + <string name="cardboard_ipd">目の間隔</string> + <string name="cardboard_screen_width">スクリーンの幅</string> + <string name="cardboard_screen_distance">スクリーンまでの距離</string> + <string name="cardboard_head_tracking">ヘッドトラッキング</string> + <string name="cardboard_no_sensor">このデバイスには回転センサーがありません。</string> + <string name="cardboard_recenter">視点をリセット</string> + <string name="cardboard_recenter_button">リセット</string> + <string name="cardboard_steamvr">SteamVR(試験運用)</string> + <string name="cardboard_steamvr_install">インストール</string> + <string name="cardboard_steamvr_install_hint">ValveのARM64版SteamVRをダウンロードします(571 MB)。</string> + <string name="cardboard_steamvr_installing">SteamVRをダウンロードして展開しています…</string> + <string name="cardboard_steamvr_start">開始</string> + <string name="cardboard_steamvr_ready_hint">頭の動きをヘッドセットとしてSteamVRを実行します。映像はサイドバイサイド3Dに切り替わります。</string> + <string name="cardboard_steamvr_stop">停止</string> + <string name="cardboard_steamvr_running_hint">SteamVRは実行中です。</string> + <string name="cardboard_steamvr_unavailable">このセッションでは利用できません。</string> + <string name="cardboard_steamvr_failed">SteamVRが応答しませんでした。セッションログを確認してください。</string> + <string name="drawer_component_downloads">コンポーネントをダウンロード</string> + <string name="drawer_component_downloads_hint">ダウンロードは「コンポーネント」ページと共有されます。上でダウンロード済みのバージョンを選ぶと、次回のゲーム起動時に使用されます。</string> + <string name="drawer_component_choose_download">ダウンロードするバージョンを選択</string> </resources> diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index c23e52efd..d2bb7be8c 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -15,8 +15,10 @@ <string name="session_channel">세션</string> <string name="session_channel_description">Steam 또는 데스크톱 세션이 실행 중일 때 표시</string> <string name="session_notification">Steam이 실행 중입니다</string> + <string name="session_notification_downloads">Steam 다운로드 진행 중</string> <string name="session_notification_paused">세션이 일시 중지됨 - 준비되면 \"재개\"를 탭하세요</string> <string name="resume_session">재개</string> + <string name="suspend_now">지금 일시 중지</string> <string name="stop_session">세션 종료</string> <string name="frame_gen_title">프레임 생성</string> <string name="frame_gen_hint">사용할 프레임 생성 모드를 선택하세요</string> @@ -182,6 +184,8 @@ <string name="add_app_flatpak_id">앱 ID</string> <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> <string name="add_app_flatpak_invalid">유효한 Flathub 앱 ID가 아닙니다</string> + <string name="add_app_flatpak_bundle_hint">또는 Flathub에 없는 앱의 .flatpak 파일.</string> + <string name="add_app_pick_flatpak">.flatpak 파일 선택</string> <string name="add_app_source">소스</string> <string name="add_app_appearance">데스크톱에 표시</string> <string name="add_app_preview_name">새 앱</string> @@ -251,6 +255,10 @@ <string name="mode_osc">화면 컨트롤</string> <string name="mode_osc_qam">Steam + QAM</string> <string name="mode_osc_note">자동 모드에서는 컨트롤러가 연결되지 않은 경우 화면 컨트롤을 표시합니다.</string> + <string name="osc_steam_touch">Steam 터치(베타)</string> + <string name="osc_style_next_session">Steam 터치 컨트롤은 다음 Steam 세션부터 적용됩니다</string> + <string name="osc_steam_layout">Steam 터치 레이아웃</string> + <string name="osc_steam_layout_hint">이 게임의 터치 컨트롤을 옮기거나 크기를 바꾸거나 숨깁니다. Steam에 저장됩니다</string> <string name="mode_controller">컨트롤러</string> <string name="mode_controller_hint">Steam이 컨트롤러를 인식하는 방식입니다. 다음 세션에 적용됩니다.</string> <string name="mode_controller_deck">Steam Deck 컨트롤러</string> @@ -268,6 +276,8 @@ <string name="mode_suspend_manual">수동</string> <string name="mode_suspend_native">네이티브</string> <string name="mode_suspend_steam_note">자동: 일시 중지되고 다시 표시되면 재개됩니다. 네이티브: 먼저 Steam을 절전에 대비시킵니다. 수동: \"재개\"를 탭할 때까지 기다립니다. 안 함: 계속 실행됩니다.</string> + <string name="mode_background_downloads">다운로드 계속 진행</string> + <string name="mode_background_downloads_hint">Steam이 다운로드 중일 때는 백그라운드 일시 중지를 미룹니다.</string> <string name="mode_suspend_note">자동: 백그라운드에서 일시 중지되고 다시 표시되면 재개됩니다. 수동: \"재개\"를 탭할 때까지 기다립니다. 안 함: 계속 실행됩니다.</string> <string name="mode_startup">시작</string> <string name="mode_steam_startup">DroidDeck을 열 때 Steam 시작</string> @@ -331,6 +341,7 @@ <string name="wincomp_found_shipped">게임에 %1$s이(가) 있습니다 (대개 그대로 작동)</string> <string name="wincomp_already_here">이 게임에 이미 있음</string> <string name="wincomp_no_recommendation">게임 폴더에서 필요한 구성 요소를 가리키는 것이 없습니다.</string> + <string name="wincomp_active">이 게임에 설치됨 (%1$d)</string> <string name="wincomp_all">모든 구성 요소 (%1$d)</string> <string name="wincomp_installers">Windows 설치 프로그램 필요 (%1$d)</string> <string name="wincomp_installers_note">이 항목들은 프리픽스 안에서 실제 설치 프로그램을 실행합니다(.NET, PhysX, XNA, 전체 Visual C++ 설치 프로그램 등). DroidDeck은 아직 지원하지 않으며 다음에 지원할 예정입니다.</string> @@ -341,6 +352,9 @@ <string name="wincomp_loading">구성 요소를 불러오는 중…</string> <string name="wincomp_failed">설치할 수 없습니다: %1$s</string> <string name="wincomp_downloaded">다운로드됨</string> + <string name="wincomp_downloading">%1$s 다운로드 중</string> + <string name="wincomp_installing">설치 중: %1$s</string> + <string name="wincomp_reverting">끄는 중…</string> <string name="wincomp_download_size">%1$s 다운로드</string> <string name="wincomp_next_launch">다음에 게임을 시작할 때 적용됩니다.</string> <string name="mode_stretch169">16:9 게임 늘리기</string> @@ -397,6 +411,44 @@ <string name="comp_check_nightlies">Nightlies의 새 패키지 확인</string> <string name="comp_proton">Proton</string> <string name="comp_proton_note">여기에서 선택한 Proton은 Steam에서 Proton을 따로 선택하지 않은 모든 게임을 실행하며, Steam에서 변경한 내용도 여기에 표시됩니다. Steam 업데이트 시 Valve의 Proton이 교체되지만 각 빌드의 원본 파일은 유지됩니다.</string> + <string name="comp_scope">구성</string> + <string name="comp_profile_configure">프로필 구성</string> + <string name="comp_profile">프로필</string> + <string name="comp_scope_general">기본 프로필</string> + <string name="comp_scope_general_detail">재정의가 없는 게임에서 자동으로 사용됩니다.</string> + <string name="comp_profile_game_detail">%1$s 실행 시 자동으로 사용되는 저장된 설정입니다.</string> + <string name="comp_game_hint">이 게임에 필요한 설정만 선택하세요. 나머지는 계속 기본 프로필을 사용합니다.</string> + <string name="comp_game_fex">FEX 프리셋</string> + <string name="comp_game_fex_inherited">기본 프로필의 프리셋 사용 중.</string> + <string name="comp_game_fex_override">이 게임에 사용자 정의 프리셋이 저장되었습니다.</string> + <string name="comp_game_fex_custom">사용자 정의 FEX 변수</string> + <string name="comp_game_use_general">기본값 사용</string> + <string name="comp_game_compatibility">게임 호환성</string> + <string name="comp_game_environment_hint">이 게임의 Proton, DXVK, VKD3D 및 동기화 환경 옵션입니다.</string> + <string name="comp_game_proton_inherited">기본 프로필의 Proton 사용 중.</string> + <string name="comp_game_proton_override">Steam 호환성 매핑을 통해 이 게임에 사용자 정의 Proton이 저장되었습니다.</string> + <string name="comp_no_proton_short">Proton 없음</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">이 게임에서 다른 패키지를 선택하지 않으면 %1$s의 기본 프로필 패키지를 사용합니다.</string> + <string name="comp_game_component_general">기본 프로필의 구성 요소 패키지를 사용합니다.</string> + <string name="comp_game_component_override">이 게임에만 %1$s을(를) 사용합니다.</string> + <string name="comp_game_runtime_options">런타임 옵션</string> + <string name="comp_game_environment_other_hint">기타 게임별 Proton, DXVK, VKD3D-Proton 및 동기화 환경 옵션입니다.</string> + <string name="comp_game_saved_next">%1$s의 프로필이 저장되었습니다. 다음 게임 실행 시 적용됩니다.</string> + <string name="game_no_steam_title">Steam 없이 시작</string> + <string name="game_no_steam_detail">바로 닫히는 게임의 경우: Proton의 Steam 설정 없이 실행합니다. 오버레이 같은 Steam 기능은 이 게임에서 꺼집니다.</string> + <string name="game_no_steam_on">켜짐</string> + <string name="comp_game_proton_live">%1$s의 Proton 선택이 저장되었으며 지금 Steam에서 사용할 수 있습니다.</string> + <string name="comp_game_proton_next">%1$s의 Proton 선택이 저장되었습니다. 게임을 실행하기 전에 Steam을 다시 시작하세요.</string> + <string name="comp_game_proton_not_runnable">%1$s은(는) 이 기기에서 선택한 Proton을 사용할 수 없습니다.</string> + <string name="comp_game_load_failed">%1$s의 프로필을 불러올 수 없습니다: %2$s</string> + <string name="comp_game_save_failed">게임 프로필을 저장할 수 없습니다: %1$s</string> + <string name="comp_game_reset">게임 프로필 초기화</string> + <string name="comp_game_reset_title">%1$s의 프로필을 초기화하시겠습니까?</string> + <string name="comp_game_reset_body">이 게임의 Proton, FEX, 구성 요소 패키지, 환경, 텍스처 필터링 및 Windows 구성 요소 재정의를 제거하시겠습니까? 이후 기본 프로필을 사용합니다.</string> + <string name="comp_game_reset_done">이제 %1$s은(는) 기본 프로필을 사용합니다.</string> + <string name="comp_game_reset_failed">게임 프로필을 완전히 초기화할 수 없습니다: %1$s</string> <string name="comp_default_live">이제 Steam에서 Proton을 따로 선택하지 않은 모든 게임을 %1$s(으)로 실행합니다.</string> <string name="comp_default_next">다음에 Steam을 시작할 때부터 Proton을 따로 선택하지 않은 모든 게임을 %1$s(으)로 실행합니다.</string> <string name="comp_default_not_runnable">%1$s(으)로는 여기에서 게임을 실행할 수 없으므로 기본값으로 설정할 수 없습니다.</string> @@ -560,6 +612,18 @@ <string name="drawer_stop_title">세션을 중지하시겠습니까?</string> <string name="drawer_running_now">현재 실행 중</string> <string name="drawer_no_game">현재 Proton으로 실행 중인 게임이 없습니다</string> + <string name="drawer_selected_profile">게임 프로필</string> + <string name="drawer_selected_profile_none">프로필 세부 정보를 아직 사용할 수 없습니다.</string> + <string name="drawer_profile_refresh_failed_initial">게임 프로필을 불러올 수 없습니다. 자동으로 다시 시도합니다.</string> + <string name="drawer_profile_refresh_failed_stale">게임 프로필을 새로 고칠 수 없습니다. 마지막으로 가져온 데이터를 표시하며 자동으로 다시 시도합니다.</string> + <string name="drawer_profile_inspect_hint">다음 실행 시 사용되는 저장된 설정을 보고 편집합니다.</string> + <string name="drawer_profile_default">기본 프로필 사용 중</string> + <string name="drawer_profile_custom">사용자 정의 프로필</string> + <string name="drawer_component_inherited">기본 프로필의 %1$s 사용 중.</string> + <string name="drawer_game_components_note">변경 사항은 %1$s의 프로필에 저장되며 다음 게임 실행 시 적용됩니다.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="other">게임 재정의 %1$d개</item> + </plurals> <string name="drawer_no_proton">Proton이 설치되지 않았습니다.</string> <string name="drawer_game_running">게임 실행 중</string> <string name="drawer_reapplied">%1$s 시작 시 다시 적용됨</string> @@ -616,6 +680,14 @@ <string name="setup_saved_logs">저장된 세션 로그</string> <string name="setup_saved_logs_hint">최신 %1$d개 유지</string> <string name="setup_clear_logs">로그 지우기</string> + <string name="setup_reset_settings">모든 설정 초기화</string> + <string name="setup_reset_settings_hint">모든 설정을 기본값으로 되돌립니다</string> + <string name="setup_reset_settings_button">초기화</string> + <string name="setup_reset_settings_title">모든 설정을 초기화할까요?</string> + <string name="setup_reset_settings_body">화면, 성능, 컨트롤러, 테마, 언어 및 런처 설정이 기본값으로 돌아갑니다. 게임, Steam 로그인, 스토어 계정, 다운로드한 드라이버 및 저장소 폴더는 유지됩니다.</string> + <string name="setup_reset_settings_profiles">게임 프로필도 초기화 (각 게임의 Proton, FEX, 구성 요소 및 환경, 기본 프로필의 환경)</string> + <string name="setup_reset_settings_session_running">설정을 초기화하기 전에 세션을 중지하세요.</string> + <string name="setup_reset_settings_partial">설정이 초기화되었지만 일부는 초기화하지 못했습니다. 자세한 내용은 로그를 확인하세요.</string> <string name="setup_offline">오프라인 모드</string> <string name="setup_sign_in_first">먼저 Steam에 로그인하세요</string> <string name="setup_launcher">런처</string> @@ -631,6 +703,10 @@ <string name="setup_home">홈 화면으로 사용</string> <string name="setup_home_on">DroidDeck을 홈 앱으로 사용할 수 있습니다</string> <string name="setup_home_off">DroidDeck은 홈 앱으로 제공되지 않습니다</string> + <string name="setup_orientation">화면 방향</string> + <string name="setup_orientation_auto">자동 가로 모드</string> + <string name="setup_orientation_landscape">가로 모드</string> + <string name="setup_orientation_reverse">역방향 가로 모드</string> <string name="setup_fullscreen">전체 화면</string> <string name="setup_fullscreen_on">Android 상태 표시줄 및 탐색 표시줄 숨기기</string> <string name="setup_fullscreen_off">Android 상태 표시줄 및 탐색 표시줄 표시</string> @@ -888,6 +964,12 @@ <string name="content_page_gone">해당 페이지가 더 이상 존재하지 않습니다. 왼쪽에서 항목을 선택하세요.</string> <string name="content_open_desktop">데스크톱 열기</string> <string name="content_install_open_desktop">설치 및 데스크톱 열기</string> + <string name="desktop_remove_title">데스크톱 제거</string> + <string name="desktop_remove_text">데스크톱과 번들 앱을 제거합니다. 게임, 추가한 앱, 설정 및 공유 라이브러리는 유지됩니다.</string> + <string name="desktop_removing">데스크톱 제거 중</string> + <string name="desktop_removed">데스크톱 제거됨</string> + <string name="desktop_cleaning_legacy">이전 LXQt 파일 제거 중</string> + <string name="desktop_removal_unknown">이 데스크톱 버전을 제거하기 전에 DroidDeck을 업데이트하세요.</string> <string name="content_linux_desktop">Linux 데스크톱</string> <string name="content_linux_desktop_hint">KDE Plasma, Firefox 및 에뮬레이터</string> <string name="games_empty">Steam에서 설치한 게임은 세션 종료 후 여기에 표시됩니다.</string> @@ -917,6 +999,8 @@ <string name="games_last_played">마지막 실행 %1$s</string> <string name="games_internal">내부 저장 공간</string> <string name="games_added">추가된 게임</string> + <string name="games_custom">사용자 정의 게임</string> + <string name="games_store_storage">게임 저장 공간</string> <string name="gpu_auto_hint">이 GPU에 가장 적합한 드라이버 조합을 선택하고 최신 상태로 유지합니다</string> <string name="gpu_manual_hint">드라이버 조합을 직접 선택</string> <string name="gpu_checking">드라이버 확인 중…</string> @@ -958,6 +1042,12 @@ <string name="deck_tab_left">왼쪽 터치패드</string> <string name="deck_tab_right">오른쪽 터치패드</string> <string name="deck_tab_both">양쪽</string> + <string name="deck_tab_gamepad">게임패드</string> + <string name="deck_surface_stick">스틱</string> + <string name="deck_surface_trackpad">터치패드</string> + <string name="deck_surface_switch">스틱과 터치패드 간 전환</string> + <string name="deck_more_controls">Deck 컨트롤 더 보기</string> + <string name="deck_panels">패널</string> <string name="deck_close">보조 화면 컨트롤 닫기</string> <string name="deck_open_steam_menu">Steam 메뉴 열기</string> <string name="deck_left_upper">왼쪽 · 위</string> @@ -993,6 +1083,10 @@ <string name="proton_installed">%1$s 설치됨</string> <string name="proton_queued">이전 요청으로 인한 대기 항목</string> <string name="proton_not_installed">설치되지 않음</string> + <string name="proton_update_available">업데이트 있음 → %1$s</string> + <string name="proton_remove_older">이전 %1$s 버전 제거</string> + <string name="proton_remove_older_body">%1$s을(를) 제거합니다. 이 버전으로 설정된 게임은 다른 Proton으로 전환해야 합니다.</string> + <string name="proton_remove_body">이 버전으로 설정된 게임은 다른 Proton으로 전환해야 합니다.</string> <string name="proton_install_now">지금 설치</string> <string name="proton_cancel_queued">대기 취소</string> <string name="pckb_android">Android 키보드</string> @@ -1021,6 +1115,8 @@ <string name="game_env_eyebrow">Proton 게임</string> <string name="pip_title">PIP (픽처 인 픽처)</string> <string name="pip_auto">자동으로 PIP 시작</string> + <string name="unfolded_controls">펼친 화면의 컨트롤</string> + <string name="unfolded_controls_hint">펼치면 게임이 Steam Deck 터치패드와 뒷면 그립 위에 표시됩니다. 접으면 게임이 전체 화면으로 표시됩니다.</string> <string name="pip_suspended">일시 중지됨</string> <string name="pip_not_ready">세션이 준비되면 PIP를 사용할 수 있습니다.</string> <string name="pip_permission">Android 설정에서 DroidDeck의 PIP 사용을 허용하세요.</string> @@ -1046,19 +1142,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> @@ -1066,19 +1155,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> @@ -1091,78 +1172,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> @@ -1170,19 +1228,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> @@ -1197,11 +1249,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> @@ -1214,39 +1262,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> @@ -1261,33 +1295,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> @@ -1300,28 +1321,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> @@ -1330,8 +1337,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> @@ -1349,27 +1354,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> @@ -1387,8 +1382,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> @@ -1396,8 +1389,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> @@ -1411,19 +1402,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> @@ -1431,8 +1414,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> @@ -1443,20 +1424,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> @@ -1470,8 +1445,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> @@ -1486,51 +1459,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> @@ -1539,8 +1492,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> @@ -1551,34 +1502,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> @@ -1593,75 +1531,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> @@ -1697,27 +1607,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> @@ -1725,24 +1621,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> @@ -1756,31 +1642,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> @@ -1788,19 +1665,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> @@ -1817,55 +1686,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> @@ -1890,29 +1742,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> @@ -1923,115 +1769,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> @@ -2049,6 +1841,7 @@ <string name="display_frame_rate">프레임 속도</string> <string name="display_look_filter">%1$s 스케일링을 %2$s(으)로 설정합니다.</string> <string name="display_match_screen">화면에 맞춤(%1$d×%2$d)</string> + <string name="display_follow_screen">화면 따라가기(접거나 펼치면 크기 변경)</string> <string name="mode_tab_steam">Steam</string> <string name="device_info_show">기기 세부 정보 표시</string> <string name="device_info_hide">기기 세부 정보 숨기기</string> @@ -2058,6 +1851,186 @@ <string name="device_info_copied">복사했습니다.</string> <string name="force_ssbs_title">SSBS 강제</string> <string name="force_ssbs_hint">Wine이 계속 끄는 CPU 속도 설정을 켜 둡니다. Snapdragon 8 Elite에서 Windows 게임이 빨라질 수 있으며, 이전 칩에서는 변화가 거의 없습니다. 다음 게임 실행 시 적용됩니다.</string> + + <string name="rail_stores">스토어</string> + <string name="source_steam">Steam</string> + <string name="source_added">사용자 정의</string> + <string name="setup_stores">스토어</string> + <string name="setup_stores_open_on">스토어를 열 때 표시</string> + <string name="setup_stores_speed">다운로드 속도 단계</string> + <string name="setup_stores_engine">스토어 엔진</string> + <string name="setup_stores_engine_chip_ready">● 엔진 준비됨</string> + <string name="setup_stores_engine_chip_missing">빌드되지 않음</string> + <string name="stores_tier_balanced">균형</string> + <string name="stores_tier_fast">빠름</string> + <string name="stores_tier_max">최대</string> + <string name="stores_title">스토어</string> + <string name="stores_settings">스토어 설정</string> + <string name="stores_settings_eyebrow">설정</string> + <string name="stores_chip_signed_out">(로그인 안 됨)</string> + <string name="stores_tab_store">스토어</string> + <string name="stores_tab_installed">설치됨</string> + <string name="stores_tab_library">라이브러리</string> + <string name="stores_tab_all">전체</string> + <string name="stores_tab_downloads">다운로드</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">로그인</string> + <string name="stores_signout_named">%1$s · 로그아웃</string> + <string name="stores_signout_title">%1$s에서 로그아웃하시겠습니까?</string> + <string name="stores_signout_note">설치된 게임은 유지됩니다. 다시 로그인하면 라이브러리를 다시 가져옵니다.</string> + <string name="stores_signout_stay">로그인 유지</string> + <string name="stores_signout_go">로그아웃</string> + <string name="stores_search_catalog">%1$s 카탈로그 검색…</string> + <string name="stores_search_installed">설치된 게임 검색…</string> + <string name="stores_search_library">내 라이브러리 검색…</string> + <string name="stores_search_open">검색</string> + <string name="stores_installed_empty">설치된 항목 없음</string> + <string name="stores_library_loading">불러오는 중</string> + <string name="stores_nothing_matches">게임 없음</string> + <string name="stores_shelves_loading">스토어 불러오는 중…</string> + <string name="stores_amazon_no_catalog">카탈로그 없음</string> + <string name="stores_shelf_new">새로 나온 게임</string> + <string name="stores_shelf_deals">할인</string> + <string name="stores_shelf_free">무료 게임</string> + <string name="stores_shelf_trending">인기 급상승</string> + <string name="stores_shelf_library">내 라이브러리</string> + <string name="stores_play">플레이</string> + <string name="stores_install">설치</string> + <string name="stores_resume_install">설치 재개</string> + <string name="stores_dl_interrupted">중단됨</string> + <string name="stores_install_size">설치 · %1$s</string> + <string name="stores_get_free">무료로 받기</string> + <string name="stores_view_on">%1$s에서 보기</string> + <string name="stores_buy_price">구매 · %1$s</string> + <string name="stores_installed_chip">설치됨</string> + <string name="stores_in_library">라이브러리에 있음</string> + <string name="stores_free">무료</string> + <string name="stores_resume">재개</string> + <string name="stores_refresh">새로 고침</string> + <string name="stores_show_mature">성인용 콘텐츠 표시</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">오프라인</string> + <string name="epic_card_none">끔</string> + <string name="epic_eos">Epic 로그인(EOS)</string> + <string name="epic_offline">오프라인으로 실행</string> + <string name="epic_resolve">Epic 로그인 문제 해결</string> + <string name="store_cloud_saves">클라우드 저장</string> + <string name="cloud_upload">클라우드에 업로드</string> + <string name="cloud_download">클라우드에서 다운로드</string> + <string name="cloud_none">클라우드 저장 없음</string> + <string name="cloud_conflict">클라우드 충돌</string> + <string name="cloud_conflict_files">클라우드 충돌 · 파일 %1$d개</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">클라우드 유지</string> + <string name="cloud_keep_local">로컬 유지</string> + <string name="cloud_checking">클라우드 확인 중…</string> + <string name="cloud_last_sync">마지막 동기화 %1$s</string> + <string name="cloud_never">아직 동기화 안 됨</string> + <string name="cloud_done">파일 %1$d개</string> + <string name="stores_eyebrow_installed">설치됨</string> + <string name="stores_eyebrow_library">내 라이브러리</string> + <string name="stores_eyebrow_store">스토어</string> + <string name="achievements_title">도전 과제</string> + <string name="achievements_count">%2$d개 중 %1$d개 달성</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">달성 · %1$d</string> + <string name="achievements_group_locked">미달성 · %1$d</string> + <string name="achievements_group_hidden">숨겨짐 · %1$d</string> + <string name="achievements_hidden">숨겨진 도전 과제</string> + <string name="achievements_hidden_detail">달성하면 세부 정보가 표시됩니다.</string> + <string name="achievements_all">모든 도전 과제 달성</string> + <string name="achievements_reading">불러오는 중…</string> + <string name="achievements_signed_out">도전 과제를 보려면 %1$s에 로그인하세요.</string> + <string name="achievements_unreachable">%1$s에 연결할 수 없습니다. 온라인 상태에서 다시 확인하세요.</string> + <string name="achievements_none">%1$s에 이 게임의 도전 과제가 없습니다.</string> + <string name="achievements_show_all">모두 보기(%1$d)</string> + <string name="achievements_dialog_title">도전 과제 · %1$s</string> + <string name="stores_in_steam">✓ Steam에 있음</string> + <string name="stores_uninstall">제거</string> + <string name="stores_uninstalling">제거 중…</string> + <string name="stores_uninstall_title">%1$s을(를) 제거하시겠습니까?</string> + <string name="stores_uninstall_body_size">%1$s의 공간을 확보하고 Steam에서 제거합니다.</string> + <string name="stores_uninstall_body">게임 파일과 Steam 바로 가기를 제거합니다.</string> + <string name="stores_launch_not_registered">다음에 Steam을 시작하면 게임이 표시됩니다. Steam에서 실행하거나 지금 Steam을 시작하세요.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">게임별 Steam 선택</string> + <string name="stores_card_audio">오디오</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">구성 요소</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">엔진</string> + <string name="stores_card_engine_value">%1$s · 네이티브</string> + <string name="stores_card_engine_java">%1$s · 내장</string> + <string name="stores_how_it_runs">실행 방식</string> + <string name="stores_kv_fetched">가져온 곳</string> + <string name="stores_kv_fetched_value">%1$s, 내 계정으로</string> + <string name="stores_kv_installed_to">설치 위치</string> + <string name="stores_kv_installed_to_value">게임 저장 공간(Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">실행 도구</string> + <string name="stores_kv_launched_by_value">Steam 및 Proton, Steam 게임과 같은 방식</string> + <string name="stores_kv_launched_by_epic">Steam 및 Proton · 실행할 때마다 Epic 로그인, EOS 오버레이 끔</string> + <string name="stores_kv_launched_by_amazon">Steam 및 Proton · Amazon Games Services 변수 설정됨</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">설치 시 Steam에 추가됨(Deck UI + Steam Input)</string> + <string name="stores_downloads_empty">다운로드 없음</string> + <string name="stores_dl_paused">일시 중지됨</string> + <string name="stores_dl_queued">대기 중</string> + <string name="stores_dl_queued_at">대기 중 · #%1$d</string> + <string name="stores_dl_cancelled">취소됨</string> + <string name="stores_dl_failed">실패</string> + <string name="stores_dl_failed_reason">실패 · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">일시 중지</string> + <string name="stores_dl_resume">재개</string> + <string name="stores_dl_cancel">취소</string> + <string name="stores_dl_retry">다시 시도</string> + <string name="stores_dl_clear">지우기</string> + <string name="stores_dl_clear_all">완료된 항목 지우기</string> + <string name="stores_stage_manifest">매니페스트</string> + <string name="stores_stage_download">다운로드</string> + <string name="stores_stage_verify">검증</string> + <string name="stores_stage_install">설치</string> + <string name="stores_stage_done">완료</string> + <string name="stores_stage_checking">확인 중</string> + <string name="stores_stage_downloading">다운로드 중</string> + <string name="stores_dl_cancel_confirm">다운로드를 삭제하시겠습니까?</string> + <string name="stores_dl_failed_kept">실패 · %1$s 유지됨</string> + <string name="stores_stage_verifying">검증 중</string> + <string name="stores_stage_installing">설치 중</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">파일 %1$d/%2$d개</string> + <string name="stores_download_manager">다운로드 관리자</string> + <string name="stores_parallel">동시 다운로드 수</string> + <string name="stores_install_where">설치 위치</string> + <string name="stores_install_title">%1$s 설치:</string> + <string name="stores_target_internal">내부 저장 공간</string> + <string name="stores_target_free">%1$s 사용 가능</string> + <string name="stores_notification_channel">스토어 다운로드</string> + <string name="stores_notification_channel_hint">게임 다운로드 진행률을 표시하고 백그라운드에서 다운로드를 계속 진행합니다</string> + <string name="stores_notification_one">다운로드 중</string> + <string name="stores_notification_many">다운로드 %1$d개</string> + <string name="stores_notification_waiting">준비 중…</string> + <string name="stores_notification_done_channel">완료된 스토어 다운로드</string> + <string name="stores_notification_installed">%1$s 설치됨</string> + <string name="stores_notification_failed">%1$s 다운로드 실패</string> + <string name="stores_notification_delete">삭제</string> + <string name="stores_notification_keep">유지</string> + <string name="stores_login_title">%1$s에 로그인</string> + <string name="stores_login_provider_title">계정 제공업체로 로그인</string> + <string name="stores_login_reload">새로 고침</string> + <string name="stores_login_close">닫기</string> + <string name="stores_login_retry">다시 시도</string> + <string name="stores_login_finishing">로그인 완료 중…</string> + <string name="stores_login_error_network">로그인 페이지를 불러올 수 없습니다: %1$s</string> + <string name="stores_login_error_idp_blocked">이 제공업체는 앱에 포함된 페이지에서 로그인을 허용하지 않습니다. 대신 스토어 비밀번호로 로그인하세요.</string> + <string name="stores_login_error_http">로그인 페이지에서 오류를 반환했습니다(HTTP %1$d).</string> + <string name="stores_login_error_ssl">로그인 페이지 연결이 안전하지 않습니다.</string> + <string name="stores_login_error_verification">로그인을 확인할 수 없습니다. 다시 시도하세요.</string> + <string name="stores_login_error_generic">로그인에 실패했습니다. 다시 시도하세요.</string> <string name="common_close">닫기</string> <string name="common_back">뒤로</string> <string name="common_edit">편집</string> @@ -2089,21 +2062,122 @@ <plurals name="games_already_n"> <item quantity="other">%1$d개는 이미 게임에 있음</item> </plurals> - <string name="achievements_title">도전 과제</string> - <string name="achievements_count">%2$d개 중 %1$d개 달성</string> - <string name="achievements_count_short">%1$d / %2$d</string> - <string name="achievements_group_unlocked">달성 · %1$d</string> - <string name="achievements_group_locked">미달성 · %1$d</string> - <string name="achievements_group_hidden">숨겨짐 · %1$d</string> - <string name="achievements_hidden">숨겨진 도전 과제</string> - <string name="achievements_hidden_detail">달성하면 세부 정보가 표시됩니다.</string> - <string name="achievements_all">모든 도전 과제 달성</string> - <string name="achievements_reading">불러오는 중…</string> - <string name="achievements_signed_out">도전 과제를 보려면 %1$s에 로그인하세요.</string> - <string name="achievements_unreachable">%1$s에 연결할 수 없습니다. 온라인 상태에서 다시 확인하세요.</string> - <string name="achievements_none">%1$s에 이 게임의 도전 과제가 없습니다.</string> - <string name="achievements_show_all">모두 보기(%1$d)</string> - <string name="achievements_dialog_title">도전 과제 · %1$s</string> + <string name="hop_back">B · %1$s(으)로 돌아가기</string> + <string name="hop_from_games">게임</string> + <string name="hop_from_play">플레이 설정</string> + <string name="sheet_graphics_drivers">그래픽 드라이버</string> + <string name="comp_this_proton">이 Proton</string> + <string name="comp_every_proton">모든 Proton</string> + <string name="comp_for_every_proton">모든 Proton에 적용</string> + <string name="comp_available_count">사용 가능 · %1$d</string> + <string name="comp_checked">확인 시각 %1$s</string> + <string name="comp_check_all">새 구성 요소 및 드라이버 확인</string> + <string name="comp_get">받기</string> + <string name="comp_proton_bar">%1$s · 아래의 DXVK, VKD3D-Proton 및 FEX는 이 Proton용입니다</string> + <string name="comp_device_tested">%1$s · 테스트됨</string> + <string name="comp_device_untested">%1$s · 테스트되지 않음</string> + <string name="comp_device_unsupported">%1$s · 지원되지 않음</string> + <string name="gpu_auto_keeps">권장 조합을 최신 상태로 유지합니다</string> + <string name="gpu_bar_auto">자동 · %1$s</string> + <string name="gpu_separately">런타임 및 디스플레이 드라이버 개별 설정</string> + <string name="gpu_use_title">%1$s을(를) 사용하시겠습니까?</string> + <string name="proton_remove_title">%1$s을(를) 제거하시겠습니까?</string> + <string name="setup_tab_diagnostics">진단</string> + <string name="setup_ready">플레이 준비 완료</string> + <string name="setup_not_ready">아직 플레이할 준비가 되지 않음</string> + <string name="setup_ready_runtime">런타임 %1$s</string> + <string name="setup_ready_limit_off">프로세스 제한 꺼짐</string> + <string name="setup_ready_signed_in">%1$s(으)로 로그인됨</string> + <string name="setup_needs_you">확인이 필요한 항목</string> + <string name="setup_group_look">외관</string> + <string name="setup_group_device">이 기기</string> + <string name="setup_group_art">라이브러리 아트워크</string> + <string name="flathub_eyebrow">LINUX 앱</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Linux 데스크톱용 앱으로, 데스크톱에서 엽니다.</string> + <string name="flathub_setup">Flathub 설정</string> + <string name="flathub_remove_title">%1$s을(를) 제거하시겠습니까?</string> + <string name="flathub_remove_note">설정과 파일은 유지됩니다.</string> + <string name="flathub_keep">유지</string> + <string name="desktop_size">디스크 사용량</string> + <string name="desktop_uninstall_title">데스크톱을 제거하시겠습니까?</string> + <string name="desktop_all_systems">전체</string> + <string name="gog_achievements">GOG 도전 과제</string> + <string name="gog_achievements_hint">지원되는 GOG 게임의 도전 과제를 동기화합니다. 다음 실행부터 적용됩니다.</string> + <string name="gog_achievements_off">도전 과제 꺼짐</string> + <string name="gog_achievements_on">도전 과제 켜짐</string> + <string name="gog_achievements_signin">GOG에 다시 로그인하세요</string> + <string name="gog_achievements_uploaded">도전 과제 업로드됨</string> + <string name="gog_achievements_pending">도전 과제 업로드 대기 중</string> + <string name="gog_achievements_enable_store">스토어 설정에서 GOG 도전 과제를 활성화하세요</string> + <string name="gog_achievements_unavailable">도전 과제를 사용할 수 없습니다. 세션 로그를 확인하세요</string> + <string name="achievement_unlocked">도전 과제 잠금 해제됨</string> + <string name="touch_paste">붙여넣기</string> + <string name="touch_dpad">방향패드</string> + <string name="touch_left_stick">왼쪽 스틱</string> + <string name="touch_right_stick">오른쪽 스틱</string> + <string name="touch_menu">메뉴</string> + <string name="touch_keyboard">키보드</string> + <string name="touch_dpad_up">방향패드 위</string> + <string name="touch_dpad_down">방향패드 아래</string> + <string name="touch_dpad_left">방향패드 왼쪽</string> + <string name="touch_dpad_right">방향패드 오른쪽</string> + <string name="touch_title">터치 컨트롤</string> + <string name="touch_controller">터치 컨트롤러</string> + <string name="touch_mouse">마우스</string> + <string name="touch_on">켜짐</string> + <string name="touch_off">꺼짐</string> + <string name="touch_speed_slow">느림</string> + <string name="touch_speed_normal">보통</string> + <string name="touch_speed_fast">빠름</string> + <string name="touch_edit_layout">레이아웃 편집</string> + <string name="touch_droiddeck_menu">DroidDeck 메뉴</string> + <string name="touch_drag_hint">화면으로 컨트롤을 끌어다 놓으세요</string> + <string name="touch_arrange">배치</string> + <string name="touch_discard">취소</string> + <string name="touch_saving">저장 중…</string> + <string name="touch_icon">아이콘</string> + <string name="touch_narrower">너−</string> + <string name="touch_wider">너+</string> + <string name="touch_shorter">높−</string> + <string name="touch_taller">높+</string> + <string name="touch_hide">숨기기</string> + <string name="touch_color_opacity">색상 및 불투명도…</string> + <string name="touch_copy_layout">이 레이아웃 복사</string> + <string name="touch_layout_copied">레이아웃이 복사되었습니다. 어떤 게임의 터치 레이아웃에도 붙여넣을 수 있습니다</string> + <string name="touch_paste_layout">복사한 레이아웃 붙여넣기</string> + <string name="touch_reset_layout">Steam 기본값으로 재설정</string> + <string name="touch_no_binding">이 컨트롤에는 아이콘을 지정할 Steam 바인딩이 없습니다</string> + <string name="touch_icon_for">%1$s 아이콘</string> + <string name="touch_layout_color">터치 레이아웃 색상</string> + <string name="touch_done">완료</string> + <string name="touch_save_failed">터치 레이아웃을 저장할 수 없습니다</string> + <string name="touch_saved">터치 레이아웃이 Steam에 저장되었습니다</string> + <string name="touch_app_id">앱 %1$d</string> + <string name="touch_label_hint">라벨(선택 사항)</string> + <string name="touch_icons_missing">Steam 아이콘을 찾을 수 없습니다</string> + <plurals name="touch_icons_found"> + <item quantity="other">Steam 아이콘 %1$d개</item> + </plurals> + <string name="touch_icon_color">아이콘 색상</string> + <string name="touch_button_color">버튼 색상</string> + <string name="touch_no_icon">아이콘 없음</string> + + <string name="stores_sync_sign_in">로그인 확인 중…</string> + <string name="stores_sync_library">라이브러리를 가져오는 중…</string> + <string name="stores_sync_game_list">게임 목록을 가져오는 중…</string> + <string name="stores_sync_details">게임 정보를 불러오는 중… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="other">게임 %1$d개 동기화 중…</item> + </plurals> + <string name="stores_sync_progress">동기화 중… %1$d/%2$d</string> + <string name="stores_sync_cover_art">커버 아트를 가져오는 중…</string> + <string name="stores_sync_failed">동기화 실패</string> + <string name="stores_sync_no_games">%1$s 라이브러리에서 게임을 찾을 수 없습니다</string> + <string name="stores_sync_amazon_no_answer">Amazon Games가 라이브러리 요청에 응답하지 않았습니다. 새로고침하거나 다시 로그인하세요</string> + <string name="stores_sync_gog_unreachable">GOG에 연결할 수 없습니다</string> + <string name="stores_sync_gog_unreadable">GOG 라이브러리를 읽을 수 없습니다</string> + <string name="stores_registering">Steam에 등록 중…</string> <string name="drawer_page_glasses">안경</string> <string name="glasses_open">VITURE 안경</string> <string name="glasses_control">안경 제어</string> @@ -2152,4 +2226,34 @@ <string name="stores_hltb_completionist">완전 정복</string> <string name="stores_hltb_hours">%1$s시간</string> <string name="stores_hltb_minutes">%1$d분</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Cardboard 모드</string> + <string name="cardboard_mode_hint">휴대폰 VR 뷰어용 왼쪽 및 오른쪽 눈 화면입니다.</string> + <string name="cardboard_source">화면</string> + <string name="cardboard_source_screen">가상 스크린</string> + <string name="cardboard_source_sbs">사이드 바이 사이드 3D</string> + <string name="cardboard_viewer">뷰어</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">렌즈 없음</string> + <string name="cardboard_ipd">눈 사이 거리</string> + <string name="cardboard_screen_width">스크린 너비</string> + <string name="cardboard_screen_distance">스크린 거리</string> + <string name="cardboard_head_tracking">머리 추적</string> + <string name="cardboard_no_sensor">이 기기에는 회전 센서가 없습니다.</string> + <string name="cardboard_recenter">시점 재설정</string> + <string name="cardboard_recenter_button">재설정</string> + <string name="cardboard_steamvr">SteamVR (실험적)</string> + <string name="cardboard_steamvr_install">설치</string> + <string name="cardboard_steamvr_install_hint">Valve의 ARM64용 SteamVR을 다운로드합니다(571MB).</string> + <string name="cardboard_steamvr_installing">SteamVR 다운로드 및 압축 해제 중…</string> + <string name="cardboard_steamvr_start">시작</string> + <string name="cardboard_steamvr_ready_hint">머리를 헤드셋으로 삼아 SteamVR을 실행하며, 화면이 사이드 바이 사이드 3D로 전환됩니다.</string> + <string name="cardboard_steamvr_stop">중지</string> + <string name="cardboard_steamvr_running_hint">SteamVR이 실행 중입니다.</string> + <string name="cardboard_steamvr_unavailable">이 세션에서는 사용할 수 없습니다.</string> + <string name="cardboard_steamvr_failed">SteamVR이 응답하지 않았습니다. 세션 로그를 확인하세요.</string> + <string name="drawer_component_downloads">구성 요소 다운로드</string> + <string name="drawer_component_downloads_hint">다운로드는 구성 요소 페이지와 공유됩니다. 위에서 다운로드한 버전을 선택하면 다음 게임 실행 시 사용됩니다.</string> + <string name="drawer_component_choose_download">다운로드할 버전 선택</string> </resources> diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml new file mode 100644 index 000000000..d197aae8a --- /dev/null +++ b/app/src/main/res/values-ms/strings.xml @@ -0,0 +1,2210 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>Larian pertama memuat turun Steam. Ini mungkin mengambil beberapa minit.</item> + <item>Secara lalai, meninggalkan apl ini menjedakan Steam; ia menyambung semula dari tempat ia berhenti apabila anda kembali.</item> + <item>Pengawal anda akan muncul sebagai pad Xbox 360. Jika tidak, gunakan kawalan pada skrin.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>Larian pertama menyediakan desktop. Ini mungkin mengambil beberapa minit.</item> + <item>Secara lalai, meninggalkan apl ini menjedakan sesi; ia menyambung semula dari tempat ia berhenti apabila anda kembali.</item> + <item>Sentuhan berfungsi sebagai pad sentuh: seret untuk menggerakkan penunjuk, ketik untuk klik.</item> + <item>Pengawal anda akan muncul sebagai pad Xbox 360. Jika tidak, gunakan kawalan pada skrin.</item> + </string-array> + <string name="session_channel">Sesi</string> + <string name="session_channel_description">Dipaparkan semasa sesi Steam atau desktop sedang dibuka</string> + <string name="session_notification">Steam sedang berjalan</string> + <string name="session_notification_downloads">Muat turun Steam aktif</string> + <string name="session_notification_paused">Sesi dijeda - ketik Sambung apabila sedia</string> + <string name="resume_session">Sambung</string> + <string name="suspend_now">Gantung sekarang</string> + <string name="stop_session">Hentikan sesi</string> + <string name="frame_gen_title">Penjanaan bingkai</string> + <string name="frame_gen_hint">Pilih mod penjanaan bingkai</string> + <string name="frame_gen_off">Mati</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG Adaptif %1$d</string> + <string name="lsfg_needs_steam">Memerlukan Lossless Scaling pada Steam</string> + <string name="lsfg_needs_install">Pasang Lossless Scaling dalam Steam atau import Lossless.dll</string> + <string name="lsfg_import">Import Lossless.dll…</string> + <string name="lsfg_using_steam">Menggunakan salinan daripada Steam</string> + <string name="lsfg_using_import">Menggunakan salinan yang diimport</string> + <string name="lsfg_pick_title">Pilih Lossless.dll</string> + <string name="lsfg_imported">Lossless.dll diimport</string> + <string name="lsfg_import_unchanged">Lossless.dll ini sudah digunakan</string> + <string name="lsfg_import_not_owned">Lossless Scaling tidak dimiliki pada akaun Steam ini</string> + <string name="lsfg_import_invalid">Fail ini bukan Lossless.dll milik Lossless Scaling</string> + <string name="lsfg_import_failed">Tidak dapat menyediakan LSFG daripada Lossless.dll ini</string> + <string name="lsfg_problem_missing">LSFG memerlukan Lossless Scaling daripada Steam atau Lossless.dll yang diimport</string> + <string name="lsfg_problem_failed">LSFG tidak dapat menyediakan shadernya daripada Lossless.dll</string> + <string name="hud_fg_unsupported">%1$s: tidak disokong</string> + <string name="hud_fg_failed">%1$s: enjin gagal</string> + <string name="hud_fg_needs_lossless">%1$s: memerlukan Lossless Scaling</string> + <string name="hud_fg_starting">%1$s %2$s · bermula</string> + <string name="second_library_import_hint">Menambah pustaka Steam dan mengimbasnya untuk permainan Windows. Permainan yang dimiliki menggunakan entri Steam masing-masing; selebihnya menjadi pintasan bukan Steam.</string> + <string name="game_env_title">Persekitaran permainan</string> + <string name="game_env_hint">Pembolehubah persekitaran untuk permainan Proton.</string> + <string name="game_env_edit">Edit</string> + <string name="game_env_shared">Semua permainan Proton</string> + <string name="game_env_scope">Profil</string> + <string name="game_env_app_id">ID apl Steam</string> + <string name="game_env_open">Buka</string> + <string name="game_env_profile">Permainan %1$s</string> + <string name="game_env_add">Tambah pembolehubah</string> + <string name="game_env_name">Nama pembolehubah</string> + <string name="game_env_value">Nilai</string> + <string name="game_env_save">Simpan</string> + <string name="game_env_remove">Alih keluar</string> + <string name="game_env_restore">Warisi semula</string> + <string name="game_env_reset">Kosongkan penggantian persekitaran</string> + <string name="game_env_unset">Nyahtetapkan untuk profil ini</string> + <string name="game_env_empty">Tiada pembolehubah ditetapkan.</string> + <string name="game_env_inherited">Diwarisi</string> + <string name="game_env_custom">Pembolehubah tersuai</string> + <string name="game_env_error">Tidak dapat memuatkan atau menyimpan. Semak storan kosong dan cuba lagi.</string> + <string name="game_env_invalid">Gunakan nama yang sah dan nilai sehingga 8192 aksara.</string> + <string name="game_env_unused">Runtime Proton standard tidak membaca pembolehubah ini.</string> + <string name="game_env_loading">Memuatkan…</string> + <string name="game_env_feature">Penggantian tahap ciri D3D12. 12_2 melaporkan keupayaan DirectX 12 Ultimate; ciri yang tidak disokong boleh gagal. Biarkan tidak ditetapkan untuk pengesanan automatik.</string> + <string name="game_env_shader">Penggantian model shader D3D12. 6_9 memerlukan VKD3D yang cukup terkini dan ciri GPU yang sepadan. Biarkan tidak ditetapkan untuk pengesanan automatik.</string> + <string name="game_env_cache">Kekalkan caching shader Mesa dihidupkan untuk mengurangkan penyusunan berulang. false menghidupkan caching.</string> + <string name="game_env_cache_size">Saiz maksimum cache shader Mesa, seperti 1G. Cache yang lebih besar menggunakan lebih banyak storan.</string> + <string name="game_env_vkd3d_config">Pilihan VKD3D untuk D3D12. nodxr melumpuhkan ray tracing; gunakan hanya untuk permainan yang memerlukannya.</string> + <string name="game_env_proton_log">Tetapkan kepada 1 untuk log diagnostik Proton. Pengelogan boleh mengurangkan prestasi dan menggunakan storan.</string> + <string name="game_env_wined3d">Tetapkan kepada 1 untuk menggunakan WineD3D berasaskan OpenGL dan bukan DXVK, untuk pengujian keserasian D3D9–11.</string> + <string name="game_env_no_ntsync">Tetapkan kepada 1 untuk mengekalkan permainan ini di luar ntsync (droiddeck-ntsync atau pemacu kernel); ia menggunakan droiddeck-esync jika pek wujud, jika tidak droiddeck-fsync.</string> + <string name="game_env_no_esync">Tetapkan kepada 1 untuk mematikan droiddeck-esync bagi permainan ini; ia menggunakan droiddeck-fsync sebagai ganti, atau droiddeck-ntsync jika fsync juga dimatikan.</string> + <string name="game_env_no_fsync">Tetapkan kepada 1 untuk mengekalkan permainan ini di luar droiddeck-fsync; ia menggunakan droiddeck-esync jika pek wujud, jika tidak droiddeck-ntsync.</string> + <string name="game_env_fsync_first">Tetapkan kepada 1 untuk menjalankan permainan ini pada droiddeck-fsync, atau 0 untuk meletakkan droiddeck-esync dahulu jika pek sepadan dengan Protonnya.</string> + <string name="game_env_xalia">Tetapkan kepada 0 untuk melumpuhkan pembantu navigasi pad permainan Proton bagi permainan ini.</string> + <string name="game_env_hud">Overlay DXVK untuk D3D9–11. fps memaparkan kadar bingkai; nyahtetapkan untuk menyembunyikannya.</string> + <string name="game_env_dxvk_config">Tetapan DXVK diasingkan dengan tanda koma bertitik. Binaan terkini menggunakan dxvk.maxFrameRate untuk pengehadan bingkai; lebih baik gunakan pengehad bingkai sesi untuk permainan biasa.</string> + <string name="game_env_vkd3d_limit">Had kadar bingkai D3D12. 0 melumpuhkan had.</string> + <string name="game_env_glthread">Threading OpenGL Mesa. true boleh membantu permainan OpenGL yang terhad oleh CPU; uji bagi setiap permainan.</string> + <string name="sync_backend_title">Segerak Wine</string> + <string name="sync_backend_hint">Cara permainan menyelaraskan bebenangnya. Berkuat kuasa pada sesi seterusnya.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">Praset FEX</string> + <string name="fex_next_launch">Berkuat kuasa pada pelancaran permainan seterusnya.</string> + <string name="game_settings_title">Permainan</string> + <string name="fex_default_label">Lalai FEX</string> + <string name="fex_default_detail">Menggunakan lalai FEX.</string> + <string name="fex_stability_label">Kestabilan</string> + <string name="fex_stability_detail">Tetapan paling selamat dan paling perlahan.</string> + <string name="fex_compatibility_label">Keserasian</string> + <string name="fex_compatibility_detail">TSO penuh dengan multiblock.</string> + <string name="fex_intermediate_label">Pertengahan</string> + <string name="fex_intermediate_detail">TSO seimbang dengan ketepatan x87 dikurangkan.</string> + <string name="fex_performance_label">Prestasi</string> + <string name="fex_performance_detail">Semua TSO dilumpuhkan; multiblock.</string> + <string name="fex_performance_tso_label">Prestasi + TSO</string> + <string name="fex_performance_tso_detail">Tetapan Prestasi dengan TSO utama dihidupkan.</string> + <string name="fex_extreme_label">Ekstrem</string> + <string name="fex_extreme_detail">Melumpuhkan pemeriksaan kod mengubah-diri-sendiri. Permainan JIT dan DRM mungkin rosak.</string> + <string name="fex_extreme_tso_label">Ekstrem + TSO</string> + <string name="fex_extreme_tso_detail">Tetapan Ekstrem dengan TSO utama dihidupkan.</string> + <string name="fex_extreme_gn_label">Ekstrem-gn</string> + <string name="fex_extreme_gn_detail">Prestasi dengan petunjuk TSC dan metadata. Cuba sebelum Ekstrem.</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">Prestasi dengan pemeriksaan kod penuh dan bit hypervisor disembunyikan.</string> + <string name="game_env_custom_value">Nilai tersuai</string> + <string name="game_env_select_value">Pilih nilai</string> + <string name="user_apps_section">Emulator & apl</string> + <string name="user_apps_add">Tambah</string> + <string name="user_apps_add_detail">Apl atau permainan anda sendiri</string> + <string name="user_apps_kind_script">Skrip</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d%%</string> + <string name="user_apps_starting">Bermula…</string> + <string name="user_apps_added">%1$s ditambah</string> + <string name="user_apps_removed">%1$s dialih keluar</string> + <string name="user_apps_back">Desktop</string> + <string name="user_apps_gone">Apl itu sudah tiada. Pilih semula daripada Desktop.</string> + <string name="user_apps_open">Buka %1$s</string> + <string name="user_apps_remove">Alih keluar</string> + <string name="user_apps_remove_confirm">Tekan sekali lagi untuk alih keluar</string> + <string name="user_apps_stop_session">Hentikan sesi untuk alih keluar</string> + <string name="user_apps_script_linked">Menjalankan %1$s daripada folder asalnya, dipautkan ke dalam sesi di tempat ia berada. Mengalih keluar apl ini tidak mengubah folder tersebut.</string> + <string name="user_apps_script_copied">Menjalankan %1$s daripada salinan foldernya dalam DroidDeck. Mengalih keluar apl ini memadam salinan tersebut, bukan yang asal.</string> + <string name="user_apps_runtime_required">Pasang runtime Linux dahulu</string> + <string name="user_apps_file_gone">Fail itu sudah tiada</string> + <string name="user_apps_no_space">Ruang kosong tidak mencukupi: menyalin folder memerlukan %1$s</string> + <string name="user_apps_copying">Menyalin %1$s</string> + <string name="user_apps_failed">Menambah apl gagal</string> + <string name="user_apps_github_checking">Mencari keluaran %1$s</string> + <string name="user_apps_github_unreachable">Tidak dapat membaca keluaran %1$s</string> + <string name="user_apps_github_no_appimage">Tiada keluaran %1$s mempunyai AppImage ARM64 atau x86_64</string> + <string name="user_apps_downloading">Memuat turun %1$s</string> + <string name="user_apps_download_failed">Muat turun gagal</string> + <string name="user_apps_update_failed">Mengemas kini apl gagal</string> + <string name="user_apps_icon_failed">Ikon itu tidak dapat dibaca</string> + <string name="user_apps_saved">%1$s disimpan</string> + <string name="user_apps_edit">Edit</string> + <string name="user_apps_check_updates">Semak kemas kini</string> + <string name="user_apps_checking_updates">Menyemak kemas kini…</string> + <string name="user_apps_update_to">Kemas kini kepada %1$s</string> + <string name="user_apps_up_to_date">Terkini · %1$s</string> + <string name="user_apps_updated">%1$s dikemas kini</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">Tambah apl</string> + <string name="add_app_tab_script">Skrip</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">Fail .sh. Ia berjalan daripada folder sendiri, skrin penuh di bawah gamescope.</string> + <string name="add_app_appimage_hint">AppImage ARM64 (aarch64), atau yang x86_64 yang berjalan melalui FEX. Ia dinyahpek ke dalam runtime Linux sekali sahaja.</string> + <string name="add_app_github_hint">Memasang AppImage ARM64 daripada keluaran terbaharu repositori, atau versi x86_64-nya melalui FEX.</string> + <string name="app_fex_title">Gunakan FEX</string> + <string name="app_fex_auto">Auto</string> + <string name="app_fex_on">Hidup</string> + <string name="app_fex_off">Mati</string> + <string name="app_fex_hint">FEX menjalankan program Linux x86 pada peranti ARM64 ini. Auto menggunakannya apabila program dibina untuk x86.</string> + <string name="app_fex_x86">%1$s melalui FEX</string> + <string name="app_fex_not_ready">Program ini dibina untuk PC %1$s dan berjalan melalui FEX, yang belum disediakan lagi. Buka Steam sekali semasa anda log masuk: DroidDeck akan memuat turun FEX dan Steam Linux Runtime yang diperlukan, kemudian ini akan dibuka.</string> + <string name="session_end_program">Program ini</string> + <string name="session_end_fex_missing">%1$s dibina untuk PC x86 dan berjalan melalui FEX, yang belum dipasang lagi. Mulakan Steam sekali semasa anda log masuk dan biarkan ia memuat turun FEX dan Steam Linux Runtime, kemudian mulakan ini semula.</string> + <string name="add_app_flatpak_hint">ID apl Flathub atau halaman Flathubnya. Flatpak akan disediakan dahulu jika belum lagi.</string> + <string name="add_app_file">Fail</string> + <string name="add_app_no_file">Tiada fail dipilih</string> + <string name="add_app_choose_file">Pilih fail</string> + <string name="add_app_change_file">Tukar</string> + <string name="add_app_pick_script">Pilih skrip</string> + <string name="add_app_pick_appimage">Pilih AppImage</string> + <string name="add_app_pick_icon">Pilih ikon</string> + <string name="add_app_repository">Repositori</string> + <string name="add_app_repository_placeholder">owner/repo atau pautan github.com</string> + <string name="add_app_repository_invalid">Bukan repositori GitHub</string> + <string name="add_app_flatpak_id">ID apl</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">Bukan ID apl Flathub</string> + <string name="add_app_flatpak_bundle_hint">Atau fail .flatpak, untuk apl yang tiada di Flathub.</string> + <string name="add_app_pick_flatpak">Pilih fail .flatpak</string> + <string name="add_app_source">Sumber</string> + <string name="add_app_appearance">Pada Desktop anda</string> + <string name="add_app_preview_name">Apl baharu</string> + <string name="add_app_name">Nama</string> + <string name="add_app_name_placeholder">Daripada apl</string> + <string name="add_app_icon">Ikon</string> + <string name="add_app_icon_default">Ikon lalai</string> + <string name="add_app_choose_icon">Pilih ikon</string> + <string name="add_app_clear_icon">Kosongkan</string> + <string name="add_app_icon_suggestions">Daripada repositori</string> + <string name="add_app_icon_searching">Mencari ikon…</string> + <string name="add_app_icon_reset">Lalai</string> + <string name="edit_app_title">Edit %1$s</string> + <string name="edit_app_save">Simpan</string> + <string name="add_app_checking_folder">Menyemak folder…</string> + <string name="add_app_folder_linked">Foldernya dipautkan ke dalam sesi: %1$s</string> + <string name="add_app_folder_copied">Foldernya menyimpan program Linux, yang tidak dapat dijalankan oleh storan kongsi, jadi ia disalin ke dalam DroidDeck (%1$s).</string> + <string name="add_app_confirm">Tambah</string> + <string name="add_app_cancel">Batal</string> + <string name="add_app_runtime_required">Pasang runtime Linux daripada Persediaan dahulu.</string> + <string name="add_app_busy">Tunggu %1$s selesai.</string> + <string name="steam_compat_label">Serasi</string> + <string name="proton_compatible_note">Binaan yang ditanda “%1$s” dalam menu Compatibility Steam mempunyai pembaikan prefix DroidDeck dan boleh memulakan permainan di sini. Lalai adalah Proton Experimental ARM64 (%1$s), atau Proton 11.0 ARM64 (%1$s) sebaik sahaja binaan terbaharu itu dipasang. Entri Proton asal Valve tanpa tanda ini tidak boleh memulakan permainan pada peranti ini.</string> + <string name="proton_installed_compatible">Dipasang %1$s · %2$s</string> + <string name="common_cancel">Batal</string> + <string name="common_auto">Auto</string> + <string name="common_never">Tidak pernah</string> + <string name="common_always">Sentiasa</string> + <string name="common_uninstall">Nyahpasang</string> + <string name="common_check">Semak</string> + <string name="common_update">Kemas kini</string> + <string name="common_checking">Menyemak…</string> + <string name="common_working">Memproses…</string> + <string name="common_forget">Lupakan</string> + <string name="common_add_ellipsis">Tambah…</string> + <string name="common_use">Guna</string> + <string name="common_applies_next_session">Berkuat kuasa pada sesi seterusnya.</string> + <string name="mode_steam_title">Sesi Steam</string> + <string name="mode_desktop_title">Sesi Desktop</string> + <string name="mode_display">Paparan</string> + <string name="mode_resolution">Resolusi</string> + <string name="mode_res_720">Sehingga 720p</string> + <string name="mode_res_900">Sehingga 900p</string> + <string name="mode_res_1080">Sehingga 1080p</string> + <string name="mode_res_native">Asli</string> + <string name="mode_res_default">%1$s (lalai)</string> + <string name="mode_res_custom_value">Tersuai · %1$d×%2$d</string> + <string name="mode_res_custom">Tersuai…</string> + <string name="mode_res_note">720p boleh menjadikan menu lebih responsif.</string> + <string name="mode_ratio">Nisbah skrin</string> + <string name="mode_ratio_custom">Ditetapkan oleh resolusi tersuai.</string> + <string name="mode_ratio_auto">Auto menggunakan sekurang-kurangnya 16:9.</string> + <string name="mode_fps">Had bingkai</string> + <string name="mode_fps_note">Mengehadkan keseluruhan sesi. Paparan beralih kepada kadar penyegaran yang boleh dibahagi sama rata dengan had ini.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">Output HDR10</string> + <string name="mode_hdr_unavailable">Tidak tersedia: %1$s.</string> + <string name="mode_hdr_restart">Mulakan semula apl untuk menggunakannya.</string> + <string name="mode_drivers">Pemacu</string> + <string name="mode_gpu_drivers">Pemacu GPU</string> + <string name="mode_gpu_drivers_hint">Dikongsi oleh Steam dan Desktop. Ditetapkan pada halaman Komponen.</string> + <string name="mode_touch_controls">Sentuhan & kawalan</string> + <string name="mode_touch">Sentuhan</string> + <string name="mode_touch_touchpad">Pad sentuh</string> + <string name="mode_touch_direct">Terus</string> + <string name="mode_touch_note">Auto: pad sentuh pada Desktop, terus dalam Steam.</string> + <string name="mode_osc">Kawalan pada skrin</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">Auto memaparkan kawalan apabila tiada pengawal disambungkan.</string> + <string name="osc_steam_touch">Sentuhan Steam (beta)</string> + <string name="osc_style_next_session">Kawalan sentuh Steam bermula pada sesi Steam seterusnya</string> + <string name="osc_steam_layout">Susun atur sentuh Steam</string> + <string name="osc_steam_layout_hint">Gerakkan, ubah saiz atau sembunyikan kawalan sentuh permainan ini; disimpan ke Steam</string> + <string name="mode_controller">Pengawal</string> + <string name="mode_controller_hint">Cara Steam mengesan pengawal anda. Berkuat kuasa pada sesi seterusnya.</string> + <string name="mode_controller_deck">Pengawal Steam Deck</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Pengawal Xbox 360</string> + <string name="mode_controller_note">Deck: butang Quick Access dan giro. Steam Controller: sama, dengan grip sense pada butang belakang. DualShock 4 dan DualSense: giro, dan trackpad sebagai pad sentuhnya. Xbox 360: buka Quick Access dengan Guide+A.</string> + <string name="mode_back">Kembali</string> + <string name="back_menu_then_qam">Menu → QAM</string> + <string name="back_qam_then_menu">QAM → Menu</string> + <string name="mode_session">Sesi</string> + <string name="mode_suspend">Gelagat latar belakang</string> + <string name="mode_suspend_hint">Apabila apl disembunyikan atau skrin dimatikan.</string> + <string name="mode_suspend_manual">Manual</string> + <string name="mode_suspend_native">Asli</string> + <string name="mode_suspend_steam_note">Auto menjeda dan menyambung semula apabila dipaparkan. Asli menyediakan Steam untuk tidur dahulu. Manual menunggu Sambung. Jangan sekali-kali terus berjalan.</string> + <string name="mode_background_downloads">Teruskan muat turun</string> + <string name="mode_background_downloads_hint">Lambatkan penjedaan latar belakang semasa Steam aktif memuat turun.</string> + <string name="mode_suspend_note">Auto menjeda di latar belakang dan menyambung semula apabila dipaparkan. Manual menunggu Sambung. Jangan sekali-kali terus berjalan.</string> + <string name="mode_startup">Permulaan</string> + <string name="mode_steam_startup">Mulakan Steam bersama DroidDeck</string> + <string name="mode_steam_startup_hint">Membuka sesi Steam apabila DroidDeck dilancarkan.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">Menyemak keluaran…</string> + <string name="mode_decky_ready">Sedia untuk memasang %1$s.</string> + <string name="mode_decky_none">Tiada binaan serasi ditemui. Cuba lagi kemudian.</string> + <string name="mode_decky_installed">Dipasang · %1$s</string> + <string name="mode_decky_update">Kemas kini tersedia · %1$s</string> + <string name="mode_decky_current">Terkini · %1$s</string> + <string name="mode_decky_loader">Loader</string> + <string name="mode_decky_install_latest">Pasang yang terbaharu</string> + <string name="mode_decky_on">Bermula bersama Steam. Semasa dihidupkan, apl lain pada peranti ini mungkin boleh mengawal Steam.</string> + <string name="mode_decky_off">Mati. Port debug jauh Steam kekal tertutup.</string> + <string name="mode_decky_remove_title">Nyahpasang Decky Loader?</string> + <string name="mode_decky_remove_text">Mengalih keluar loader. Plugin dan tetapan anda dikekalkan.</string> + <string name="mode_client">Klien</string> + <string name="mode_mangoapp">Overlay prestasi</string> + <string name="mode_mangoapp_hint">Matikan jika permainan ranap atau skrin hitam dalam mod Deck. Berkuat kuasa pada sesi seterusnya.</string> + <string name="mode_steam_repair">Baik pulih klien Steam</string> + <string name="mode_steam_repair_hint">Memuat turun semula klien Steam pada permulaan Steam seterusnya. Gunakan jika Steam tersekat pada skrin pemuatannya. Permainan, log masuk dan tetapan dikekalkan.</string> + <string name="mode_steam_repair_queued">Klien Steam akan dimuat turun semula pada permulaan Steam seterusnya.</string> + <string name="mode_steam_repair_button">Baik pulih</string> + <string name="mode_added_art">Karya seni</string> + <string name="mode_sgdb_key">Kunci API SteamGridDB</string> + <string name="mode_sgdb_key_yours">Kunci anda</string> + <string name="mode_sgdb_edit">Edit…</string> + <string name="mode_added_art_hint">Melengkapkan karya seni yang hilang daripada Steam, kemudian SteamGridDB. cover.jpg, hero.jpg, logo.png atau icon.png anda sendiri dalam folder permainan diutamakan.</string> + <string name="summary_remove_game">Alih keluar %1$s</string> + <string name="mode_added_remove_confirm">Alih keluar?</string> + <string name="games_add">Tambah permainan</string> + <string name="games_add_title">Tambah permainan:</string> + <string name="games_pick_exe_title">Pilih exe:</string> + <string name="games_add_unreachable">Lokasi ini tidak tersedia dalam sesi</string> + <string name="mode_added_exe_check">Semak exe ini</string> + <string name="wincomp_title">Komponen Windows</string> + <string name="wincomp_pad_hint">LB / RB bertukar antara bahagian.</string> + <string name="wincomp_card_none">Tiada</string> + <string name="wincomp_applies">DLL Windows yang tidak disertakan dalam Proton, daripada senarai komponen. Hidupkan yang diperlukan oleh permainan ini: ia dimasukkan ke dalam folder Windows permainan itu sendiri, bukan failnya, dan kekal pada Proton mana sekalipun ia berjalan.</string> + <string name="wincomp_offline">Tidak dapat mencapai senarai komponen. Komponen yang sudah dimuat turun masih boleh ditukar.</string> + <string name="wincomp_recommended">Disyorkan untuk permainan ini</string> + <string name="wincomp_checking">Menyemak keperluan permainan ini…</string> + <string name="wincomp_when_online">Tersedia semasa dalam talian</string> + <plurals name="wincomp_turning_on"> + <item quantity="other">Menghidupkan %1$d komponen…</item> + </plurals> + <string name="wincomp_auto_section">Dihidupkan untuk permainan ini</string> + <string name="wincomp_auto_steam">Steam memasang %1$s dengan %2$s (apl %3$s)</string> + <string name="wincomp_auto_gog">Daripada senarai GOG untuk permainan ini</string> + <string name="wincomp_auto_epic">Daripada prasyarat Epic untuk permainan ini</string> + <string name="wincomp_auto_amazon">Daripada langkah pemasangan Amazon untuk permainan ini</string> + <string name="wincomp_steam_by_name">Steam memasang %1$s dengan apl %2$s, disepadankan mengikut nama</string> + <string name="wincomp_suggestions">Turut disenaraikan</string> + <string name="wincomp_suggestion_reason">Disenaraikan oleh %1$s; bukan komponen di sini</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">Permainan ini menyertakan %1$s</string> + <string name="wincomp_found_shipped">Permainan ini mempunyai %1$s (selalunya berfungsi seadanya)</string> + <string name="wincomp_already_here">Sudah ada dalam permainan ini</string> + <string name="wincomp_no_recommendation">Tiada apa-apa dalam folder permainan yang merujuk kepada komponen.</string> + <string name="wincomp_active">Dipasang untuk permainan ini (%1$d)</string> + <string name="wincomp_all">Semua komponen (%1$d)</string> + <string name="wincomp_installers">Memerlukan pemasang Windows (%1$d)</string> + <string name="wincomp_installers_note">Ini adalah program pemasangan sendiri tanpa pakej dalaman untuk dibuka, dan belum direkodkan apa yang dipasangnya. Kebanyakannya runtime lama yang sudah digantikan oleh Mono dalam Proton; rekod ditambah apabila ia disediakan.</string> + <string name="wincomp_show">Tunjukkan</string> + <string name="wincomp_hide">Sembunyikan</string> + <string name="wincomp_needs_installer">Menjalankan program pemasangan sendiri (belum direkodkan)</string> + <string name="wincomp_unsupported">Belum disokong di sini</string> + <string name="wincomp_loading">Memuatkan komponen…</string> + <string name="wincomp_failed">Tidak dapat memasang: %1$s</string> + <string name="wincomp_downloaded">Dimuat turun</string> + <string name="wincomp_downloading">Memuat turun %1$s</string> + <string name="wincomp_installing">Memasang: %1$s</string> + <string name="wincomp_reverting">Mematikan…</string> + <string name="wincomp_download_size">Muat turun %1$s</string> + <string name="wincomp_next_launch">Berkuat kuasa pada permulaan permainan seterusnya.</string> + <string name="mode_stretch169">Regangkan permainan 16:9</string> + <string name="mode_stretch169_hint">Pada skrin yang lebih segi empat daripada 16:9, meregangkan permainan untuk memenuhi skrin dan bukan menunjukkan jalur. Berkuat kuasa pada sesi seterusnya.</string> + <string name="mode_audio">Audio</string> + <string name="mode_directaudio">DirectAudio untuk permainan</string> + <string name="mode_directaudio_hint">Memintas PulseAudio untuk kependaman yang lebih rendah.</string> + <string name="mode_mic">Mikrofon</string> + <string name="mode_mic_hint">Untuk sembang suara.</string> + <string name="mode_network">Rangkaian</string> + <string name="mode_wifi_discovery">Penemuan Wi-Fi</string> + <string name="mode_wifi_discovery_hint">Tunjukkan nama Wi-Fi dan rangkaian berdekatan dalam Steam.</string> + <string name="mode_wifi_location_off">Perkhidmatan lokasi dimatikan.</string> + <string name="mode_wifi_permission_denied">Kebenaran lokasi tidak diberikan.</string> + <string name="mode_wifi_open_settings">Buka tetapan</string> + <string name="mode_wifi_continue">Teruskan</string> + <string name="mode_wifi_explain_title">Aktifkan penemuan Wi-Fi?</string> + <string name="mode_wifi_explain_text">Android memerlukan kebenaran Lokasi untuk membaca nama Wi-Fi dan menemui rangkaian berdekatan. DroidDeck menggunakan akses ini untuk menunjukkan rangkaian tersebut dalam Steam. Kami tidak menjejaki lokasi anda. Akses internet berfungsi tanpa kebenaran ini.</string> + <string name="mode_storage">Storan permainan</string> + <string name="mode_storage_auto">Automatik - kad SD jika ada, serta folder Games anda</string> + <string name="mode_storage_auto_none">Automatik - folder Games anda, serta kad SD jika ada (tiada sekarang)</string> + <string name="mode_storage_internal">Dalaman sahaja</string> + <string name="mode_storage_folder">Folder: %1$s</string> + <string name="mode_second_library">Pustaka kedua</string> + <string name="mode_storage_note">Permainan yang menstrim aset daripada SD atau storan kongsi mungkin tersekat-sekat.</string> + <string name="mode_storage_diagnostics">Diagnostik storan</string> + <string name="mode_storage_diagnostics_hint">Rekod keputusan peruntukan dan masa storan yang disampel dalam Kongsi log. Boleh melambatkan pemasangan semasa dihidupkan. Berkuat kuasa pada sesi seterusnya.</string> + <string name="mode_choose_folder">Pilih folder…</string> + <string name="mode_custom_res_title">Resolusi tersuai</string> + <string name="mode_custom_res_text">Menggantikan had dan nisbah. Saiz yang tidak sepadan dengan skrin akan mendapat jalur.</string> + <string name="mode_width">Lebar</string> + <string name="mode_height">Tinggi</string> + <string name="mode_custom_res_range">Antara 320×240 dan 3840×2160.</string> + <plurals name="mode_added_count"> + <item quantity="other">%1$d permainan</item> + </plurals> + <string name="common_ok">OK</string> + <string name="common_delete">Padam</string> + <string name="common_restore">Pulihkan</string> + <string name="common_download">Muat turun</string> + <string name="common_delete_named">Padam %1$s</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Pemacu runtime</string> + <string name="comp_runtime_driver_hint">Digunakan oleh Steam, permainan dan desktop. Berkuat kuasa pada sesi seterusnya.</string> + <string name="comp_import_turnip">Import zip Turnip…</string> + <string name="comp_display_driver">Pemacu paparan</string> + <string name="comp_display_driver_hint">Melukis bingkai ke skrin dalam kedua-dua mod. Mulakan semula apl untuk menggunakannya.</string> + <string name="comp_import_adrenotools">Import zip AdrenoTools…</string> + <string name="comp_never">tidak pernah</string> + <string name="comp_nightlies_checked">Nightlies disemak %1$s</string> + <string name="comp_title">Komponen</string> + <string name="comp_about">Perihal Komponen</string> + <string name="comp_check_drivers">Semak pemacu baharu</string> + <string name="comp_check_nightlies">Semak Nightlies untuk pakej baharu</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">Proton yang dipilih di sini menjalankan setiap permainan yang belum memilih Protonnya sendiri dalam Steam, dan perubahan yang dibuat dalam Steam turut dipaparkan di sini. Steam menggantikan Proton Valve apabila ia dikemas kini; versi asal dikekalkan bagi setiap binaan.</string> + <string name="comp_scope">Konfigurasi</string> + <string name="comp_profile_configure">Konfigurasikan profil</string> + <string name="comp_profile">Profil</string> + <string name="comp_scope_general">Profil lalai</string> + <string name="comp_scope_general_detail">Digunakan secara automatik oleh permainan tanpa penggantian.</string> + <string name="comp_profile_game_detail">Tetapan tersimpan yang digunakan secara automatik apabila %1$s dilancarkan.</string> + <string name="comp_game_hint">Pilih hanya tetapan yang diperlukan permainan ini. Selebihnya terus menggunakan profil Lalai.</string> + <string name="comp_game_fex">Praset FEX</string> + <string name="comp_game_fex_inherited">Menggunakan praset profil Lalai.</string> + <string name="comp_game_fex_override">Praset tersuai disimpan untuk permainan ini.</string> + <string name="comp_game_fex_custom">Pembolehubah FEX tersuai</string> + <string name="comp_game_use_general">Guna Lalai</string> + <string name="comp_game_compatibility">Keserasian permainan</string> + <string name="comp_game_environment_hint">Pilihan persekitaran Proton, DXVK, VKD3D dan penyegerakan untuk permainan ini.</string> + <string name="comp_game_proton_inherited">Menggunakan Proton profil Lalai.</string> + <string name="comp_game_proton_override">Proton tersuai disimpan untuk permainan ini melalui pemetaan keserasian Steam.</string> + <string name="comp_no_proton_short">Tiada Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Menggunakan pakej komponen profil Lalai daripada %1$s melainkan permainan ini memilih pakej lain.</string> + <string name="comp_game_component_general">Menggunakan pakej komponen profil Lalai.</string> + <string name="comp_game_component_override">Menggunakan %1$s hanya untuk permainan ini.</string> + <string name="comp_game_runtime_options">Pilihan runtime</string> + <string name="comp_game_environment_other_hint">Pilihan persekitaran Proton, DXVK, VKD3D-Proton dan penyegerakan lain bagi setiap permainan.</string> + <string name="comp_game_saved_next">Profil %1$s disimpan untuk pelancaran permainan seterusnya.</string> + <string name="game_no_steam_title">Mulakan tanpa Steam</string> + <string name="game_no_steam_detail">Untuk permainan yang terus tertutup: memulakannya tanpa persediaan Steam oleh Proton. Ciri Steam seperti overlay dimatikan untuknya.</string> + <string name="game_no_steam_on">Hidup</string> + <string name="comp_game_proton_live">Pilihan Proton %1$s disimpan dan tersedia dalam Steam sekarang.</string> + <string name="comp_game_proton_next">Pilihan Proton %1$s disimpan. Mulakan semula Steam sebelum melancarkan permainan.</string> + <string name="comp_game_proton_not_runnable">%1$s tidak boleh menggunakan Proton yang dipilih pada peranti ini.</string> + <string name="comp_game_load_failed">Tidak dapat memuatkan profil %1$s: %2$s</string> + <string name="comp_game_save_failed">Profil permainan tidak dapat disimpan: %1$s</string> + <string name="comp_game_reset">Tetapkan semula profil permainan</string> + <string name="comp_game_reset_title">Tetapkan semula profil %1$s?</string> + <string name="comp_game_reset_body">Alih keluar penggantian Proton, FEX, pakej komponen, persekitaran, penapisan tekstur dan komponen Windows permainan ini? Ia akan menggunakan profil Lalai.</string> + <string name="comp_game_reset_done">%1$s kini menggunakan profil Lalai.</string> + <string name="comp_game_reset_failed">Profil permainan tidak dapat ditetapkan semula sepenuhnya: %1$s</string> + <string name="comp_default_live">%1$s kini menjalankan setiap permainan yang belum memilih Protonnya sendiri dalam Steam.</string> + <string name="comp_default_next">%1$s akan menjalankan setiap permainan yang belum memilih Protonnya sendiri, mulai permulaan Steam seterusnya.</string> + <string name="comp_default_not_runnable">%1$s tidak boleh memulakan permainan di sini, jadi ia tidak boleh menjadi lalai.</string> + <string name="comp_default_failed">Proton lalai tidak dapat disimpan: %1$s</string> + <string name="comp_swapped">%1$s · %2$d ditukar</string> + <string name="comp_all_original">%1$s · semua asal</string> + <string name="comp_game_running_suffix">%1$s · permainan sedang berjalan</string> + <string name="comp_esync_pack">%1$s · pek droiddeck-esync</string> + <string name="comp_esync_wanted">%1$s · belum ada pek droiddeck-esync</string> + <string name="comp_esync_builtin">%1$s · esync terbina dalam, tiada pek diperlukan</string> + <string name="comp_gpu_drivers">Pemacu GPU</string> + <string name="comp_reapplied">%1$s · digunakan semula pada pelancaran %2$s</string> + <string name="comp_next_queued">Seterusnya: %1$s · selepas permainan ditutup</string> + <string name="comp_in_use">%1$s digunakan: %2$s</string> + <string name="comp_waits">Menunggu sehingga permainan ditutup</string> + <string name="comp_applies_next_game">Berkuat kuasa pada permulaan permainan seterusnya</string> + <string name="comp_import_zip">Import .zip</string> + <string name="comp_reading">Membaca Proton…</string> + <string name="comp_no_proton">Tiada Proton dipasang lagi. Mulakan Steam sekali untuk memuat turun Proton ARM64-nya, atau tambah GE-Proton, CachyOS atau DroidDeck-Proton daripada Persediaan.</string> + <string name="comp_original">Asal · %1$s</string> + <string name="comp_this_build">%1$s · binaan ini</string> + <string name="comp_earlier_build">%1$s · binaan terdahulu</string> + <string name="comp_installed_count">Dipasang · %1$d</string> + <string name="comp_nothing_stored">Belum ada apa-apa disimpan untuk %1$s. Fail asal Proton ini disimpan sebagai yang asalnya.</string> + <string name="comp_waits_long">Permainan sedang berjalan pada Proton ini, jadi ini menunggu sehingga ia ditutup.</string> + <string name="comp_applies_long">Berkuat kuasa pada permulaan permainan seterusnya.</string> + <string name="comp_swap_title">Tukar %1$s?</string> + <string name="comp_swap_body">Masukkan %1$s ke dalam %2$s? Fail asalnya kekal disimpan. %3$s</string> + <string name="comp_restore_title">Pulihkan %1$s?</string> + <string name="comp_restore_body">Masukkan semula %1$s asal ke dalam %2$s? %3$s</string> + <string name="comp_delete_package_body">Mengalih keluar pakej daripada apl. Anda boleh memuat turunnya semula.</string> + <string name="comp_delete_original_title">Padam yang asal ini?</string> + <string name="comp_delete_original_body">%1$s asal ini daripada binaan terdahulu %2$s. Yang asal bagi binaan semasa sentiasa dikekalkan.</string> + <string name="comp_nightlies_count">Pada Nightlies · %1$d</string> + <string name="comp_refresh_to_list">Segarkan semula untuk menyenaraikan pakej pada Nightlies.</string> + <string name="comp_nothing_new">Tiada apa-apa yang baharu sejak semakan terakhir.</string> + <string name="comp_import_wcp">Import .wcp</string> + <string name="comp_restore_original">Pulihkan yang asal bagi Proton ini</string> + <string name="comp_restore_original_body">Masukkan semula %1$s asal milik %2$s?</string> + <string name="comp_swap">Tukar</string> + <string name="comp_about_text">FEX, DXVK dan VKD3D-Proton untuk setiap Proton yang menjalankan permainan dalam Steam.\n\nPilih baris yang dipasang untuk menukarnya. Ia berkuat kuasa pada permulaan permainan seterusnya, atau selepas permainan yang sedang berjalan ditutup.\n\nPilihan anda disemak semula sebelum setiap pelancaran dan dikembalikan jika ada yang mengubahnya. Fail asal setiap Proton dikekalkan, jadi kemas kini Steam tidak akan menghilangkannya.</string> + <string name="comp_tag_original">Asal</string> + <string name="comp_tag_in_use">Digunakan</string> + <string name="comp_tag_new">Baharu</string> + <string name="comp_tag_stored">Disimpan</string> + <string name="perf_title">Prestasi</string> + <string name="perf_lede">Teras CPU, pembaikan sesi dan nama hos. Berkuat kuasa pada sesi seterusnya.</string> + <string name="perf_client_cores_group">Teras klien Steam</string> + <string name="perf_override">Gantikan pilihan teras Steam</string> + <string name="perf_override_hint">Memasak Steam, pembantu UI-nya dan gamescope kepada teras di bawah.</string> + <string name="perf_client_cores">Teras klien</string> + <string name="perf_client_cores_on">Teras untuk Steam.</string> + <string name="perf_client_cores_off">Hidupkan penggantian untuk memilih.</string> + <string name="perf_game_cores_group">Teras permainan</string> + <string name="perf_game_cores">Teras permainan</string> + <string name="perf_game_cores_hint">Teras untuk permainan. Dengan semua dipilih, Android menjadualkan secara bebas.</string> + <string name="perf_client_ui">Antara muka klien</string> + <string name="perf_glthread">GL berbenang</string> + <string name="perf_glthread_hint">Boleh menjadikan menu Steam lebih responsif.</string> + <string name="perf_zink">Zink: deskriptor malas</string> + <string name="perf_zink_hint">Disyorkan untuk pemacu tanpa penampan deskriptor.</string> + <string name="perf_noglerror">Langkau semakan ralat GL</string> + <string name="perf_noglerror_hint">Mematikan pengesahan GL bagi setiap panggilan.</string> + <string name="perf_gsrealtime">gamescope: baris gilir GPU masa nyata</string> + <string name="perf_gsrealtime_hint">Mengutamakan kerja GPU compositor. Boleh melicinkan rentak bingkai dengan sedikit kos kepada permainan.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">Kekalkan GPU pada kelajuan tertinggi</string> + <string name="perf_gpuclock_hint">Adreno sahaja. Kurang tersekat-sekat, lebih penggunaan bateri dan haba.</string> + <string name="perf_fixes">Pembetulan sesi</string> + <string name="perf_sysmem">Turnip: rendering sysmem</string> + <string name="perf_sysmem_hint">Lebih pantas untuk Steam dan kebanyakan permainan. Diperlukan pada Adreno 710/720/722.</string> + <string name="perf_xalia">Langkau pembantu xalia Steam</string> + <string name="perf_fastsync">droiddeck-ntsync (eksperimen)</string> + <string name="perf_fastsync_hint">Pengganti ruang pengguna untuk pemacu ntsync Linux, untuk pengujian. Semasa ia hidup, Proton menggunakannya sebagai ganti droiddeck-esync dan droiddeck-fsync. Berfungsi dengan Proton Experimental ARM64, GE-Proton dan proton-cachyos. Berkuat kuasa pada sesi seterusnya.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync dahulu</string> + <string name="perf_fsync_hint">Menjalankan fsync Proton sendiri pada fail Proton standard, dengan futex_waitv dibekalkan dalam ruang pengguna, jadi ia tidak memerlukan pek dan terus berfungsi sebaik sahaja Steam mengemas kini Proton. Semasa ia hidup, permainan menggunakannya mendahului droiddeck-esync, kecuali beberapa Proton yang mematikan fsync. Berkuat kuasa pada sesi seterusnya.</string> + <string name="perf_esync_hint">Thread Windows permainan menunggu satu sama lain melalui eventfd dan memori kongsi daripada rangkaian pergi balik melalui wineserver. Menggunakan pek droiddeck-esync yang dibuat untuk binaan Proton itu dengan tepat; pek diambil secara automatik. Sehingga satu wujud untuk binaan Proton baharu, droiddeck-fsync menggantikannya. Berkuat kuasa pada sesi seterusnya.</string> + <string name="perf_xalia_hint">Melumpuhkan pembantu navigasi pad Proton, yang memakan CPU dalam setiap permainan.</string> + <string name="perf_seccomp">Jalankan proot tanpa seccomp</string> + <string name="perf_seccomp_hint">Mungkin membetulkan ralat syscall yang hilang pada sesetengah kernel. Boleh mengurangkan prestasi.</string> + <string name="perf_fastpath">Laluan pantas proot</string> + <string name="perf_fastpath_needs_seccomp">Memerlukan penapis seccomp proot.</string> + <string name="perf_fastpath_hint">Carian fail lebih pantas dan kurang tersekat-sekat. Matikan jika sesuatu program tidak dapat mencari failnya.</string> + <string name="perf_identity">Identiti sesi</string> + <string name="perf_hostname">Nama hos</string> + <string name="perf_hostname_hint">Nama yang dilihat Steam untuk peranti ini. Kosongkan untuk memulihkan %1$s.</string> + <string name="perf_hostname_invalid">Huruf, digit dan tanda sempang dalaman sahaja, sehingga 63 aksara.</string> + <string name="perf_phantom_warning">Android ditetapkan untuk menamatkan sesi ini</string> + <string name="driver_installed">Dipasang</string> + <string name="driver_available">Tersedia untuk dimuat turun</string> + <string name="driver_nothing_new">Tiada yang baharu sejak semakan terakhir.</string> + <string name="driver_restore_builtin">Pulihkan pemacu terbina dalam</string> + <string name="driver_delete_builtin">Terbina dalam apl: ini mengalih keluar fail nyahpaknya dan menyembunyikannya. Pulihkan pemacu terbina dalam akan mengembalikannya.</string> + <string name="driver_delete_bundle">Separuh daripada gabungan Android + Linux: kedua-dua separuh dialih keluar. Pemacu yang digunakan beralih kepada lalai.</string> + <string name="driver_delete_plain">Failnya dialih keluar daripada apl.</string> + <string name="driver_delete_in_use">Failnya dialih keluar. Ia sedang digunakan, jadi lalai mengambil alih.</string> + <string name="drawer_page_display">Paparan</string> + <string name="drawer_page_effects">Kesan</string> + <string name="drawer_page_controls">Kawalan</string> + <string name="drawer_page_components">Komponen</string> + <string name="drawer_page_controller">Pengawal</string> + <string name="drawer_page_settings">Tetapan</string> + <string name="drawer_page_desc">Halaman %1$s</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Desktop</string> + <string name="drawer_steam_menu">Menu Steam</string> + <string name="drawer_quick_access">Akses pantas</string> + <string name="drawer_open_qam">Buka Menu Akses Pantas</string> + <string name="drawer_prev_page">Halaman sebelumnya</string> + <string name="drawer_next_page">Halaman seterusnya</string> + <string name="drawer_hud">HUD prestasi</string> + <string name="drawer_scaling">Mod penskalaan</string> + <string name="drawer_scaling_sharpness">Ketajaman penskalaan</string> + <string name="drawer_effects">Kesan skrin</string> + <string name="drawer_effects_hint">Langsung. Memakan sedikit masa GPU semasa mana-mana kesan dihidupkan.</string> + <string name="drawer_look">Prapasang</string> + <string name="drawer_look_custom">Tersuai</string> + <string name="drawer_effects_advanced">Lanjutan</string> + <string name="widgets_expanded">Dikembangkan</string> + <string name="widgets_collapsed">Dikuncupkan</string> + <string name="drawer_cas">Ketajaman (CAS)</string> + <string name="drawer_cas_level">Tahap ketajaman</string> + <string name="drawer_fake_hdr">HDR palsu</string> + <string name="drawer_deband">Pengurangan jalur</string> + <string name="drawer_deband_strength">Kekuatan pengurangan jalur</string> + <string name="drawer_brightness">Kecerahan</string> + <string name="drawer_contrast">Kontras</string> + <string name="drawer_gamma">Gamma</string> + <string name="drawer_saturation">Ketepuan</string> + <string name="drawer_fxaa">FXAA (pelicinan tepi)</string> + <string name="drawer_toon">Garis luar kartun</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Penapisan tekstur</string> + <string name="drawer_texture_hint">Permainan DirectX 9–11, bermula daripada pelancaran seterusnya.</string> + <string name="drawer_anisotropy">Penapisan anisotropik</string> + <string name="drawer_texture_sharpness">Ketajaman tekstur</string> + <string name="drawer_texture_sharpness_note">Pincang mip negatif: tekstur lebih tajam, sedikit kilauan. Auto sepadan dengan sejauh mana sesi dibesarkan ke skrin.</string> + <string name="drawer_touch_auto">Auto (%1$s)</string> + <string name="drawer_keyboard">Papan kekunci</string> + <string name="drawer_pc_keyboard">Papan kekunci PC</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">Skrin kedua</string> + <string name="drawer_second_screen_mode">Mod</string> + <string name="drawer_second_screen_shows">Memaparkan</string> + <string name="drawer_android_apps">Apl Android</string> + <string name="drawer_launch_app">Lancarkan apl</string> + <string name="common_hide">Sembunyi</string> + <string name="common_show">Papar</string> + <string name="drawer_no_apps">Tiada apl boleh dilancarkan</string> + <string name="drawer_session_behavior">Kelakuan sesi</string> + <string name="drawer_suspend_hint">Berkuat kuasa sekarang dan untuk sesi akan datang dalam mod ini.</string> + <string name="drawer_next_session">Sesi seterusnya</string> + <string name="drawer_support">Sokongan</string> + <string name="drawer_logs">Log sesi</string> + <string name="drawer_logs_hint">Lampirkan pada laporan pepijat</string> + <string name="drawer_share_logs">Kongsi log</string> + <string name="drawer_background">Latar belakang</string> + <string name="drawer_stop">Hentikan</string> + <string name="drawer_stop_title">Hentikan sesi?</string> + <string name="drawer_running_now">Sedang berjalan</string> + <string name="drawer_no_game">Tiada permainan berjalan pada mana-mana Proton</string> + <string name="drawer_selected_profile">Profil permainan</string> + <string name="drawer_selected_profile_none">Butiran profil belum tersedia.</string> + <string name="drawer_profile_refresh_failed_initial">Tidak dapat memuatkan profil permainan. Mencuba semula secara automatik.</string> + <string name="drawer_profile_refresh_failed_stale">Tidak dapat menyegarkan profil permainan. Memaparkan data terakhir yang tersedia; mencuba semula secara automatik.</string> + <string name="drawer_profile_inspect_hint">Lihat dan sunting tetapan tersimpan yang digunakan pada pelancaran seterusnya.</string> + <string name="drawer_profile_default">Menggunakan profil Lalai</string> + <string name="drawer_profile_custom">Profil tersuai</string> + <string name="drawer_component_inherited">Menggunakan %1$s daripada profil Lalai.</string> + <string name="drawer_game_components_note">Perubahan disimpan ke profil %1$s dan berkuat kuasa pada kali seterusnya permainan dilancarkan.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="other">%1$d tindanan permainan</item> + </plurals> + <string name="drawer_no_proton">Belum ada Proton dipasang.</string> + <string name="drawer_game_running">permainan sedang berjalan</string> + <string name="drawer_reapplied">digunakan semula pada pelancaran %1$s</string> + <string name="drawer_original">Asal</string> + <string name="drawer_components_note">Import dan padam pada halaman Komponen.</string> + <string name="setup_check_done">Selesai</string> + <string name="setup_check_warn">Perlu perhatian</string> + <string name="setup_check_busy">Sedang berjalan</string> + <string name="setup_limit_adb_failed">Semak alamat IP dan port Debugging Wayarles, atau gandingkan semula jika Android mengalih keluar peranti ini.</string> + <string name="setup_install">Pasang</string> + <string name="setup_manage">Urus</string> + <string name="setup_tab_overview">Gambaran keseluruhan</string> + <string name="setup_tab_controller">Pengawal</string> + <string name="setup_tab_session">Sesi</string> + <string name="setup_tab_launcher">Pelancar</string> + <string name="setup_title">Persediaan</string> + <string name="setup_all_set">● Semua sedia</string> + <string name="setup_n_ready">%1$d daripada %2$d sedia</string> + <string name="setup_system_check">Semakan sistem</string> + <string name="setup_gpu_supported">Peranti disokong</string> + <string name="setup_gpu_untested">GPU belum diuji</string> + <string name="setup_gpu_unsupported">GPU tidak disokong</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">Steam memerlukan pemacu Adreno; %1$s mungkin memaparkan skrin hitam</string> + <string name="setup_runtime">Masa jalan Linux</string> + <string name="setup_runtime_progress">%1$s · %2$d%%</string> + <string name="setup_runtime_missing">Belum dipasang · kira-kira 3 GB, dipasang pada Main pertama</string> + <string name="setup_installed">Dipasang</string> + <string name="setup_runtime_update">%1$s · kemas kini tersedia</string> + <string name="setup_runtime_current">%1$s · terkini</string> + <string name="setup_limit">Had proses anak</string> + <string name="setup_limit_on">Hidup · Android mungkin menutup Steam. Mematikannya mengambil masa seminit.</string> + <string name="setup_limit_unset">Tidak ditetapkan · Android mungkin menutup Steam. Mematikannya mengambil masa seminit.</string> + <string name="setup_limit_unknown">Tidak dapat disemak · mematikannya mengambil masa seminit.</string> + <string name="setup_fix_it">Betulkan</string> + <string name="setup_details">Butiran</string> + <string name="setup_dev_options">Pilihan pembangun</string> + <string name="setup_use_wireless">Gunakan Debugging Wayarles</string> + <string name="setup_check_again">Semak semula</string> + <string name="setup_limit_turn_on">Hidupkan had</string> + <string name="setup_account">Akaun Steam</string> + <string name="setup_signed_in_offline">Dilog masuk sebagai %1$s · mod luar talian</string> + <string name="setup_signed_in">Dilog masuk sebagai %1$s</string> + <string name="setup_sign_in">Tekan Main dan log masuk ke Steam</string> + <string name="setup_tools">Alatan</string> + <string name="setup_controller">Pengawal</string> + <string name="setup_controller_unavailable">Tetapan pengawal tidak tersedia.</string> + <string name="setup_session">Sesi</string> + <string name="setup_logs">Log sesi</string> + <string name="setup_logs_hint">Disimpan selepas setiap sesi</string> + <string name="setup_agent_commands">Alatan penyahpepijatan</string> + <string name="setup_latest_logs">Log sesi terkini</string> + <string name="setup_saved_logs">Log sesi tersimpan</string> + <string name="setup_saved_logs_hint">Menyimpan %1$d yang terbaharu</string> + <string name="setup_clear_logs">Kosongkan log</string> + <string name="setup_offline">Mod luar talian</string> + <string name="setup_sign_in_first">Log masuk ke Steam dahulu</string> + <string name="setup_launcher">Pelancar</string> + <string name="setup_language">Bahasa</string> + <string name="setup_language_hint">Steam juga menggunakannya, dari kali seterusnya ia dimulakan.</string> + <string name="setup_language_system">Lalai sistem (%1$s)</string> + <string name="setup_language_system_short">Lalai sistem</string> + <string name="setup_app_scale">Skala apl</string> + <string name="setup_app_scale_hint">Teks dan kawalan DroidDeck</string> + <string name="setup_app_scale_default">%1$d%% (Lalai)</string> + <string name="setup_theme">Tema</string> + <string name="setup_theme_hint">Rupa pelancar</string> + <string name="setup_home">Gunakan sebagai skrin Utama</string> + <string name="setup_home_on">DroidDeck boleh menjadi apl Utama</string> + <string name="setup_home_off">DroidDeck tidak sekali-kali ditawarkan sebagai apl Utama</string> + <string name="setup_orientation">Orientasi</string> + <string name="setup_orientation_auto">Lanskap automatik</string> + <string name="setup_orientation_landscape">Lanskap</string> + <string name="setup_orientation_reverse">Lanskap songsang</string> + <string name="setup_fullscreen">Skrin penuh</string> + <string name="setup_fullscreen_on">Menyembunyikan bar status dan navigasi Android</string> + <string name="setup_fullscreen_off">Memaparkan bar status dan navigasi Android</string> + <string name="setup_default_home">Apl Utama lalai</string> + <string name="setup_choose_home">Pilih apl Utama</string> + <string name="setup_choose">Pilih</string> + <string name="setup_linux_apps">Apl Linux (beta)</string> + <string name="setup_tool_files">Fail</string> + <string name="setup_tool_files_hint">Semak imbas dan urus fail</string> + <string name="setup_tool_protons">Versi Proton</string> + <string name="setup_tool_protons_hint">Pasang binaan Proton ARM64</string> + <string name="setup_tool_performance">Prestasi</string> + <string name="setup_tool_performance_hint">Penetapan teras CPU</string> + <string name="setup_tool_roms">Folder ROM</string> + <string name="setup_tool_roms_hint">Tempat permainan emulator disimpan</string> + <string name="setup_card_components">Komponen</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Kawalan</string> + <string name="setup_card_controls_hint">Pemetaan butang</string> + <string name="store_tab_discover">Terokai</string> + <string name="store_tab_search">Cari</string> + <string name="store_tab_installed">Dipasang</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Kedai</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak sedia</string> + <string name="store_needs_runtime">Pasang masa jalan Linux daripada Persediaan dahulu.</string> + <string name="store_setup_title">Sediakan Flatpak</string> + <string name="store_setup_text">Menambah Flatpak dan menyambungkannya ke Flathub (kira-kira 20 MB). Apl pertama juga memuat turun masa jalannya.</string> + <string name="store_setting_up">Menyediakan…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Kemas kini</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d%%</string> + <string name="store_starting">Memulakan…</string> + <string name="store_categories">Kategori</string> + <string name="store_unreachable">Tidak dapat menghubungi Flathub. Semak sambungan anda dan cuba lagi.</string> + <string name="store_try_again">Cuba lagi</string> + <string name="store_search_label">Cari di Flathub</string> + <string name="store_search_placeholder">Firefox, permainan, emulator…</string> + <string name="store_search">Cari</string> + <string name="store_all">Semua</string> + <string name="store_searching">Mencari…</string> + <string name="store_search_hint">Cari mengikut nama atau pilih kategori. Hanya apl ARM64 disenaraikan.</string> + <string name="store_no_results">Tiada apl ARM64 di Flathub sepadan dengan itu.</string> + <string name="store_results">Hasil</string> + <string name="store_check_updates">Semak kemas kini</string> + <string name="store_update_all">Kemas kini semua (%1$d)</string> + <string name="store_all_current">● Semuanya terkini</string> + <string name="store_no_apps">Tiada apl lagi. Cari di Terokai atau Cari.</string> + <string name="store_setup_first_note">Sediakan Flatpak untuk memasang apl.</string> + <string name="store_update_available_prefix">Kemas kini tersedia · %1$s</string> + <string name="store_open">Buka</string> + <string name="store_details">Butiran</string> + <string name="store_remove">Alih keluar</string> + <string name="store_press_again">Tekan sekali lagi untuk alih keluar</string> + <string name="store_installing">Memasang…</string> + <string name="store_install">Pasang</string> + <string name="store_setup_first">Sediakan Flatpak dahulu</string> + <string name="store_download_size">Muat turun %1$s</string> + <string name="store_installed_chip">● Dipasang</string> + <string name="store_details_failed">Tidak dapat memuatkan halaman Flathub apl ini.</string> + <string name="store_loading">Memuatkan daripada Flathub…</string> + <string name="store_no_arm">Tiada binaan ARM64 di Flathub, jadi ia tidak boleh dipasang di sini.</string> + <string name="store_screenshots">Tangkapan skrin</string> + <string name="store_about">Perihal</string> + <string name="store_app_id">ID Apl</string> + <string name="store_license">Lesen</string> + <string name="store_runtime">Masa jalan</string> + <string name="store_installed_size">Saiz dipasang</string> + <string name="store_installed_size_value">%1$s (tambah masa jalan kongsinya)</string> + <string name="store_website">Laman web</string> + <string name="store_verified">Disahkan</string> + <string name="upd_title">Kemas kini</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Disemak %1$s</string> + <string name="upd_not_checked">Belum disemak</string> + <string name="upd_perm_title">Benarkan DroidDeck mengemas kini sendiri</string> + <string name="upd_perm_text">Pada skrin seterusnya, hidupkan Benarkan daripada sumber ini, kemudian kembali semula. Kemas kini akan diteruskan secara automatik.</string> + <string name="upd_open_settings">Buka tetapan</string> + <string name="upd_not_now">Bukan sekarang</string> + <string name="upd_signed_status">Ditandatangani berbeza</string> + <string name="upd_signed_headline">Tidak boleh dikemas kini di tempat</string> + <string name="upd_signed_detail">Salinan ini tidak ditandatangani dengan kunci keluaran DroidDeck, jadi kemas kini tidak boleh dipasang menggantikannya. Nyahpasang untuk bertukar.</string> + <string name="upd_available">Kemas kini tersedia</string> + <string name="upd_blocked_status">Tidak boleh dipasang</string> + <string name="upd_switch">Sedia untuk bertukar</string> + <string name="upd_ahead_status">Mendahului Stable</string> + <string name="upd_ahead_headline">Anda mendahului Stable</string> + <string name="upd_ahead_detail">Binaan ini lebih baharu daripada Stable terkini. Anda akan beralih ke Stable pada keluaran seterusnya.</string> + <string name="upd_gone_status">Ujian tamat</string> + <string name="upd_gone_headline">Ujian ini telah tamat</string> + <string name="upd_gone_detail">Pembetulannya telah digabungkan atau dibuang. Ikuti Preview untuk pembetulan terbaharu.</string> + <string name="upd_current_status">Terkini</string> + <string name="upd_current_headline">Anda mempunyai %1$s yang terbaharu</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Kemas kini%1$s</string> + <string name="upd_install_button">Pasang%1$s</string> + <string name="upd_install_anyway">Pasang Stable %1$s juga</string> + <string name="upd_follow_preview">Ikuti Preview</string> + <string name="upd_stop_session">Hentikan sesi yang berjalan untuk mengemas kini.</string> + <string name="upd_less">Kurang</string> + <string name="upd_more">Lagi</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Binaan Preview baharu</string> + <string name="upd_new_test">Binaan ujian PR #%1$d</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">SALURAN KEMAS KINI</string> + <string name="upd_stable">Stable</string> + <string name="upd_stable_hint">Keluaran yang telah diuji, untuk kebanyakan orang</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Preview</string> + <string name="upd_preview_hint">Pembetulan terbaharu, sebelum Stable</string> + <string name="upd_preview_current">Terkini</string> + <string name="upd_tests">Binaan ujian</string> + <string name="upd_tests_hint">Cuba pembetulan sebelum dikeluarkan</string> + <string name="upd_tests_none">Tiada pada masa ini</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Tidak dibina untuk salinan DroidDeck ini</string> + <string name="upd_updated">Dikemas kini %1$s</string> + <string name="upd_test_name">ujian PR #%1$d</string> + <string name="upd_while_ago">beberapa ketika dahulu</string> + <string name="upd_just_now">sebentar tadi</string> + <plurals name="upd_tests_count"> + <item quantity="other">%1$d untuk dicuba</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="other">Perubahan sejak binaan anda · %1$d binaan ketinggalan</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="other">Perubahan terkini · %1$d binaan</item> + </plurals> + <string name="gate_title">Satu tetapan Android untuk ditukar</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Android menghentikan apl yang memulakan banyak proses latar belakang, dan Steam memulakan berpuluh-puluh. Sehingga ini berubah, permainan tertutup tanpa ralat.</string> + <string name="gate_fix_for_me">Betulkan untuk saya</string> + <string name="gate_toggle_steps">Hidupkan “Lumpuhkan sekatan proses anak”, kemudian kembali semula. Biarkan Pilihan pembangun hidup, atau suis itu akan mati semula.</string> + <string name="gate_open_dev">Buka Pilihan pembangun</string> + <string name="gate_no_toggle">Versi Android ini tidak mempunyai suis untuknya, jadi DroidDeck menukarnya melalui Debugging Wayarles. Mengambil masa kira-kira seminit, tanpa komputer diperlukan.</string> + <string name="gate_needs_wifi">Debugging Wayarles memerlukan Wi-Fi. Mana-mana rangkaian berfungsi; ia tidak memerlukan internet.</string> + <string name="gate_needs_notifications">Kod gandingan dihantar dalam pemberitahuan. Benarkan pemberitahuan untuk DroidDeck, atau gunakan Cara lain.</string> + <string name="gate_allow_notifications">Benarkan pemberitahuan</string> + <string name="gate_step1">Buka Tetapan → Pilihan pembangun → Debugging Wayarles dan hidupkannya</string> + <string name="gate_step2">Ketik “Gandingkan peranti dengan kod gandingan”</string> + <string name="gate_step3">Taip kod itu ke dalam pemberitahuan DroidDeck</string> + <string name="gate_waiting">Menunggu tetingkap timbul gandingan…</string> + <string name="gate_code_needed">Tetingkap timbul dijumpai. Masukkan kodnya dalam pemberitahuan.</string> + <string name="gate_status">Status</string> + <string name="gate_polling">Menyemak setiap 2 saat. Halaman ini ditutup apabila ia mati.</string> + <string name="gate_other_ways">Cara lain</string> + <string name="gate_other_toggle">Debugging Wayarles, alamat manual, ADB komputer</string> + <string name="gate_other_no_toggle">Alamat manual, ADB komputer, Pilihan pembangun</string> + <string name="gate_use_wireless_instead">Gunakan Debugging Wayarles sebaliknya</string> + <string name="gate_some_roms">Sesetengah ROM turut menambah suis itu. Cari “proses anak” dalam Pilihan pembangun.</string> + <string name="gate_manual_hint">Jika pemberitahuan tidak berfungsi, masukkan alamat dan kod tetingkap timbul itu sendiri.</string> + <string name="gate_enter_manually">Masukkan alamat secara manual</string> + <string name="gate_from_computer">Daripada komputer dengan ADB:</string> + <string name="gate_copy_command">Salin arahan</string> + <string name="gate_choose_display">Pilih paparan mana untuk membuka Tetapan Android.</string> + <string name="gate_main_screen">Skrin utama</string> + <string name="wadb_finding_port">Digandingkan. Mencari port Debugging Wayarles…</string> + <string name="wadb_enter_port">Digandingkan. Masukkan port daripada Tetapan.</string> + <string name="wadb_applying">Peranti dijumpai. Menggunakan tetapan…</string> + <string name="wadb_apply_failed">Tidak dapat digunakan secara automatik. Semak alamat di bawah dan cuba lagi. %1$s</string> + <string name="wadb_title_wireless">Debugging Wayarles</string> + <string name="wadb_title_done">Had proses anak dikemas kini</string> + <string name="wadb_title">Tukar had proses anak</string> + <string name="wadb_lede_compact0">Jika pilihan itu tiada, gandingkan melalui Debugging Wayarles.</string> + <string name="wadb_lede_compact1">Digandingkan. Semak alamat IP dan port dalam Debugging Wayarles.</string> + <string name="wadb_lede_done">Had proses anak telah dikemas kini.</string> + <string name="wadb_lede0">Gunakan langkah 2 jika Pilihan pembangun tidak mempunyai tetapan proses anak. Buka Pilihan pembangun → Debugging Wayarles → Gandingkan peranti dengan kod gandingan.</string> + <string name="wadb_lede1">Digandingkan. Gunakan alamat di bawah, atau kemas kini daripada Debugging Wayarles → Alamat IP & Port.</string> + <string name="wadb_lede_done_long">Had proses anak telah dikemas kini melalui Debugging Wayarles.</string> + <string name="wadb_confirmed_on">Android mengesahkan had itu hidup.</string> + <string name="wadb_confirmed_off">Android mengesahkan had itu mati.</string> + <string name="wadb_pair_group">2 · Gandingkan Debugging Wayarles</string> + <string name="wadb_pair_address">Alamat IP & port tetingkap timbul gandingan</string> + <string name="wadb_pin">Kod gandingan (PIN)</string> + <string name="wadb_pin_placeholder">6 digit</string> + <string name="wadb_pair">Gandingkan</string> + <string name="wadb_apply_group">2 · Gunakan melalui Debugging Wayarles</string> + <string name="wadb_connect_address">Alamat IP & port sambungan</string> + <string name="wadb_pair_again">Gandingkan semula</string> + <string name="wadb_turn_off">Matikan had</string> + <string name="wadb_other_ways">Cara lain untuk menukarnya</string> + <string name="wadb_dev_try_first">1 · Pilihan pembangun (cuba dahulu)</string> + <string name="wadb_restrict_on">Tetapkan “Sekat proses anak” hidup.</string> + <string name="wadb_restrict_off">Tetapkan “Sekat proses anak” mati.</string> + <string name="wadb_computer_last">3 · ADB komputer (pilihan akhir)</string> + <string name="wadb_only_if">Hanya jika Debugging Wayarles tidak tersedia.</string> + <string name="wadb_computer_group">3 · ADB komputer (pilihan akhir)</string> + <string name="ctrl_osc_hint">Bila kawalan sentuh muncul dalam sesi</string> + <string name="ctrl_color">Warna</string> + <string name="ctrl_color_hint">Rona butang pada skrin</string> + <string name="ctrl_custom">Tersuai</string> + <string name="ctrl_opacity">Kelegapan</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">Saiz butang</string> + <string name="ctrl_size_hint" formatted="false">100% ialah saiz standard</string> + <string name="ctrl_stick_click">Klik stik</string> + <string name="ctrl_stick_click_hint">Ketik dua kali dan tahan stik untuk L3 atau R3</string> + <string name="ctrl_adaptive">Stik adaptif</string> + <string name="ctrl_rumble">Getaran permainan</string> + <string name="ctrl_steam_button">Butang Steam</string> + <string name="ctrl_qam_button">Butang QAM</string> + <string name="ctrl_keyboard_button">Butang papan kekunci</string> + <string name="ctrl_select_steam">Tekan Select untuk membuka menu Steam</string> + <string name="ctrl_select_steam_hint">Sebagai ganti View/Select.</string> + <string name="setup_animations">Animasi</string> + <string name="ctrl_adaptive_hint">Stik muncul di tempat anda sentuh dan tersembunyi apabila dilepaskan</string> + <string name="ctrl_layout">Susun atur</string> + <string name="ctrl_layout_custom">Kedudukan tersuai disimpan</string> + <string name="ctrl_layout_default">Disesuaikan dengan skrin ini</string> + <string name="ctrl_edit">Sunting</string> + <string name="ctrl_reset">Tetapkan semula</string> + <string name="ctrl_mapping">Pemetaan butang</string> + <string name="ctrl_mapping_default">Setiap butang menghantar inputnya sendiri</string> + <string name="ctrl_mapping_count">%1$d daripada %2$d butang dipetakan semula</string> + <string name="ctrl_configure">Konfigurasikan</string> + <string name="ctrl_reset_all">Tetapkan semula pengawal</string> + <string name="ctrl_reset_all_hint">Memulihkan semua tetapan pengawal dan susun atur</string> + <string name="ctrl_mapping_lede">Apa yang setiap butang pada skrin hantar kepada permainan. Tersembunyi mengalih keluar butang itu.</string> + <string name="ctrl_onscreen_buttons">Butang pada skrin</string> + <string name="ctrl_defaults">Lalai</string> + <string name="ctrl_reset_mapping">Tetapkan semula pemetaan</string> + <string name="content_android_apps">Apl Android</string> + <string name="content_apps_count">%1$d apl</string> + <string name="content_no_android_apps">Tiada apl Android boleh dilancarkan dijumpai.</string> + <string name="content_desktop_installed">● Desktop dipasang</string> + <string name="content_desktop_first_open">Dipasang pada pembukaan pertama</string> + <string name="content_available">Tersedia untuk dipasang</string> + <string name="content_not_installed">Belum dipasang.</string> + <string name="content_not_installed_suffix">%1$s · belum dipasang</string> + <string name="content_open_named">Buka %1$s</string> + <string name="content_games">Permainan</string> + <string name="content_choose_roms">Pilih folder ROM.</string> + <string name="content_retroarch_roms">Semak imbas ke /root/ROMs dalam RetroArch.</string> + <string name="content_add_roms">Tambah permainan %1$s ke ROMs/%2$s.</string> + <string name="content_install_named">Pasang %1$s</string> + <string name="content_stop_to_install">Hentikan sesi untuk memasang</string> + <string name="content_runtime_required">Masa jalan diperlukan</string> + <string name="content_loading_details">Memuatkan butiran pemasangan…</string> + <string name="content_details_unavailable">Butiran pemasangan tidak tersedia. Cuba lagi kemudian.</string> + <string name="content_needs_runtime">Pasang masa jalan Linux daripada Persediaan dahulu.</string> + <string name="content_stop_first">Hentikan sesi yang berjalan untuk memasang apl desktop.</string> + <string name="content_install_into">Memasang %1$s ke dalam desktop Linux.</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">Menyembunyikannya mengalih keluarkannya daripada Desktop. Failnya kekal dalam masa jalan Linux.</string> + <string name="content_rom_gone">Permainan itu tidak lagi berada dalam folder ROM.</string> + <string name="content_desktop_system">Desktop · %1$s</string> + <string name="content_launch_in">Lancarkan dalam %1$s</string> + <string name="content_also_in">Juga dalam %1$s</string> + <string name="content_page_gone">Halaman itu telah hilang. Pilih bahagian di sebelah kiri.</string> + <string name="content_open_desktop">Buka desktop</string> + <string name="content_install_open_desktop">Pasang & buka desktop</string> + <string name="desktop_remove_title">Nyahpasang desktop</string> + <string name="desktop_remove_text">Alih keluar desktop dan apl terbundlenya. Kekalkan permainan, apl ditambah, tetapan dan pustaka kongsi.</string> + <string name="desktop_removing">Mengalih keluar desktop</string> + <string name="desktop_removed">Desktop dialih keluar</string> + <string name="desktop_cleaning_legacy">Mengalih keluar fail LXQt lama</string> + <string name="desktop_removal_unknown">Kemas kini DroidDeck sebelum mengalih keluar versi desktop ini.</string> + <string name="content_linux_desktop">Desktop Linux</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox dan emulator anda</string> + <string name="games_empty">Permainan yang anda pasang dalam Steam muncul di sini selepas sesi.</string> + <string name="games_play_steam">Main Steam</string> + <string name="games_launch_settings">Tetapan pelancaran</string> + <string name="games_launch">Lancarkan</string> + <string name="games_files">Fail permainan</string> + <string name="games_prefix">Prefix Proton</string> + <string name="games_saves_looking">Mencari simpanan…</string> + <string name="games_saves_none">Tiada folder simpanan dijumpai mengikut nama. Eksport mengambil seluruh folder pengguna.</string> + <string name="games_saves_folders">%1$d folder simpanan</string> + <string name="games_saves_summary">%1$s · %2$d fail · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">Urus simpanan</string> + <string name="games_saves">Simpanan permainan</string> + <string name="games_import_saves">Import simpanan…</string> + <string name="games_export_gamehub">Eksport sebagai zip GameHub…</string> + <string name="games_export_winlator">Eksport sebagai zip Winlator…</string> + <string name="games_open_save_folder">Buka folder simpanan</string> + <string name="games_never_played">Belum pernah dimainkan</string> + <string name="games_runtime_busy_percent">Masa jalan sibuk · %1$d%%</string> + <string name="games_runtime_busy">Masa jalan sibuk</string> + <string name="games_runtime_first_play">Masa jalan dipasang pada Main pertama</string> + <string name="games_runtime_update">Kemas kini masa jalan tersedia</string> + <string name="games_runtime_ready">● Masa jalan sedia</string> + <string name="games_last_played">Terakhir dimainkan %1$s</string> + <string name="games_internal">Storan dalaman</string> + <string name="games_added">Permainan ditambah</string> + <string name="games_custom">Permainan tersuai</string> + <string name="games_store_storage">Storan permainan</string> + <string name="gpu_auto_hint">Pasangan terbaik untuk GPU ini, dikemas kini sentiasa</string> + <string name="gpu_manual_hint">Pilih pasangan sendiri</string> + <string name="gpu_checking">Menyemak pemacu…</string> + <string name="gpu_none_listed">Belum ada pemacu disenaraikan. Segarkan untuk mencarinya.</string> + <string name="gpu_hide_other">Sembunyikan pemacu untuk GPU lain</string> + <string name="gpu_show_other">Papar pemacu untuk GPU lain</string> + <string name="gpu_advanced">Lanjutan</string> + <string name="gpu_set_with_display">Ditetapkan bersama pemacu paparan: %1$s membawa kedua-duanya.</string> + <string name="gpu_set_with_runtime">Ditetapkan bersama pemacu masa jalan: %1$s membawa kedua-duanya.</string> + <string name="gpu_runtime_hint">Digunakan oleh permainan dan desktop.</string> + <string name="gpu_runtime_default">Lalai masa jalan</string> + <string name="gpu_display_hint">Melukis bingkai ke skrin. Berkuat kuasa selepas DroidDeck dimulakan semula.</string> + <string name="gpu_auto_by_gpu">Auto - dipilih mengikut GPU</string> + <string name="gpu_manage">Muat turun, import atau padam…</string> + <string name="gpu_in_use_caps">SEDANG DIGUNAKAN</string> + <string name="gpu_builtin">Pemacu terbina dalam DroidDeck</string> + <string name="gpu_matched_pair">Pemacu masa jalan dan paparan ditetapkan bersama sebagai pasangan sepadan.</string> + <string name="gpu_recommended_for">Disyorkan untuk GPU ini: %1$s.</string> + <string name="gpu_refresh_hint">Segarkan untuk mencari pemacu bagi GPU ini.</string> + <string name="gpu_recommended_caps">DISYORKAN</string> + <string name="gpu_unavailable">Tidak tersedia</string> + <string name="load_runtime">Masa jalan Linux</string> + <string name="load_runtime_detail">Menyediakan masa jalan Linux</string> + <string name="load_session">Sesi</string> + <string name="load_session_detail">Memulakan paparan dan audio</string> + <string name="load_steam">Klien Steam</string> + <string name="load_steam_detail">Menyemak Steam dan alatan keserasiannya</string> + <string name="load_opening_steam">Membuka Steam</string> + <string name="load_opening_steam_detail">Menunggu bingkai pertama Steam</string> + <string name="load_desktop">Desktop</string> + <string name="load_desktop_detail">Menyediakan desktop</string> + <string name="load_opening">Membuka</string> + <string name="load_opening_detail">Menunggu bingkai pertama</string> + <string name="load_starting_steam">Memulakan Steam</string> + <string name="load_ended">Sesi telah tamat</string> + <string name="load_step">Langkah %1$d daripada %2$d</string> + <string name="load_steam_paused">Steam dijeda</string> + <string name="deck_tab_grips">Pemegang</string> + <string name="deck_tab_left">Pad kiri</string> + <string name="deck_tab_right">Pad kanan</string> + <string name="deck_tab_both">Kedua-dua</string> + <string name="deck_tab_gamepad">Gamepad</string> + <string name="deck_surface_stick">Stik</string> + <string name="deck_surface_trackpad">Trackpad</string> + <string name="deck_surface_switch">Tukar antara stik dan trackpad</string> + <string name="deck_more_controls">Lagi kawalan Deck</string> + <string name="deck_panels">PANEL</string> + <string name="deck_close">Tutup kawalan skrin kedua</string> + <string name="deck_open_steam_menu">Buka menu Steam</string> + <string name="deck_left_upper">Kiri · atas</string> + <string name="deck_left_lower">Kiri · bawah</string> + <string name="deck_right_upper">Kanan · atas</string> + <string name="deck_right_lower">Kanan · bawah</string> + <string name="deck_click">Klik</string> + <string name="deck_right_trackpad">Trackpad kanan</string> + <string name="deck_left_trackpad">Trackpad kiri</string> + <string name="second_trackpad">Trackpad</string> + <string name="second_show_keyboard">Papar papan kekunci</string> + <string name="second_hide_keyboard">Sembunyikan papan kekunci</string> + <string name="second_terminal">Terminal Linux</string> + <string name="second_terminal_not_ready">Sesi Linux belum sedia untuk terminal.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Permainan</string> + <string name="rail_desktop">Desktop</string> + <string name="rail_store">Kedai</string> + <string name="rail_components">Komponen</string> + <string name="rail_apps">Apl</string> + <string name="rail_setup">Persediaan</string> + <string name="rail_updates">Kemas kini</string> + <string name="rail_percent">%1$d%%</string> + <string name="rail_resume_named">Sambung %1$s</string> + <string name="roms_text">Muncul sebagai /root/ROMs dalam setiap sesi. Storan dalaman berada di /root/Storage.</string> + <string name="roms_none">Tiada folder dipilih</string> + <string name="roms_applies">Berkuat kuasa pada sesi seterusnya. Kad SD berfungsi.</string> + <string name="roms_choose">Pilih folder</string> + <string name="roms_change">Tukar folder</string> + <string name="proton_lede">Pasang binaan Proton ARM64, kemudian pilihnya mengikut permainan dalam Steam → Properties → Compatibility.</string> + <string name="proton_stop_first">Hentikan sesi yang berjalan untuk memasang atau mengalih keluar alat keserasian.</string> + <string name="proton_available">Binaan tersedia</string> + <string name="proton_installed">%1$s dipasang</string> + <string name="proton_queued">Tertangguh daripada permintaan terdahulu</string> + <string name="proton_not_installed">Belum dipasang</string> + <string name="proton_update_available">Kemas kini tersedia → %1$s</string> + <string name="proton_remove_older">Alih keluar versi %1$s lama</string> + <string name="proton_remove_older_body">Mengalih keluar %1$s. Permainan yang ditetapkan ke versi ini perlu ditukar ke Proton lain.</string> + <string name="proton_remove_body">Permainan yang ditetapkan ke versi ini perlu ditukar ke Proton lain.</string> + <string name="proton_install_now">Pasang sekarang</string> + <string name="proton_cancel_queued">Batalkan yang tertangguh</string> + <string name="pckb_android">Papan kekunci Android</string> + <string name="pckb_hide">Sembunyi ✕</string> + <string name="main_launch_named">Lancarkan %1$s</string> + <string name="main_choose_screen">Pilih skrin. Kedua: %1$s.</string> + <string name="main_secondary_gone">Paparan kedua tidak lagi tersedia.</string> + <string name="main_primary">Skrin utama</string> + <string name="main_secondary">Skrin kedua</string> + <string name="widgets_prev_tab">Tab sebelumnya</string> + <string name="widgets_next_tab">Tab seterusnya</string> + <string name="widgets_on">Hidup</string> + <string name="widgets_all_n">Semua %1$d</string> + <string name="widgets_none">Tiada</string> + <string name="widgets_n_of_m">%1$d daripada %2$d</string> + <string name="widgets_off">Mati</string> + <string name="screen_bottom">Skrin bawah</string> + <string name="game_env_applies">Berkuat kuasa pada pelancaran permainan seterusnya. Profil permainan mengatasi profil kongsi.</string> + <string name="game_env_other">Permainan lain mengikut ID apl…</string> + <string name="game_env_done">Selesai</string> + <string name="game_env_default">Lalai</string> + <string name="game_env_off">Mati</string> + <string name="game_env_change">Tukar…</string> + <string name="game_env_change_title">Tukar pemboleh ubah</string> + <string name="game_env_restore_default">Kembali ke lalai</string> + <string name="game_env_eyebrow">Permainan Proton</string> + <string name="pip_title">Gambar dalam gambar</string> + <string name="pip_auto">Masuk ke PiP secara automatik</string> + <string name="unfolded_controls">Kawalan pada skrin terbuka</string> + <string name="unfolded_controls_hint">Terbuka, permainan terletak di atas trackpad dan pemegang belakang Steam Deck. Dilipat, permainan menjadi skrin penuh.</string> + <string name="pip_suspended">Digantung</string> + <string name="pip_not_ready">Gambar dalam gambar tersedia sebaik sahaja sesi sedia.</string> + <string name="pip_permission">Benarkan gambar dalam gambar untuk DroidDeck dalam tetapan Android.</string> + <string name="pip_unavailable">Android tidak dapat membuka gambar dalam gambar.</string> + <string name="game_shortcut">Pintasan</string> + <string name="game_shortcut_add">Tambah ke skrin utama</string> + <string name="game_shortcut_choose">Pilih permainan</string> + <string name="game_shortcut_scanning">Mengimbas permainan dipasang…</string> + <string name="game_link_copy">Salin pautan pelancaran</string> + <string name="game_link_copied">Pautan pelancaran disalin</string> + <string name="game_shortcut_unsupported">Pelancar ini tidak dapat menambah pintasan. Gunakan Salin pautan pelancaran sebaliknya.</string> + <string name="game_link_invalid">Pautan pelancaran permainan tidak sah.</string> + <string name="game_link_missing">Permainan ini tidak lagi berada dalam pustaka DroidDeck anda.</string> + <string name="game_link_session_busy">Hentikan sesi semasa sebelum melancarkan permainan Steam.</string> + <string name="game_link_failed">Tidak dapat menghantar permainan ke Steam. Cuba lancarkan semula.</string> + <string name="setup_retry_removal">Cuba semula pengalihan keluar</string> + <string name="runtime_removing">Mengalih keluar</string> + <string name="runtime_removal_incomplete">Pengalihan keluar tidak selesai</string> + <string name="game_file_export">Eksport fail permainan</string> + <string name="game_file_sync">Segerakkan semua fail permainan ke folder</string> + <string name="game_file_stop_sync">Hentikan penyegerakan fail permainan</string> + <string name="game_file_folder">Pilih folder untuk fail permainan frontend</string> + <string name="game_file_exported">Fail permainan disimpan ke %1$s</string> + <string name="game_frontend_files">Fail frontend</string> + + <string name="main_wait_runtime">Tunggu operasi masa jalan selesai</string> + <string name="main_wait_proton_install">Tunggu pemasangan alat keserasian selesai</string> + <string name="main_wait_desktop_install">Tunggu pemasangan apl desktop selesai</string> + <string name="main_wait_install_game">Tunggu pemasangan selesai sebelum melancarkan permainan</string> + <string name="main_saves_close_session">Tutup sesi Steam dahulu, supaya permainan tidak dapat menyimpan di atas import</string> + <string name="main_saves_importing">Mengimport ke %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="other">Mengimport %1$d fail daripada %2$s ke %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="other">Mengimport %1$d fail daripada %2$s ke %3$s. Simpanan lama disandarkan ke Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Pilih zip simpanan untuk %1$s</string> + <string name="main_saves_exporting">Mengeksport %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="other">Mengeksport %1$d fail sebagai %2$s: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Pilih tempat menyimpan %1$s (%2$s)</string> + <string name="main_action_failed">%1$s gagal: %2$s</string> + <string name="main_no_logs_yet">Belum ada log sesi: jalankan sesi dahulu.</string> + <string name="main_no_logs_to_clear">Tiada log sesi untuk dikosongkan.</string> + <plurals name="main_logs_cleared"> + <item quantity="other">%1$d folder log sesi dikosongkan.</item> + </plurals> + <string name="main_wadb_pair_failed">Gandingan Debugging Wayarles gagal</string> + <string name="main_wadb_command_failed">Arahan Debugging Wayarles gagal</string> + <string name="main_wadb_unavailable">Debugging Wayarles tidak tersedia</string> + <string name="main_adb_copied">Arahan ADB disalin</string> + <string name="main_pick_roms">Pilih folder ROM</string> + <string name="main_non_adreno_title">Bukan GPU Adreno</string> + <string name="main_non_adreno_text">Turnip menyokong GPU Adreno. Pada %1$s, Steam mungkin memaparkan skrin hitam. Muat turun: %2$.0f MB.</string> + <string name="main_install_anyway">Pasang juga</string> + <string name="main_remove_runtime_title">Alih keluar masa jalan Linux</string> + <string name="main_remove_runtime_text">Ini memadam masa jalan, klien Steam di dalamnya, dan setiap permainan yang dipasang di sana.</string> + <string name="main_applied">Digunakan: %1$s</string> + <string name="android_app_open_failed_primary">Tidak dapat membuka %1$s pada skrin utama</string> + <string name="android_app_open_failed_on">Tidak dapat membuka %1$s pada %2$s</string> + <string name="android_app_open_failed_display">Tidak dapat membuka %1$s pada paparan %2$d</string> + <string name="comp_busy_importing">Mengimport</string> + <string name="comp_busy_swapping">Menukar</string> + <string name="comp_busy_restoring">Memulihkan</string> + <string name="comp_busy_cancelling">Membatalkan</string> + <string name="comp_busy_deleting">Memadam</string> + <string name="comp_cannot_read">tidak dapat membaca fail</string> + <string name="comp_imported">%1$s diimport</string> + <string name="comp_swap_cancelled">Penukaran yang menunggu telah dibatalkan.</string> + <string name="comp_pick_wcp">Pilih pakej komponen (-linux .wcp)</string> + <string name="comp_nightlies_unreachable">Nightlies tidak dapat dihubungi</string> + <string name="comp_stored">%1$s disimpan</string> + <string name="comp_download_failed">Muat turun gagal: %1$s</string> + <string name="drivers_pick_linux">Pilih pemacu masa jalan Linux (zip -Linux atau gabungan Android + Linux)</string> + <string name="drivers_pick_android">Pilih pemacu paparan (zip AdrenoTools atau gabungan Android + Linux)</string> + <string name="drivers_pick_any">Pilih zip pemacu atau gabungan Android + Linux</string> + <string name="main_pkg_problem">%1$s: %2$s</string> + <string name="main_removing_named">Mengalih keluar %1$s…</string> + <string name="main_forgetting_named">Melupakan %1$s…</string> + <string name="main_pick_game_storage">Pilih folder storan permainan</string> + <string name="main_pick_decky_zip">Pilih ZIP pemalam Decky</string> + <string name="main_storage_not_usable">Tidak dapat digunakan: %1$s</string> + <string name="main_running_program">Program</string> + <string name="main_removing_runtime">Mengalih keluar masa jalan Linux</string> + <string name="main_settings_main_screen">Tidak dapat membuka Tetapan pada skrin bawah; dibuka pada skrin utama.</string> + <string name="main_open_dev_options">Buka Pilihan pembangun dalam Tetapan</string> + + <string name="upd_unknown_apps_failed">Tidak dapat membuka tetapan “Pasang apl tidak diketahui” Android</string> + <string name="upd_couldnt_check">Tidak dapat menyemak: %1$s</string> + <string name="upd_no_download">Binaan ini tidak mempunyai muat turun untuk salinan DroidDeck ini</string> + <string name="upd_stop_first">Hentikan sesi yang berjalan dahulu, kemudian kemas kini</string> + <string name="upd_stage_downloading">Memuat turun</string> + <string name="upd_stage_installing">Memasang</string> + <string name="upd_stage_confirm">Menunggu pengesahan anda</string> + <string name="upd_failed">Kemas kini gagal</string> + <string name="upd_not_offered">Android tidak menawarkan untuk memasang kemas kini</string> + + <string name="decky_install_failed">Pemasangan Decky gagal: %1$s</string> + <string name="decky_plugin_starting">Memulakan import pemalam…</string> + <string name="decky_plugin_failed">Import pemalam gagal: %1$s</string> + <string name="decky_plugin_installed">Pemalam dipasang. Mulakan semula Steam untuk memuatkannya.</string> + + <string name="drivers_not_checked">Belum disemak - ketik segar semula untuk mencari pemacu baharu</string> + <string name="drivers_bundle_sets_display">Android + Linux: menetapkan pemacu paparan sekali</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: menetapkan pemacu masa jalan sekali</string> + <string name="drivers_runtime_builtin">Turnip yang terbina dalam masa jalan</string> + <string name="drivers_system_vulkan">Vulkan sistem: tiada binaan terbundle untuk GPU ini</string> + <string name="drivers_bundled">%1$s (terbundle)</string> + <string name="drivers_manual">Manual</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">Hanya satu separuh diterbitkan sekarang</string> + <string name="drivers_pair_size">%1$.0f MB untuk kedua-duanya</string> + <string name="drivers_none_to_set">Tiada pemacu untuk ditetapkan: %1$s</string> + <string name="drivers_checking_latest">Menyemak pemacu terkini…</string> + <string name="drivers_up_to_date_check_failed">Terkini (tidak dapat disemak: %1$s)</string> + <string name="drivers_recommended_unpublished">Pemacu yang disyorkan tidak diterbitkan sekarang</string> + <string name="drivers_downloading_pair">Memuat turun %1$s %2$s…</string> + <string name="drivers_updated_to">Dikemas kini ke %1$s %2$s.</string> + <string name="drivers_updated_to_restart">Dikemas kini ke %1$s %2$s. Pemacu paparan berkuat kuasa selepas DroidDeck dimulakan semula.</string> + <string name="drivers_switched_to">Ditukar ke %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Ditukar ke %1$s %2$s. Pemacu paparan berkuat kuasa selepas DroidDeck dimulakan semula.</string> + <string name="drivers_using">Menggunakan %1$s %2$s.</string> + <string name="drivers_using_restart">Menggunakan %1$s %2$s. Pemacu paparan berkuat kuasa selepas DroidDeck dimulakan semula.</string> + <string name="drivers_not_a_driver">Bukan zip pemacu: bukan pemacu AdrenoTools, Turnip -Linux, atau gabungan Android + Linux</string> + <string name="drivers_import_failed">Import gagal: %1$s</string> + <string name="drivers_imported_unnamed">Pemacu diimport</string> + <string name="drivers_using_bundle">Menggunakan %1$s sebagai pemacu masa jalan dan paparan.</string> + <string name="drivers_using_bundle_restart">Menggunakan %1$s sebagai pemacu masa jalan dan paparan. Pemacu paparan berkuat kuasa selepas DroidDeck dimulakan semula.</string> + <string name="drivers_deleted_bundle">%1$s dipadam (kedua-dua pemacu)</string> + <string name="drivers_deleted">%1$s dipadam</string> + <string name="drivers_release_status">Terkini: %1$s · disemak %2$s</string> + <string name="drivers_release_status_failed">Terkini: %1$s · %2$s tidak dapat dihubungi · disemak %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="other">%1$d minit lalu</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="other">%1$d jam lalu</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="other">%1$d hari lalu</item> + </plurals> + <string name="drivers_checking_sources">Menyemak Banners-Turnip, WinNative dan DroidDeck…</string> + <string name="drivers_check_failed">semakan gagal</string> + <string name="drivers_installed_pick_either">%1$s dipasang - pilih dalam mana-mana senarai untuk menetapkan kedua-dua pemacu</string> + <string name="drivers_installed_pick">%1$s dipasang - pilih dalam menu</string> + + <string name="session_wait_removal">Tunggu pengalihan keluar masa jalan selesai</string> + <string name="session_title_game">Permainan</string> + <string name="session_touch_auto_touchpad">touchpad</string> + <string name="session_touch_auto_direct">langsung</string> + <string name="session_swap_failed">Penukaran gagal: %1$s</string> + <string name="session_profile_save_failed">Penyimpanan profil gagal: %1$s</string> + <string name="session_desktop_paused">Desktop dijeda</string> + <string name="session_named_paused">%1$s dijeda</string> + <string name="session_program_paused">Program dijeda</string> + <string name="session_starting_desktop">Memulakan desktop</string> + <string name="session_starting_named">Memulakan %1$s</string> + <string name="session_starting_program">Memulakan program</string> + <string name="session_no_logs">Tiada log untuk sesi ini.</string> + <string name="session_logs_archive_failed">Tidak dapat membuat arkib log sesi.</string> + <string name="session_logs_share_failed">Tidak dapat berkongsi log sesi.</string> + <string name="session_starting_session">Memulakan sesi…</string> + <string name="session_runtime_downloading">memuat turun masa jalan Linux</string> + <string name="session_runtime_downloading_mb">memuat turun masa jalan Linux · %1$d daripada %2$d MB</string> + <string name="session_runtime_checking">menyemak masa jalan Linux</string> + <string name="session_runtime_unpacking">menyahpak masa jalan Linux</string> + <string name="session_desktop_downloading">memuat turun desktop</string> + <string name="session_desktop_downloading_mb">memuat turun desktop · %1$d daripada %2$d MB</string> + <string name="session_desktop_checking">menyemak desktop</string> + <string name="session_desktop_unpacking">menyahpak desktop</string> + <string name="session_proton_downloading">memuat turun Proton Experimental (ARM64)</string> + <string name="session_proton_downloading_mb">memuat turun Proton Experimental (ARM64) · %1$d daripada %2$d MB</string> + <string name="session_proton_checking">menyemak Proton Experimental (ARM64)</string> + <string name="session_proton_unpacking">menyahpak Proton Experimental (ARM64)</string> + <string name="session_runtime_catalog_unreachable">Tidak dapat menghubungi katalog masa jalan. Semak sambungan dan tekan Main semula.</string> + <string name="session_runtime_install_failed">Masa jalan Linux tidak berjaya dipasang. Semak masa jalan dalam Persediaan dan tekan Main semula.</string> + <string name="session_desktop_install_failed">Desktop tidak berjaya dipasang (%1$s). Semak sambungan dan tekan Desktop semula.</string> + <string name="session_ended_named">%1$s terhenti secara tidak dijangka. Kongsi log dengan laporan pepijat, atau cuba lagi.</string> + <string name="session_ended_program">Program terhenti secara tidak dijangka. Kongsi log dengan laporan pepijat, atau cuba lagi.</string> + <string name="session_ended_desktop">Desktop terhenti secara tidak dijangka. Kongsi log dengan laporan pepijat, atau cuba lagi.</string> + <string name="session_ended_detail">Status keluar %1$d · %2$s</string> + <string name="session_ended_no_log">tiada log</string> + <plurals name="session_hint_enosys"> + <item quantity="other">Log menunjukkan %1$d ralat “Function not implemented”: pecutan seccomp proot gagal pada pembantu di peranti ini. Cuba %2$s → “%3$s” (atau “%4$s”) dan mulakan semula.</item> + </plurals> + + <string name="mode_decky_plugins">Pemalam</string> + <string name="mode_decky_plugins_hint">Pasang ZIP pemalam untuk sesi Steam seterusnya.</string> + <string name="mode_decky_plugin_downloading">Memuat turun…</string> + <string name="mode_decky_plugin_install_zip">Pasang daripada ZIP</string> + <string name="store_cat_games">Permainan</string> + <string name="store_cat_audio_video">Audio & Video</string> + <string name="store_cat_graphics">Grafik</string> + <string name="store_cat_internet">Internet</string> + <string name="store_cat_office">Pejabat</string> + <string name="store_cat_developer">Pembangun</string> + <string name="store_cat_education">Pendidikan</string> + <string name="store_cat_science">Sains</string> + <string name="store_cat_system">Sistem</string> + <string name="store_cat_utilities">Utiliti</string> + <string name="store_section_popular">Popular</string> + <string name="store_section_trending">Tular</string> + <string name="store_section_new">Baharu</string> + <string name="store_section_updated">Dikemas kini</string> + <string name="store_section_loading">memuatkan…</string> + <string name="store_flatpak_setup">Persediaan Flatpak</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Tidak dapat menyemak kemas kini di Flathub</string> + <string name="content_rom_installed">dipasang</string> + <string name="content_rom_folder">folder</string> + <string name="theme_graphite">Graphite</string> + <string name="theme_graphite_detail">Permukaan graphite, dengan bebola biru logo untuk semua yang dipilih, difokus atau ditekan.</string> + <string name="theme_paper">Kertas pada hitam</string> + <string name="theme_paper_detail">Putih sebagai utama, biru ikon sebagai isyarat untuk garis, gelang dan titik.</string> + <string name="theme_icon_blue">Biru ikon</string> + <string name="theme_icon_blue_detail">Latar hitam, teks putih, biru untuk semua yang dipilih, difokus atau ditekan.</string> + <string name="theme_electric">Navy elektrik</string> + <string name="theme_electric_detail">Biru lebih cerah, pada permukaan yang lebih cenderung navy daripada kelabu.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Space</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <string name="game_sync_err_runtime">Pasang masa jalan dahulu</string> + <string name="game_sync_err_steam_library">Pustaka Steam tidak tersedia</string> + <string name="game_sync_err_previous_sd">Pustaka SD terdahulu tidak tersedia</string> + <string name="game_sync_err_game_storage">Storan permainan tidak tersedia</string> + <string name="game_sync_err_added_games">Folder permainan yang ditambah tidak tersedia</string> + <string name="game_sync_err_manifests">Manifest Steam tidak tersedia</string> + <string name="content_system_many">Banyak sistem</string> + <string name="fm_drive_internal">Dalaman</string> + <string name="fm_drive_linux">Sistem Linux</string> + <string name="fm_drive_sd">Kad SD</string> + <string name="fm_drive_storage">Storan</string> + <string name="fm_select">Pilih</string> + <string name="fm_properties">Ciri</string> + <string name="fm_favorite_add">Tambah ke Kegemaran</string> + <string name="fm_favorite_remove">Alih keluar daripada Kegemaran</string> + <string name="fm_rename">Namakan semula</string> + <string name="fm_copy">Salin</string> + <string name="fm_cut">Potong</string> + <string name="fm_actions">Tindakan</string> + <string name="fm_favorites">Kegemaran</string> + <string name="fm_pin_current">Sematkan folder semasa</string> + <string name="fm_no_favorites">Belum ada kegemaran - sematkan folder dengan menu ⋮-nya untuk melompat ke sini dengan pantas.</string> + <string name="fm_unpin_favorite">Alih keluar daripada kegemaran</string> + <string name="fm_size_zero">0 bait</string> + <string name="fm_unit_bytes">bait</string> + <string name="fm_prop_location">Lokasi</string> + <string name="fm_prop_type">Jenis</string> + <string name="fm_prop_size">Saiz</string> + <string name="fm_prop_modified">Diubah suai</string> + <string name="fm_type_folder">Folder</string> + <string name="fm_type_file">Fail</string> + <string name="fm_type_ext_file">Fail %1$s</string> + <string name="fm_read_only">Baca sahaja</string> + <string name="fm_read_only_hint">Menghalang permainan (dan Wine) daripada menulis ganti atau memadam fail ini.</string> + <string name="fm_read_only_failed">Tidak dapat menukar Baca sahaja</string> + <string name="fm_hidden">Tersembunyi</string> + <string name="fm_hidden_unsupported">Storan ini tidak dapat menyimpan bendera tersembunyi.</string> + <string name="fm_hidden_hint">Menandakan fail tersembunyi dalam Windows (atribut tersembunyi Wine).</string> + <string name="fm_hidden_failed">Tidak dapat menukar Tersembunyi pada storan ini</string> + <string name="fm_done">Selesai</string> + <string name="fm_op_deleting">Memadam...</string> + <string name="fm_delete_failed">Padam gagal</string> + <string name="fm_op_moving">Mengalihkan...</string> + <string name="fm_op_copying">Menyalin...</string> + <string name="fm_op_moving_item">Mengalihkan - %1$s</string> + <string name="fm_op_copying_item">Menyalin - %1$s</string> + <string name="fm_op_moving_batch">Mengalihkan %1$d/%2$d - %3$s</string> + <string name="fm_op_copying_batch">Menyalin %1$d/%2$d - %3$s</string> + <string name="fm_paste_failed">%1$d selesai, %2$d gagal</string> + <string name="fm_paste_skipped">%1$d selesai, %2$d dilangkau</string> + <plurals name="fm_paste_moved"> + <item quantity="other">%1$d item dialihkan</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="other">%1$d item disalin</item> + </plurals> + <string name="fm_already_exists">“%1$s” sudah wujud</string> + <string name="fm_op_renaming">Menamakan semula...</string> + <string name="fm_rename_failed">Namakan semula gagal</string> + <string name="fm_op_creating_folder">Mencipta folder...</string> + <string name="fm_create_folder_failed">Tidak dapat mencipta folder</string> + <string name="fm_new_folder">Folder Baharu</string> + <string name="fm_folder_name">Nama folder</string> + <string name="fm_create">Cipta</string> + <string name="fm_new_name">Nama baharu</string> + <string name="fm_delete_title">Padam?</string> + <string name="fm_delete_text">Padam “%1$s” secara kekal?</string> + <plurals name="fm_delete_items_title"> + <item quantity="other">Padam %1$d item?</item> + </plurals> + <string name="fm_cannot_undo">Ini tidak boleh dibuat asal.</string> + <plurals name="fm_and_more"> + <item quantity="other">…dan %1$d lagi</item> + </plurals> + <string name="fm_op_deleting_batch">Memadam %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="other">%1$d tidak dapat dipadam</item> + </plurals> + <string name="fm_conflict_merge_hint">Gabung menambah dan menggantikan fail di dalam folder sedia ada.</string> + <string name="fm_conflict_overwrite_hint">Tulis ganti menggantikan fail sedia ada.</string> + <string name="fm_conflict_apply_all">Gunakan untuk semua konflik</string> + <string name="fm_conflict_merge">Gabung</string> + <string name="fm_conflict_overwrite">Tulis ganti</string> + <string name="fm_conflict_keep_both">Simpan kedua-duanya</string> + <string name="fm_conflict_skip">Langkau</string> + <string name="fm_select_this_folder">Pilih folder ini</string> + <string name="fm_back">Kembali</string> + <string name="fm_drive_not_readable">%1$s dilekapkan tetapi tidak dapat dibaca sekarang</string> + <string name="fm_list_view">Paparan senarai</string> + <string name="fm_grid_view">Paparan grid</string> + <string name="fm_search">Cari</string> + <string name="fm_sort">Susun</string> + <string name="fm_sort_name">Nama</string> + <string name="fm_sort_date">Tarikh diubah suai</string> + <string name="fm_rows_comfortable">Baris selesa</string> + <string name="fm_rows_compact">Baris padat</string> + <string name="fm_hide_hidden">Sembunyikan fail tersembunyi</string> + <string name="fm_show_hidden">Papar fail tersembunyi</string> + <string name="fm_hide_favorites">Sembunyikan kegemaran</string> + <string name="fm_show_favorites">Papar kegemaran</string> + <string name="fm_filter_folder">Tapis folder ini</string> + <string name="fm_free_space">%1$s bebas</string> + <string name="fm_selected_count">%1$d dipilih</string> + <string name="fm_select_none">Tiada</string> + <string name="fm_select_all">Semua</string> + <string name="fm_paste">Tampal</string> + <plurals name="fm_items"> + <item quantity="other">%1$d item</item> + </plurals> + <string name="fm_paste_here">Tampal %1$s di sini</string> + <string name="fm_paste_move_here">Tampal %1$s (alih) di sini</string> + <string name="fm_rail_title">Fail</string> + <string name="fm_rail_storage">STORAN</string> + <string name="fm_rail_quick">PANTAS</string> + <string name="fm_rail_favorites">KEGEMARAN</string> + <string name="fm_rail_downloads">Muat turun</string> + <string name="fm_rail_downloads_short">MuatTrn</string> + <string name="fm_rail_roms">ROM</string> + <string name="fm_rail_session_logs">Log sesi</string> + <string name="fm_rail_pictures">Gambar</string> + <string name="fm_favorite_added">“%1$s” ditambah ke Kegemaran</string> + <string name="fm_favorite_removed">“%1$s” dialih keluar daripada Kegemaran</string> + <string name="fm_empty_directory">Direktori kosong</string> + <string name="fm_up_one_level">Naik satu tahap</string> + <string name="fm_choose_folder">Pilih folder</string> + <string name="fm_choose_file">Pilih fail</string> + <string name="ctrl_tint_steam_blue">Biru Steam</string> + <string name="ctrl_tint_sky">Langit</string> + <string name="ctrl_tint_white">Putih</string> + <string name="ctrl_tint_violet">Violet</string> + <string name="ctrl_tint_green">Hijau</string> + <string name="ctrl_tint_amber">Ambar</string> + <string name="ctrl_tint_red">Merah</string> + <string name="ctrl_tint_pink">Merah jambu</string> + <string name="ctrl_button_a">Butang A</string> + <string name="ctrl_button_b">Butang B</string> + <string name="ctrl_button_x">Butang X</string> + <string name="ctrl_button_y">Butang Y</string> + <string name="ctrl_button_lb">Bumper kiri</string> + <string name="ctrl_button_rb">Bumper kanan</string> + <string name="ctrl_button_lt">Pencetus kiri</string> + <string name="ctrl_button_rt">Pencetus kanan</string> + <string name="ctrl_button_view">Butang View</string> + <string name="ctrl_button_menu">Butang Menu</string> + <string name="ctrl_target_view">View</string> + <string name="ctrl_target_menu">Menu</string> + <string name="ctrl_target_up">D-pad atas</string> + <string name="ctrl_target_down">D-pad bawah</string> + <string name="ctrl_target_left">D-pad kiri</string> + <string name="ctrl_target_right">D-pad kanan</string> + <string name="ctrl_target_hidden">Tersembunyi</string> + <string name="ctrl_editor_drag">Seret untuk alih</string> + <string name="ctrl_editor_snap">Lekat</string> + <string name="ctrl_editor_save">Simpan</string> + <string name="second_mode_none">Tiada</string> + <string name="second_mode_keyboard_trackpad">Papan kekunci + trackpad</string> + <string name="second_mode_terminal">Terminal</string> + <string name="second_mode_deck_controls">Pemegang Deck + trackpad</string> + + <string name="rtinst_removing">Mengalih keluar masa jalan Linux</string> + <plurals name="rtinst_removing_entries"> + <item quantity="other">Mengalih keluar masa jalan Linux · %1$d entri</item> + </plurals> + <string name="rtinst_removed">Masa jalan Linux dialih keluar</string> + <string name="rtinst_removal_failed">Tidak dapat selesai mengalih keluar masa jalan Linux. Cuba semula pengalihan keluar dalam Persediaan.</string> + <string name="rtinst_removing_leftovers">Mengalih keluar baki masa jalan</string> + <string name="rtinst_downloading">Memuat turun</string> + <string name="rtinst_verifying">Mengesahkan</string> + <string name="rtinst_extracting">Mengekstrak</string> + <string name="rtinst_channel_description">Dipaparkan semasa masa jalan Linux dipasang atau dialih keluar</string> + <string name="rtinst_title_installing">Memasang masa jalan Linux</string> + <string name="rtinst_title_removing">Mengalih keluar masa jalan Linux</string> + + <string name="deskpkg_runtime_missing">Masa jalan Linux belum dipasang</string> + <string name="deskpkg_no_checksum">Katalog tidak mempunyai checksum untuk %1$s</string> + <string name="deskpkg_checksum_mismatch">Checksum tidak sepadan - tiada apa yang diubah</string> + <string name="deskpkg_installing">Memasang %1$s</string> + <string name="deskpkg_place_failed">Tidak dapat meletakkan AppImage</string> + <string name="deskpkg_extract_failed">Pengekstrakan gagal</string> + <string name="deskpkg_install_failed">Pemasangan gagal</string> + + <string name="flatpakmgr_setup_failed">Persediaan Flatpak gagal (keluar %1$d)</string> + <string name="flatpakmgr_setup_unfinished">Persediaan Flatpak tidak selesai</string> + <string name="flatpakmgr_busy">Tugas kedai lain sedang berjalan</string> + <string name="flatpakmgr_not_ready">Sediakan Flatpak dahulu</string> + <string name="flatpakmgr_starting">Memulakan</string> + <string name="flatpakmgr_exit_status">Flatpak keluar dengan status %1$d</string> + <string name="flatpakmgr_installing">Memasang %1$s</string> + <string name="flatpakmgr_updating">Mengemas kini %1$s</string> + <string name="flatpakmgr_removing">Mengalih keluar %1$s</string> + <string name="flatpakmgr_installing_step">Memasang %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_updating_step">Mengemas kini %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_removing_step">Mengalih keluar %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_ref_translations">terjemahan</string> + <string name="flatpakmgr_ref_graphics_drivers">pemacu grafik</string> + <string name="flatpakmgr_ref_media_codecs">kodek media</string> + <string name="flatpakmgr_ref_runtime">masa jalan %1$s</string> + <string name="flatpakmgr_ref_runtime_branch">masa jalan %1$s %2$s</string> + + <string name="appimg_too_small">Fail terlalu kecil untuk menjadi AppImage</string> + <string name="appimg_unreadable">Fail tidak dapat dibaca: %1$s</string> + <string name="appimg_not_linux">Ini bukan AppImage (ia bukan program Linux)</string> + <string name="appimg_wrong_arch">AppImage ini dibina bukan untuk ARM64 (aarch64) mahupun PC x86</string> + <string name="appimg_not_appimage">Ini adalah program Linux tetapi bukan AppImage</string> + <string name="appimg_wrong_type">Hanya AppImage jenis 2 boleh diimport (ini jenis %1$d)</string> + <string name="appimg_no_space_import">Ruang bebas tidak mencukupi: import memerlukan kira-kira %1$s</string> + <string name="appimg_no_space_update">Ruang bebas tidak mencukupi: kemas kini memerlukan kira-kira %1$s</string> + <string name="appimg_import_failed">Import gagal</string> + <string name="appimg_gone">AppImage itu sudah hilang</string> + <string name="appimg_update_failed">Kemas kini gagal</string> + <string name="appimg_extracting">Mengekstrak %1$s</string> + <string name="appimg_extract_failed">AppImage tidak dapat diekstrak</string> + <string name="appimg_extract_failed_detail">AppImage tidak dapat diekstrak: %1$s</string> + + <string name="deckymgr_stop_session_loader">Hentikan sesi aktif sebelum memasang Decky Loader</string> + <string name="deckymgr_download_failed">Muat turun Decky Loader gagal</string> + <string name="deckymgr_size_mismatch">Saiz Decky Loader tidak sepadan dengan metadata keluaran</string> + <string name="deckymgr_checksum_download_failed">Tidak dapat memuat turun checksum keluaran</string> + <string name="deckymgr_checksum_missing">Checksum keluaran tiada</string> + <string name="deckymgr_checksum_invalid">Checksum keluaran tidak sah</string> + <string name="deckymgr_checksum_mismatch">Checksum Decky Loader tidak sepadan</string> + <string name="deckymgr_not_elf">Aset keluaran bukan boleh laku Linux 64-bit yang serasi</string> + <string name="deckymgr_session_started_loader">Sesi dimulakan semasa muat turun; hentikan dahulu sebelum memasang Decky Loader</string> + <string name="deckymgr_loader_not_executable">Tidak dapat menandakan PluginLoader sebagai boleh laku</string> + <string name="deckymgr_cef_failed">Tidak dapat membenarkan penyahpepijatan jauh CEF Steam</string> + <string name="deckymgr_stage_failed">Tidak dapat menyediakan PluginLoader</string> + <string name="deckymgr_loader_preserve_failed">Tidak dapat mengekalkan PluginLoader sedia ada</string> + <string name="deckymgr_loader_activate_failed">Tidak dapat mengaktifkan PluginLoader baharu</string> + <string name="deckymgr_stop_session_plugin">Hentikan sesi aktif sebelum memasang pemalam</string> + <string name="deckymgr_loader_required">Pasang Decky Loader sebelum mengimport pemalam</string> + <string name="deckymgr_zip_unreadable">Tidak dapat membaca fail ZIP yang dipilih</string> + <string name="deckymgr_plugins_dir_failed">Tidak dapat mencipta direktori pemalam Decky</string> + <string name="deckymgr_extracting_zip">Mengekstrak ZIP pemalam</string> + <string name="deckymgr_zip_one_folder">ZIP pemalam mesti mengandungi satu folder pemalam peringkat atas</string> + <string name="deckymgr_zip_missing_files">ZIP pemalam tiada plugin.json, package.json atau dist/index.js</string> + <string name="deckymgr_plugin_name_missing">Nama pemalam tiada</string> + <string name="deckymgr_session_started_plugin">Sesi dimulakan semasa pemasangan; hentikan dahulu sebelum memasang pemalam</string> + <string name="deckymgr_plugin_installed">Pemalam dipasang · mulakan semula Steam untuk memuatkannya</string> + <string name="deckymgr_plugin_install_failed">Pemasangan ZIP pemalam gagal</string> + <string name="deckymgr_zip_too_many">ZIP pemalam mengandungi terlalu banyak entri</string> + <string name="deckymgr_zip_invalid_path">ZIP pemalam mengandungi laluan yang tidak sah</string> + <string name="deckymgr_zip_duplicate_paths">ZIP pemalam mengandungi laluan pendua</string> + <string name="deckymgr_zip_unsafe_path">ZIP pemalam mengandungi laluan yang tidak selamat</string> + <string name="deckymgr_folder_failed">Tidak dapat mencipta folder pemalam</string> + <string name="deckymgr_zip_too_large">ZIP pemalam mengembang melebihi had 1 GiB</string> + <string name="deckymgr_bundled_not_executable">Tidak dapat menandakan binari pemalam terbundle sebagai boleh laku</string> + <string name="deckymgr_remote_entry_invalid">Entri remote_binary tidak sah</string> + <string name="deckymgr_remote_name_invalid">Nama binari jauh tidak sah</string> + <string name="deckymgr_remote_url_https">URL binari jauh mesti menggunakan HTTPS</string> + <string name="deckymgr_remote_hash_invalid">Binari jauh %1$s mempunyai cincang SHA-256 yang tidak sah</string> + <string name="deckymgr_bin_dir_failed">Tidak dapat mencipta direktori bin pemalam</string> + <string name="deckymgr_downloading_binary">Memuat turun binari pemalam · %1$s</string> + <string name="deckymgr_binary_download_failed">Tidak dapat memuat turun binari pemalam %1$s</string> + <string name="deckymgr_binary_checksum_mismatch">Checksum binari pemalam tidak sepadan · %1$s</string> + <string name="deckymgr_binary_not_executable">Tidak dapat menandakan binari pemalam sebagai boleh laku · %1$s</string> + <string name="deckymgr_binary_replace_failed">Tidak dapat menggantikan binari pemalam · %1$s</string> + <string name="deckymgr_binary_install_failed">Tidak dapat memasang binari pemalam · %1$s</string> + <string name="deckymgr_folder_taken">Folder pemalam dimiliki oleh pemalam lain</string> + <string name="deckymgr_preserve_failed">Tidak dapat mengekalkan pemalam sedia ada</string> + <string name="deckymgr_activate_failed">Tidak dapat mengaktifkan pemalam yang diimport</string> + <string name="deckymgr_settings_failed">Tidak dapat mengemas kini tetapan pemalam Decky</string> + <string name="deckymgr_restore_failed">Tidak dapat memulihkan pemalam sedia ada</string> + + <string name="adbfix_save_failed">Tidak dapat menyimpan identiti Debugging Wayarles</string> + <string name="adbfix_command_failed">Arahan ADB gagal (%1$d)</string> + <string name="adbfix_not_confirmed">Android tidak mengesahkan had proses anak telah ditukar: %1$s</string> + <string name="adbfix_no_pairing">Tiada gandingan Debugging Wayarles disimpan</string> + <string name="adbfix_enter_address">Masukkan alamat IP & Port Debugging Wayarles semasa</string> + + <string name="adbpair_timed_out">Masa tamat menunggu tetingkap timbul gandingan. Cuba lagi daripada DroidDeck.</string> + <string name="adbpair_discovery_failed_code">Tidak dapat mencari tetingkap timbul gandingan (%1$d). Semak Wi-Fi dihidupkan.</string> + <string name="adbpair_discovery_failed">Tidak dapat mencari tetingkap timbul gandingan: %1$s</string> + <string name="adbpair_six_digits">Masukkan kesemua 6 digit kod</string> + <string name="adbpair_pairing">Menggandingkan…</string> + <string name="adbpair_code_rejected">Kod itu tidak berfungsi. Buka tetingkap timbul gandingan semula dan masukkan kod baharunya.</string> + <string name="adbpair_connecting">Digandingkan. Menyambung…</string> + <string name="adbpair_port_not_found">Digandingkan, tetapi port Debugging Wayarles tidak dijumpai. Biarkan Debugging Wayarles hidup dan cuba lagi.</string> + <string name="adbpair_applying">Menggunakan tetapan…</string> + <string name="adbpair_command_failed">Arahan Debugging Wayarles gagal</string> + <string name="adbpair_channel_name">Persediaan Debugging Wayarles</string> + <string name="adbpair_channel_description">Meminta kod gandingan Debugging Wayarles semasa Tetapan dibuka</string> + <string name="adbpair_title_pair">Gandingkan Debugging Wayarles</string> + <string name="adbpair_waiting_text">Hidupkan Debugging Wayarles, kemudian ketik “Gandingkan peranti dengan kod gandingan”.</string> + <string name="adbpair_waiting_long">Hidupkan Debugging Wayarles, kemudian ketik “Gandingkan peranti dengan kod gandingan”. Kekal dalam Tetapan: kod akan masuk dalam pemberitahuan ini.</string> + <string name="adbpair_code_label">Kod gandingan 6 digit</string> + <string name="adbpair_code_hint">Ketik Masukkan kod dan taip 6 digit yang dipaparkan dalam Tetapan.</string> + <string name="adbpair_title_code">Masukkan kod gandingan Wi-Fi</string> + <string name="adbpair_title_retry">Cuba kod itu sekali lagi</string> + <string name="adbpair_enter_code">Masukkan kod</string> + <string name="adbpair_title_working">Menyediakan Steam</string> + <string name="adbpair_title_done">Steam sedia</string> + <string name="adbpair_done_text">Ketik untuk kembali ke DroidDeck. Anda boleh matikan Debugging Wayarles sekarang.</string> + <string name="adbpair_title_failed">Persediaan Debugging Wayarles terhenti</string> + + <string name="selfinst_http">Muat turun menjawab HTTP %1$d</string> + <string name="selfinst_size_mismatch">Saiz muat turun tidak sepadan dengan APK yang diterbitkan dan telah dibuang - cuba lagi</string> + <string name="selfinst_checksum_mismatch">Muat turun tidak sepadan dengan checksumnya dan telah dibuang - cuba lagi</string> + <string name="selfinst_wrong_package">Kemas kini ini untuk pakej Android %1$s, bukan %2$s</string> + <string name="selfinst_metadata_signer">Metadata kemas kini tidak menamakan kunci tandatangan keluaran DroidDeck</string> + <string name="selfinst_unreadable">Android tidak dapat membaca APK yang dimuat turun</string> + <string name="selfinst_apk_wrong_package">APK yang dimuat turun untuk %1$s, bukan %2$s</string> + <string name="selfinst_version_mismatch">APK yang dimuat turun menyatakan versionCode %1$d, bukan %2$d yang diterbitkan</string> + <string name="selfinst_downgrade">Android tidak akan memasang versionCode %1$d menggantikan %2$d yang dipasang</string> + <string name="selfinst_no_signing">APK yang dimuat turun tidak mempunyai maklumat tandatangan</string> + <string name="selfinst_wrong_signer">APK yang dimuat turun tidak ditandatangani dengan kunci keluaran DroidDeck</string> + <string name="appupd_http">Katalog kemas kini menjawab HTTP %1$d</string> + + <string name="loadstate_starting">Memulakan sesi…</string> + <string name="loadstate_elapsed">%1$d:%2$02d berlalu · masih berjalan</string> + <string name="loadstate_steam_update">Memuat turun kemas kini klien Steam · %1$d%%</string> + <string name="loadstate_steam_client">Memuat turun klien Steam · %1$s</string> + + <string name="pextra_busy">Pemasangan alat keserasian sudah berjalan</string> + <string name="pextra_stop_session">Hentikan sesi aktif sebelum memasang alat keserasian</string> + <string name="pextra_no_release">Tidak dapat mencari keluaran ARM64 untuk %1$s</string> + <string name="pextra_no_space">Sekurang-kurangnya 4 GB bebas tambah saiz muat turun diperlukan untuk memasang %1$s</string> + <string name="pextra_downloading">Memuat turun %1$s %2$s</string> + <string name="pextra_retrying">Mencuba semula muat turun · percubaan %1$d daripada %2$d</string> + <string name="pextra_interrupted">Muat turun terganggu</string> + <string name="pextra_download_failed">Muat turun gagal; fail separa disimpan supaya cubaan semula boleh disambung</string> + <string name="pextra_too_big">Muat turun melebihi saiz keluaran dan telah dipadam</string> + <string name="pextra_incomplete">Muat turun tidak selesai; fail separa disimpan untuk cubaan semula yang boleh disambung</string> + <string name="pextra_verifying">Mengesahkan muat turun</string> + <string name="pextra_checksum_unreadable">Tidak dapat membaca checksum keluaran; cuba lagi</string> + <string name="pextra_no_checksum">%1$s %2$s tidak menerbitkan checksum; tiada apa yang dipasang</string> + <string name="pextra_checksum_mismatch">Checksum tidak sepadan; muat turun telah dipadam</string> + <string name="pextra_session_started">Sesi dimulakan semasa muat turun; hentikan dahulu sebelum memasang alat keserasian</string> + <string name="pextra_installing">Memasang %1$s</string> + <string name="pextra_install_failed">Pemasangan %1$s gagal (keluar %2$d)</string> + <string name="pextra_could_not_install">Tidak dapat memasang %1$s</string> + <string name="esync_fetching">Mengambil pek droiddeck-esync</string> + + <string name="cmgr_fex_linux_helpers">dengan pembantu Linux</string> + <string name="cmgr_fex_dlls_only">DLL sahaja</string> + <string name="cmgr_original_proton_updated">Asal (Proton dikemas kini sejak penukaran terakhir)</string> + <string name="cmgr_changed_outside">Diubah di luar Komponen</string> + <string name="cmgr_not_present">tidak ada</string> + <string name="cmgr_original_version">Asal %1$s</string> + <string name="cmgr_original_from">Asal daripada %1$s</string> + <string name="cmgr_swap_queued">%1$s %2$s akan masuk ke %3$s apabila permainan yang berjalan padanya ditutup.</string> + <string name="cmgr_swap_done">%1$s %2$s kini dalam %3$s. Ia berkuat kuasa pada kali seterusnya permainan dimulakan.</string> + <string name="cmgr_original_missing">Pakej asal itu tidak disimpan</string> + <string name="cmgr_restore_queued">%1$s bagi %2$s akan kembali kepada asalnya apabila permainan yang berjalan padanya ditutup.</string> + <string name="cmgr_restore_done">%1$s bagi %2$s dipulihkan daripada asal %3$s-nya.</string> + <string name="cmgr_package_in_use">%1$s sedang digunakan dalam %2$s. Tukar Proton itu kepada yang lain dahulu.</string> + <string name="cmgr_package_not_stored">%1$s tidak disimpan</string> + <string name="cmgr_deleted">%1$s telah dipadam.</string> + <string name="cmgr_original_kept">Asal binaan yang dipasang dikekalkan: itulah jalan kembali.</string> + <string name="cmgr_not_stored">Tidak disimpan</string> + <string name="cmgr_deleted_original">Asal %1$s bagi %2$s telah dipadam.</string> + <string name="cmgr_nightlies_only">Muat turun hanya datang daripada keluaran Nightlies</string> + <string name="cmgr_list_predates_checksums">Senarai pakej ini mendahului checksum - segarkan dan cuba lagi</string> + <string name="cmgr_checksum_mismatch">Checksum tidak sepadan</string> + + <string name="sprefs_upscaler_linear">Linear</string> + <string name="sprefs_upscaler_nearest">Terdekat</string> + <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (kualiti)</string> + <string name="sprefs_upscaler_sharpen">Ketajaman sahaja</string> + <string name="sprefs_lod_bias_auto">Auto (sepadan penskalaan)</string> + + <string name="look_off_desc">Tiada yang digunakan.</string> + <string name="look_game_clarity">Kejelasan Permainan</string> + <string name="look_game_clarity_desc">Ketajaman dengan sedikit kontras - yang harian.</string> + <string name="look_vivid">Jelas</string> + <string name="look_vivid_desc">Warna didorong kuat. Pengurangan jalur dihidupkan supaya langit tidak bergaris.</string> + <string name="look_cinematic">Sinematik</string> + <string name="look_cinematic_desc">Kontras lebih dalam, warna lebih sejuk, tepi dilicinkan.</string> + <string name="look_competitive">Kompetitif</string> + <string name="look_competitive_desc">Bayang diangkat dan ketajaman berat - kesan orang dahulu.</string> + <string name="look_adaptive_sharpen">Ketajaman Adaptif</string> + <string name="look_adaptive_sharpen_desc">Ketajaman sahaja, warna tidak disentuh.</string> + <string name="look_filmic">Filmik</string> + <string name="look_filmic_desc">Lengkuk seperti filem, sedikit lembut.</string> + <string name="look_arcade">Arked</string> + <string name="look_arcade_desc">Kuat dan bertenaga.</string> + <string name="look_retro_crt">CRT Retro</string> + <string name="look_retro_crt_desc">Tiub CRT dan isyarat analog bergabung sekali.</string> + <string name="look_upscale_sharp">Peningkatan Skala Tajam</string> + <string name="look_upscale_sharp_desc">Untuk berjalan di bawah resolusi panel.</string> + <string name="look_pixel_clean">Piksel Bersih</string> + <string name="look_pixel_clean_desc">Piksel tajam untuk 2D dan tajuk lama.</string> + <string name="look_anime_edge">Tepi Anime</string> + <string name="look_anime_edge_desc">Garis luar kartun tambah ketajaman.</string> + + <string name="gpuinfo_not_adreno">Bukan GPU Adreno</string> + <string name="gpuinfo_supported">Disokong</string> + <string name="gpuinfo_experimental">Eksperimen: pemacunya ialah binaan ujian</string> + <string name="gpuinfo_below_tested">Di bawah perkakasan yang diuji (Adreno 725 dan lebih baharu): mungkin tidak dapat berjalan</string> + <string name="gpuinfo_unsupported">Tidak disokong: DroidDeck memerlukan GPU Adreno (Snapdragon)</string> + + <string name="turnip_rate_limited">Had kadar GitHub dicapai - cuba lagi kemudian</string> + <string name="turnip_github_http">GitHub menjawab HTTP %1$d</string> + <string name="turnip_download_http">muat turun menjawab HTTP %1$d</string> + <string name="turnip_list_predates_checksums">Senarai pemacu ini mendahului checksum - segarkan dan cuba lagi</string> + <string name="turnip_checksum_mismatch">Checksum tidak sepadan - muat turun telah dibuang</string> + <string name="turnip_not_adrenotools">Bukan pemacu AdrenoTools: %1$s</string> + <string name="turnip_reject_no_meta">tiada meta.json</string> + <string name="turnip_reject_meta_unreadable">meta.json tidak dapat dibaca (%1$s)</string> + <string name="turnip_reject_linux_driver">ini ialah pemacu masa jalan Linux (import di bawah “Pemacu masa jalan Linux”)</string> + <string name="turnip_reject_wayland">ini ialah pemacu permainan Wayland, yang tidak berguna untuk apl ini</string> + <string name="turnip_reject_no_library_name">meta.json tidak menamakan libraryName, jadi tiada apa dapat diberikan kepada AdrenoTools</string> + <string name="turnip_reject_library_missing">meta.json menamakan %1$s, yang tidak ada dalam zip</string> + <string name="turnip_reject_not_aarch64">%1$s bukan pustaka kongsi ELF AArch64 64-bit</string> + <string name="turnip_reject_glibc">%1$s menghubungkan glibc - Turnip “-Linux”, yang tergolong di bawah “Pemacu masa jalan Linux”</string> + <string name="lvd_no_library">Tiada libvulkan_freedreno*.so dalam zip ini. Zip Turnip Android (AdrenoTools) atau -Wayland bukan pemacu masa jalan Linux.</string> + <string name="lvd_not_aarch64">%1$s bukan pustaka kongsi ELF AArch64 64-bit.</string> + <string name="lvd_not_glibc">%1$s bukan pemacu glibc - ia menghubungkan libc Android. Masa jalan Linux memerlukan zip “-Linux”; Turnip biasa atau “-Wayland” langsung tidak dapat dimuatkan oleh klien Steam.</string> + <string name="bundle_no_manifest">Bukan gabungan pemacu: tiada manifest.json yang menamakan pemacu android dan linux</string> + <string name="bundle_entry_outside">Bukan gabungan pemacu: %1$s menunjuk ke luar darinya</string> + <string name="bundle_too_large">Bukan gabungan pemacu: ia mengembang lebih daripada %1$d MB</string> + + <string name="gstore_sd_card">Kad SD</string> + <string name="gstore_folder">Folder</string> + + <string name="logshare_chooser">Kongsi log sesi</string> + + <string name="hdr_needs_android_10">memerlukan Android 10 atau lebih baharu (lapisan paparan dengan ruang warna)</string> + <string name="hdr_none">paparan ini melaporkan tiada sokongan HDR</string> + <string name="hdr_not_hdr10">paparan ini menyokong %1$s, bukan HDR10</string> + + <string name="gpu_this_gpu">GPU ini</string> + <string name="phantom_title_on">Had proses anak dihidupkan</string> + <string name="phantom_title_unset">Had proses anak tidak ditetapkan</string> + <string name="phantom_title_unreadable">Had proses anak tidak dapat disemak</string> + <string name="phantom_title_off">Had proses anak dimatikan</string> + <string name="phantom_title_not_required">Had proses anak tidak diperlukan</string> + <string name="phantom_instructions_fix">%1$s Jika itu tidak boleh, sambungkan peranti ini ke komputer dengan ADB dan jalankan arahan di bawah. Kembali ke sini dan semak semula.</string> + <string name="phantom_instructions_off">Sesi Steam boleh dimulakan.</string> + <string name="phantom_instructions_not_applicable">Versi Android ini tidak menggunakan had ini.</string> + <string name="phantom_fix_toggle">Hidupkan “Lumpuhkan sekatan proses anak” dalam Pilihan pembangun, dan biarkan Pilihan pembangun hidup selepas itu.</string> + <string name="phantom_fix_no_toggle">Versi Android ini tidak mempunyai suis untuknya. Gunakan Debugging Wayarles untuk menukarnya daripada peranti ini.</string> + <string name="upd_block_no_apk">Binaan ini tidak mempunyai muat turun untuk salinan DroidDeck ini.</string> + <string name="upd_block_version_code">Android tidak akan memasangnya menggantikan binaan ini kerana versionCode %1$d lebih rendah daripada %2$d yang dipasang.</string> + <string name="upd_fail_downgrade">Android tidak akan memasang versi lama menggantikan versi baharu. Kekal pada binaan ini sehingga saluran menyusul.</string> + <string name="upd_fail_signature">Salinan DroidDeck ini ditandatangani secara berbeza (binaan tempatan?), jadi Android tidak akan mengemas kininya di tempat.</string> + <string name="upd_fail_storage">Tidak cukup ruang bebas untuk kemas kini.</string> + <string name="upd_fail_aborted">Kemas kini telah dibatalkan.</string> + <string name="upd_fail_with_message">Kemas kini tidak terpasang: %1$s</string> + <string name="upd_fail_generic">Kemas kini tidak terpasang.</string> + <string name="saves_layout_zip">zip %1$s</string> + <string name="saves_zip">zip</string> + <string name="saves_not_launched">%1$s belum pernah dilancarkan</string> + <string name="saves_none_found">Tiada fail simpanan dijumpai untuk %1$s</string> + <string name="saves_not_save_zip">%1$s tidak mempunyai folder drive_c, jadi ia bukan zip simpanan GameHub atau Winlator</string> + <string name="gstore_cannot_create">tidak dapat mencipta folder dalam %1$s</string> + <string name="gstore_cannot_write">tidak dapat menulis dalam %1$s</string> + <string name="gstore_cannot_write_detail">tidak dapat menulis dalam %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">Folder tidak tersedia</string> + <string name="gfiles_name_taken">Fail lain sudah menggunakan %1$s</string> + <string name="gfiles_art_unavailable">Folder seni tidak tersedia</string> + <string name="gfiles_unrelated_inventory">Folder mempunyai inventori penyegerakan yang tidak berkaitan</string> + <string name="gfiles_remove_failed">Tidak dapat mengalih keluar %1$s</string> + <string name="gfiles_outside_folder">Fail keluar daripada folder eksport</string> + <string name="cmgr_fex_unnamed">FEX (binaan tanpa nama versi)</string> + <string name="cmgr_no_profile">tiada profile.json dalam %1$s</string> + <string name="cmgr_not_component_package">%1$s bukan pakej komponen Linux (jenis %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s bebas</string> + <string name="mode_storage_auto_short">Automatik</string> + <string name="appupd_catalog_rejected">Katalog kemas kini gagal semakannya (%1$s)</string> + <string name="main_adb_clip_label">Tetapan proses anak DroidDeck</string> + <string name="setup_signed_in_offline_unnamed">Dilog masuk · mod luar talian</string> + <string name="setup_signed_in_unnamed">Dilog masuk</string> + <string name="driver_tag_bundled">TERBUNDLE</string> + <string name="driver_tag_downloaded">DIMUAT TURUN</string> + <string name="driver_tag_imported">DIIMPORT</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Paparan sesi</string> + <string name="display_resolution">Resolusi</string> + <string name="display_game_fit">Suaian permainan 16:9</string> + <string name="display_game_fit_hint">Cara permainan 16:9 disuaikan ke paparan ini. Sesi seterusnya.</string> + <string name="display_game_fit_unavailable">Pilih Sepadan skrin atau saiz tersuai lebih nipis daripada 16:9.</string> + <string name="display_preserve">Kekalkan nisbah</string> + <string name="display_stretch">Regang ke paparan</string> + <string name="display_image_scaling">Penskalaan imej</string> + <string name="display_filter">Penapis penskalaan</string> + <string name="display_filter_hint">Dikongsi oleh Steam dan Desktop.</string> + <string name="display_filter_live">Berkuat kuasa secara langsung. Dikongsi oleh Steam dan Desktop.</string> + <string name="display_filter_note">FSR, GSR dan NIS berkuat kuasa semasa membesarkan imej sesi. Ketajaman sahaja juga berfungsi tanpa pembesaran.</string> + <string name="display_sharpness">Ketajaman penapis</string> + <string name="display_frame_rate">Kadar bingkai</string> + <string name="display_look_filter">%1$s Menetapkan penskalaan ke %2$s.</string> + <string name="display_match_screen">Sepadan skrin (%1$d×%2$d)</string> + <string name="display_follow_screen">Ikut skrin (mengubah saiz apabila dilipat atau dibuka)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Papar butiran peranti</string> + <string name="device_info_hide">Sembunyikan butiran peranti</string> + <string name="device_info_loading">Membaca peranti…</string> + <string name="device_info_copy">Salin</string> + <string name="device_info_copy_hint">Salin butiran ini ke dalam laporan pepijat.</string> + <string name="device_info_copied">Disalin.</string> + <string name="force_ssbs_title">Paksa SSBS</string> + <string name="force_ssbs_hint">Mengekalkan tetapan kelajuan CPU yang sentiasa dimatikan oleh Wine. Boleh melajukan permainan Windows pada Snapdragon 8 Elite; sedikit perubahan dilihat pada cip lama. Berkuat kuasa pada pelancaran permainan seterusnya.</string> + + <string name="rail_stores">Kedai</string> + <string name="source_steam">Steam</string> + <string name="source_added">Tersuai</string> + <string name="setup_stores">Kedai</string> + <string name="setup_stores_open_on">Buka kedai pada</string> + <string name="setup_stores_speed">Tahap kelajuan muat turun</string> + <string name="setup_stores_engine">Enjin kedai</string> + <string name="setup_stores_engine_chip_ready">● Enjin sedia</string> + <string name="setup_stores_engine_chip_missing">Tidak dibina</string> + <string name="stores_tier_balanced">Seimbang</string> + <string name="stores_tier_fast">Pantas</string> + <string name="stores_tier_max">Maksimum</string> + <string name="stores_title">Kedai</string> + <string name="stores_settings">Tetapan kedai</string> + <string name="stores_settings_eyebrow">Tetapan</string> + <string name="stores_chip_signed_out">(belum log masuk)</string> + <string name="stores_tab_store">Kedai</string> + <string name="stores_tab_installed">Dipasang</string> + <string name="stores_tab_library">Pustaka</string> + <string name="stores_tab_all">Semua</string> + <string name="stores_tab_downloads">Muat turun</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Log masuk</string> + <string name="stores_signout_named">%1$s · log keluar</string> + <string name="stores_signout_title">Log keluar daripada %1$s?</string> + <string name="stores_signout_note">Permainan yang dipasang kekal. Pustaka diambil semula apabila anda log masuk kembali.</string> + <string name="stores_signout_stay">Kekal log masuk</string> + <string name="stores_signout_go">Log keluar</string> + <string name="stores_search_catalog">Cari katalog %1$s…</string> + <string name="stores_search_installed">Cari permainan dipasang…</string> + <string name="stores_search_library">Cari pustaka anda…</string> + <string name="stores_search_open">Cari</string> + <string name="stores_installed_empty">Tiada apa dipasang</string> + <string name="stores_library_loading">Memuatkan</string> + <string name="stores_nothing_matches">Tiada permainan</string> + <string name="stores_shelves_loading">Memuatkan kedai…</string> + <string name="stores_amazon_no_catalog">Tiada katalog</string> + <string name="stores_shelf_new">Apa yang baharu</string> + <string name="stores_shelf_deals">Tawaran</string> + <string name="stores_shelf_free">Permainan percuma</string> + <string name="stores_shelf_trending">Tular</string> + <string name="stores_shelf_library">Pustaka anda</string> + <string name="stores_play">Main</string> + <string name="stores_install">Pasang</string> + <string name="stores_resume_install">Sambung pemasangan</string> + <string name="stores_dl_interrupted">Terganggu</string> + <string name="stores_install_size">Pasang · %1$s</string> + <string name="stores_get_free">Dapatkan percuma</string> + <string name="stores_view_on">Lihat pada %1$s</string> + <string name="stores_buy_price">Beli · %1$s</string> + <string name="stores_installed_chip">Dipasang</string> + <string name="stores_in_library">Dalam pustaka</string> + <string name="stores_free">Percuma</string> + <string name="stores_resume">Sambung</string> + <string name="stores_refresh">Segar semula</string> + <string name="stores_show_mature">Papar kandungan matang</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Luar talian</string> + <string name="epic_card_none">Mati</string> + <string name="epic_eos">Log masuk Epic (EOS)</string> + <string name="epic_offline">Lancarkan secara luar talian</string> + <string name="epic_resolve">Selesaikan log masuk Epic</string> + <string name="store_cloud_saves">Simpanan awan</string> + <string name="cloud_upload">Muat naik ke awan</string> + <string name="cloud_download">Muat turun daripada awan</string> + <string name="cloud_none">Tiada simpanan awan</string> + <string name="cloud_conflict">Konflik awan</string> + <string name="cloud_conflict_files">Konflik awan · %1$d fail</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Kekalkan awan</string> + <string name="cloud_keep_local">Kekalkan tempatan</string> + <string name="cloud_checking">Menyemak awan…</string> + <string name="cloud_last_sync">Segerak terakhir %1$s</string> + <string name="cloud_never">Belum disegerakkan</string> + <string name="cloud_done">%1$d fail</string> + <string name="stores_eyebrow_installed">dipasang</string> + <string name="stores_eyebrow_library">dalam pustaka anda</string> + <string name="stores_eyebrow_store">kedai</string> + <string name="achievements_title">Pencapaian</string> + <string name="achievements_count">%1$d daripada %2$d dibuka kunci</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Dibuka kunci · %1$d</string> + <string name="achievements_group_locked">Berkunci · %1$d</string> + <string name="achievements_group_hidden">Tersembunyi · %1$d</string> + <string name="achievements_hidden">Pencapaian tersembunyi</string> + <string name="achievements_hidden_detail">Butiran dipaparkan sebaik ia dibuka kunci.</string> + <string name="achievements_all">Semua pencapaian dibuka kunci</string> + <string name="achievements_reading">Memuatkan…</string> + <string name="achievements_signed_out">Log masuk ke %1$s untuk melihat pencapaian anda.</string> + <string name="achievements_unreachable">Tidak dapat menghubungi %1$s. Semak semula sebaik anda dalam talian.</string> + <string name="achievements_none">%1$s tidak menyenaraikan pencapaian untuk permainan ini.</string> + <string name="achievements_show_all">Papar semua %1$d</string> + <string name="achievements_dialog_title">Pencapaian · %1$s</string> + <string name="stores_in_steam">✓ Dalam Steam</string> + <string name="stores_uninstall">Nyahpasang</string> + <string name="stores_uninstalling">Menyahpasang…</string> + <string name="stores_uninstall_title">Nyahpasang %1$s?</string> + <string name="stores_uninstall_body_size">Membebaskan %1$s dan mengalih keluarkannya daripada Steam.</string> + <string name="stores_uninstall_body">Mengalih keluar failnya dan pintasan Steamnya.</string> + <string name="stores_launch_not_registered">Steam akan mengambilnya pada kali seterusnya ia dimulakan; lancarkan dari sana, atau mulakan Steam sekarang.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Pilihan Steam mengikut permainan</string> + <string name="stores_card_audio">Audio</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Komponen</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Enjin</string> + <string name="stores_card_engine_value">%1$s · asli</string> + <string name="stores_card_engine_java">%1$s · terbina dalam</string> + <string name="stores_how_it_runs">Cara ia berjalan</string> + <string name="stores_kv_fetched">Diambil daripada</string> + <string name="stores_kv_fetched_value">%1$s, dengan akaun anda</string> + <string name="stores_kv_installed_to">Dipasang ke</string> + <string name="stores_kv_installed_to_value">Storan permainan (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Dilancarkan oleh</string> + <string name="stores_kv_launched_by_value">Steam dan Proton, laluan yang sama seperti permainan Steam</string> + <string name="stores_kv_launched_by_epic">Steam dan Proton · Log masuk Epic setiap pelancaran, overlay EOS mati</string> + <string name="stores_kv_launched_by_amazon">Steam dan Proton · Pemboleh ubah Amazon Games Services ditetapkan</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">ditambah ke Steam semasa pemasangan (UI Deck + Steam Input)</string> + <string name="stores_downloads_empty">Tiada muat turun</string> + <string name="stores_dl_paused">Dijeda</string> + <string name="stores_dl_queued">Dalam baris gilir</string> + <string name="stores_dl_queued_at">Dalam baris gilir · #%1$d</string> + <string name="stores_dl_cancelled">Dibatalkan</string> + <string name="stores_dl_failed">Gagal</string> + <string name="stores_dl_failed_reason">Gagal · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Jeda</string> + <string name="stores_dl_resume">Sambung</string> + <string name="stores_dl_cancel">Batal</string> + <string name="stores_dl_retry">Cuba lagi</string> + <string name="stores_dl_clear">Kosongkan</string> + <string name="stores_dl_clear_all">Kosongkan yang selesai</string> + <string name="stores_stage_manifest">Manifest</string> + <string name="stores_stage_download">Muat turun</string> + <string name="stores_stage_verify">Sahkan</string> + <string name="stores_stage_install">Pasang</string> + <string name="stores_stage_done">Selesai</string> + <string name="stores_stage_checking">Menyemak</string> + <string name="stores_stage_downloading">Memuat turun</string> + <string name="stores_dl_cancel_confirm">Padam muat turun?</string> + <string name="stores_dl_failed_kept">Gagal · %1$s dikekalkan</string> + <string name="stores_stage_verifying">Mengesahkan</string> + <string name="stores_stage_installing">Memasang</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d fail</string> + <string name="stores_download_manager">Pengurus muat turun</string> + <string name="stores_parallel">Muat turun pada satu masa</string> + <string name="stores_install_where">Pasang ke</string> + <string name="stores_install_title">Pasang %1$s:</string> + <string name="stores_target_internal">Dalaman</string> + <string name="stores_target_free">%1$s bebas</string> + <string name="stores_notification_channel">Muat turun kedai</string> + <string name="stores_notification_channel_hint">Memaparkan kemajuan muat turun permainan dan mengekalkan muat turun berjalan di latar belakang</string> + <string name="stores_notification_one">Memuat turun</string> + <string name="stores_notification_many">%1$d muat turun</string> + <string name="stores_notification_waiting">Menyediakan…</string> + <string name="stores_notification_done_channel">Muat turun kedai selesai</string> + <string name="stores_notification_installed">%1$s dipasang</string> + <string name="stores_notification_failed">Muat turun %1$s gagal</string> + <string name="stores_notification_delete">Padam</string> + <string name="stores_notification_keep">Kekalkan</string> + <string name="stores_login_title">Log masuk ke %1$s</string> + <string name="stores_login_provider_title">Log masuk dengan penyedia anda</string> + <string name="stores_login_reload">Muat semula</string> + <string name="stores_login_close">Tutup</string> + <string name="stores_login_retry">Cuba lagi</string> + <string name="stores_login_finishing">Menyelesaikan log masuk…</string> + <string name="stores_login_error_network">Halaman log masuk tidak dapat dimuatkan: %1$s</string> + <string name="stores_login_error_idp_blocked">Penyedia ini menolak log masuk daripada halaman terbenam. Log masuk dengan kata laluan kedai anda sebaliknya.</string> + <string name="stores_login_error_http">Halaman log masuk menjawab dengan ralat (HTTP %1$d).</string> + <string name="stores_login_error_ssl">Sambungan ke halaman log masuk tidak selamat.</string> + <string name="stores_login_error_verification">Log masuk tidak dapat disahkan. Cuba lagi.</string> + <string name="stores_login_error_generic">Log masuk gagal. Cuba lagi.</string> + <string name="common_close">Tutup</string> + <string name="common_back">Kembali</string> + <string name="common_edit">Sunting</string> + <string name="games_add_all">Tambah semua permainan dalam folder ini</string> + <string name="games_added_none">Tiada yang baharu dalam folder ini</string> + <string name="edit_name">Nama</string> + <string name="edit_target">Sasaran</string> + <string name="edit_start_in">Mula dalam</string> + <string name="edit_launch_options">Pilihan pelancaran</string> + <string name="edit_artwork">Seni</string> + <string name="edit_other_exe">Exe lain…</string> + <string name="edit_remove">Alih keluar</string> + <string name="art_cover">Kulit</string> + <string name="art_background">Latar belakang</string> + <string name="art_logo">Logo</string> + <string name="art_icon">Ikon</string> + <string name="art_src_folder">Folder permainan</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">Fail anda</string> + <string name="art_src_exe">Daripada exe</string> + <string name="art_src_auto">Automatik</string> + <string name="art_pick_image">Pilih imej…</string> + <string name="art_pick_title">Pilih imej: %1$s</string> + <string name="art_reset">Tetapkan semula ke automatik</string> + <plurals name="games_added_n"> + <item quantity="other">%1$d permainan ditambah</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="other">%1$d sudah berada dalam Permainan</item> + </plurals> + <string name="hop_back">B · Kembali ke %1$s</string> + <string name="hop_from_games">permainan itu</string> + <string name="hop_from_play">Tetapan Main</string> + <string name="sheet_graphics_drivers">Pemacu grafik</string> + <string name="comp_this_proton">Proton ini</string> + <string name="comp_every_proton">Setiap Proton</string> + <string name="comp_for_every_proton">Untuk setiap Proton</string> + <string name="comp_available_count">Tersedia · %1$d</string> + <string name="comp_checked">Disemak %1$s</string> + <string name="comp_check_all">Semak komponen dan pemacu baharu</string> + <string name="comp_get">Dapatkan</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton dan FEX di bawah adalah milik Proton ini</string> + <string name="comp_device_tested">%1$s · diuji</string> + <string name="comp_device_untested">%1$s · belum diuji</string> + <string name="comp_device_unsupported">%1$s · tidak disokong</string> + <string name="gpu_auto_keeps">Mengekalkan pasangan disyorkan terkini</string> + <string name="gpu_bar_auto">Auto · %1$s</string> + <string name="gpu_separately">Pemacu masa jalan dan paparan secara berasingan</string> + <string name="gpu_use_title">Gunakan %1$s?</string> + <string name="proton_remove_title">Alih keluar %1$s?</string> + <string name="setup_tab_diagnostics">Diagnostik</string> + <string name="setup_ready">Sedia untuk bermain</string> + <string name="setup_not_ready">Belum sedia untuk bermain</string> + <string name="setup_ready_runtime">masa jalan %1$s</string> + <string name="setup_ready_limit_off">had proses dimatikan</string> + <string name="setup_ready_signed_in">dilog masuk sebagai %1$s</string> + <string name="setup_needs_you">Apabila sesuatu memerlukan anda</string> + <string name="setup_group_look">Rupa</string> + <string name="setup_group_device">Peranti ini</string> + <string name="setup_group_art">Seni pustaka</string> + <string name="flathub_eyebrow">APL LINUX</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Apl desktop untuk desktop Linux, dibuka daripada Desktop.</string> + <string name="flathub_setup">Sediakan Flathub</string> + <string name="flathub_remove_title">Alih keluar %1$s?</string> + <string name="flathub_remove_note">Tetapan dan failnya dikekalkan.</string> + <string name="flathub_keep">Kekalkan</string> + <string name="desktop_size">Saiz pada cakera</string> + <string name="desktop_uninstall_title">Nyahpasang desktop?</string> + <string name="desktop_all_systems">Semua</string> + <string name="gog_achievements">Pencapaian GOG</string> + <string name="gog_achievements_hint">Menyegerakkan pencapaian untuk permainan GOG yang disokong. Terpakai pada pelancaran seterusnya.</string> + <string name="gog_achievements_off">Pencapaian dimatikan</string> + <string name="gog_achievements_on">Pencapaian dihidupkan</string> + <string name="gog_achievements_signin">Log masuk semula ke GOG</string> + <string name="gog_achievements_uploaded">Pencapaian dimuat naik</string> + <string name="gog_achievements_pending">Muat naik pencapaian belum selesai</string> + <string name="gog_achievements_enable_store">Hidupkan pencapaian GOG dalam tetapan Stores</string> + <string name="gog_achievements_unavailable">Pencapaian tidak tersedia; semak log sesi</string> + <string name="achievement_unlocked">Pencapaian dibuka kunci</string> + <string name="touch_paste">Tampal</string> + <string name="touch_dpad">Pad arah</string> + <string name="touch_left_stick">Kayu bedik kiri</string> + <string name="touch_right_stick">Kayu bedik kanan</string> + <string name="touch_menu">Menu</string> + <string name="touch_keyboard">Papan kekunci</string> + <string name="touch_dpad_up">Pad arah atas</string> + <string name="touch_dpad_down">Pad arah bawah</string> + <string name="touch_dpad_left">Pad arah kiri</string> + <string name="touch_dpad_right">Pad arah kanan</string> + <string name="touch_title">Kawalan sentuh</string> + <string name="touch_controller">Pengawal sentuh</string> + <string name="touch_mouse">Tetikus</string> + <string name="touch_on">Hidup</string> + <string name="touch_off">Mati</string> + <string name="touch_speed_slow">Perlahan</string> + <string name="touch_speed_normal">Biasa</string> + <string name="touch_speed_fast">Pantas</string> + <string name="touch_edit_layout">Edit susun atur</string> + <string name="touch_droiddeck_menu">Menu DroidDeck</string> + <string name="touch_drag_hint">Seret kawalan ke skrin</string> + <string name="touch_arrange">Susun</string> + <string name="touch_discard">Buang</string> + <string name="touch_saving">Menyimpan…</string> + <string name="touch_icon">Ikon</string> + <string name="touch_narrower">L−</string> + <string name="touch_wider">L+</string> + <string name="touch_shorter">T−</string> + <string name="touch_taller">T+</string> + <string name="touch_hide">Sembunyikan</string> + <string name="touch_color_opacity">Warna dan kelegapan…</string> + <string name="touch_copy_layout">Salin susun atur ini</string> + <string name="touch_layout_copied">Susun atur disalin; tampalkannya pada susun atur sentuh mana-mana permainan</string> + <string name="touch_paste_layout">Tampal susun atur yang disalin</string> + <string name="touch_reset_layout">Tetapkan semula kepada lalai Steam</string> + <string name="touch_no_binding">Kawalan ini tidak mempunyai pengikatan dalam Steam untuk diberikan ikon</string> + <string name="touch_icon_for">Ikon untuk %1$s</string> + <string name="touch_layout_color">Warna susun atur sentuh</string> + <string name="touch_done">Selesai</string> + <string name="touch_save_failed">Susun atur sentuh tidak dapat disimpan</string> + <string name="touch_saved">Susun atur sentuh disimpan ke Steam</string> + <string name="touch_app_id">Apl %1$d</string> + <string name="touch_label_hint">Label (pilihan)</string> + <string name="touch_icons_missing">Ikon Steam tidak dijumpai</string> + <plurals name="touch_icons_found"> + <item quantity="other">%1$d ikon daripada Steam</item> + </plurals> + <string name="touch_icon_color">Warna ikon</string> + <string name="touch_button_color">Warna butang</string> + <string name="touch_no_icon">Tiada ikon</string> + + <string name="stores_sync_sign_in">Menyemak log masuk…</string> + <string name="stores_sync_library">Mengambil pustaka anda…</string> + <string name="stores_sync_game_list">Mengambil senarai permainan…</string> + <string name="stores_sync_details">Memuatkan butiran permainan… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="other">Menyegerakkan %1$d permainan…</item> + </plurals> + <string name="stores_sync_progress">Menyegerakkan… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Mengambil kulit permainan…</string> + <string name="stores_sync_failed">Penyegerakan gagal</string> + <string name="stores_sync_no_games">Tiada permainan dijumpai dalam pustaka %1$s anda</string> + <string name="stores_sync_amazon_no_answer">Amazon Games tidak memberi respons untuk pustaka anda: muat semula, atau log masuk semula</string> + <string name="stores_sync_gog_unreachable">Tidak dapat menyambung ke GOG</string> + <string name="stores_sync_gog_unreadable">Pustaka GOG anda tidak dapat dibaca</string> + <string name="stores_registering">Mendaftar dengan Steam…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Mod Cardboard</string> + <string name="cardboard_mode_hint">Paparan mata kiri dan kanan untuk kaca mata VR telefon.</string> + <string name="cardboard_source">Gambar</string> + <string name="cardboard_source_screen">Skrin maya</string> + <string name="cardboard_source_sbs">3D sebelah-menyebelah</string> + <string name="cardboard_viewer">Kaca mata</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Tiada kanta</string> + <string name="cardboard_ipd">Jarak mata</string> + <string name="cardboard_screen_width">Lebar skrin</string> + <string name="cardboard_screen_distance">Jarak skrin</string> + <string name="cardboard_head_tracking">Penjejakan kepala</string> + <string name="cardboard_no_sensor">Peranti ini tiada penderia putaran.</string> + <string name="cardboard_recenter">Pusatkan semula paparan</string> + <string name="cardboard_recenter_button">Pusatkan semula</string> + <string name="cardboard_steamvr">SteamVR (eksperimen)</string> + <string name="cardboard_steamvr_install">Pasang</string> + <string name="cardboard_steamvr_install_hint">Memuat turun SteamVR ARM64 daripada Valve (571 MB).</string> + <string name="cardboard_steamvr_installing">Memuat turun dan menyahmampat SteamVR…</string> + <string name="cardboard_steamvr_start">Mula</string> + <string name="cardboard_steamvr_ready_hint">Menjalankan SteamVR dengan kepala anda sebagai set kepala; gambar bertukar kepada 3D sebelah-menyebelah.</string> + <string name="cardboard_steamvr_stop">Henti</string> + <string name="cardboard_steamvr_running_hint">SteamVR sedang berjalan.</string> + <string name="cardboard_steamvr_unavailable">Tidak tersedia dalam sesi ini.</string> + <string name="cardboard_steamvr_failed">SteamVR tidak memberi respons; semak log sesi.</string> + <string name="drawer_component_downloads">Muat turun komponen</string> + <string name="drawer_component_downloads_hint">Muat turun dikongsi dengan halaman Komponen. Pilih versi yang telah dimuat turun di atas untuk menggunakannya pada pelancaran permainan seterusnya.</string> + <string name="drawer_component_choose_download">Pilih versi untuk dimuat turun</string> + <string name="stores_fact_developer">Pembangun</string> + <string name="stores_fact_publisher">Penerbit</string> + <string name="stores_fact_released">Dikeluarkan</string> + <string name="stores_hltb_main">Utama</string> + <string name="stores_hltb_extras">+ Tambahan</string> + <string name="stores_hltb_completionist">Lengkap</string> + <string name="stores_hltb_hours">%1$s j</string> + <string name="stores_hltb_minutes">%1$d min</string> +</resources> diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml new file mode 100644 index 000000000..4e03c7770 --- /dev/null +++ b/app/src/main/res/values-nb/strings.xml @@ -0,0 +1,2235 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>Første gang lastes Steam ned. Dette kan ta noen minutter.</item> + <item>Som standard settes Steam på pause når du lukker appen, og fortsetter der du slapp når du kommer tilbake.</item> + <item>Kontrolleren din vises som en Xbox 360-kontroller. Hvis ikke, bruk skjermkontrollene.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>Første gang settes skrivebordet opp. Dette kan ta noen minutter.</item> + <item>Som standard settes sesjonen på pause når du lukker appen, og fortsetter der du slapp når du kommer tilbake.</item> + <item>Berøring fungerer som styreflate: dra for å flytte pekeren, trykk for å klikke.</item> + <item>Kontrolleren din vises som en Xbox 360-kontroller. Hvis ikke, bruk skjermkontrollene.</item> + </string-array> + <string name="session_channel">Sesjon</string> + <string name="session_channel_description">Vises mens en Steam- eller skrivebordssesjon er åpen</string> + <string name="session_notification">Steam kjører</string> + <string name="session_notification_downloads">Steam-nedlasting aktiv</string> + <string name="session_notification_paused">Sesjonen er pauset – trykk på Gjenoppta når du er klar</string> + <string name="resume_session">Gjenoppta</string> + <string name="suspend_now">Sett på pause nå</string> + <string name="stop_session">Avslutt sesjon</string> + <string name="frame_gen_title">Bildegenerering</string> + <string name="frame_gen_hint">Velg modus for bildegenerering</string> + <string name="frame_gen_off">Av</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG Adaptiv %1$d</string> + <string name="lsfg_needs_steam">Krever Lossless Scaling på Steam</string> + <string name="lsfg_needs_install">Installer Lossless Scaling via Steam, eller importer Lossless.dll</string> + <string name="lsfg_import">Importer Lossless.dll…</string> + <string name="lsfg_using_steam">Bruker kopien fra Steam</string> + <string name="lsfg_using_import">Bruker en importert kopi</string> + <string name="lsfg_pick_title">Velg Lossless.dll</string> + <string name="lsfg_imported">Lossless.dll importert</string> + <string name="lsfg_import_unchanged">Denne Lossless.dll-filen er allerede i bruk</string> + <string name="lsfg_import_not_owned">Lossless Scaling eies ikke av denne Steam-kontoen</string> + <string name="lsfg_import_invalid">Denne filen er ikke Lossless Scalings Lossless.dll</string> + <string name="lsfg_import_failed">Kunne ikke forberede LSFG fra denne Lossless.dll-filen</string> + <string name="lsfg_problem_missing">LSFG krever Lossless Scaling fra Steam eller en importert Lossless.dll</string> + <string name="lsfg_problem_failed">LSFG kunne ikke forberede shaderne sine fra Lossless.dll</string> + <string name="hud_fg_unsupported">%1$s: ikke støttet</string> + <string name="hud_fg_failed">%1$s: motoren feilet</string> + <string name="hud_fg_needs_lossless">%1$s: krever Lossless Scaling</string> + <string name="hud_fg_starting">%1$s %2$s · starter</string> + <string name="second_library_import_hint">Legger til et Steam-bibliotek og søker gjennom det etter Windows-spill. Eide spill bruker sine Steam-oppføringer, resten blir snarveier til spill som ikke er fra Steam.</string> + <string name="game_env_title">Spillmiljø</string> + <string name="game_env_hint">Miljøvariabler for Proton-spill.</string> + <string name="game_env_edit">Rediger</string> + <string name="game_env_shared">Alle Proton-spill</string> + <string name="game_env_scope">Profil</string> + <string name="game_env_app_id">Steam-app-ID</string> + <string name="game_env_open">Åpne</string> + <string name="game_env_profile">Spill %1$s</string> + <string name="game_env_add">Legg til variabel</string> + <string name="game_env_name">Variabelnavn</string> + <string name="game_env_value">Verdi</string> + <string name="game_env_save">Lagre</string> + <string name="game_env_remove">Fjern</string> + <string name="game_env_restore">Arv på nytt</string> + <string name="game_env_reset">Fjern alle miljøoverstyringer</string> + <string name="game_env_unset">Fjern for denne profilen</string> + <string name="game_env_empty">Ingen variabler satt.</string> + <string name="game_env_inherited">Arvet</string> + <string name="game_env_custom">Egendefinert variabel</string> + <string name="game_env_error">Kunne ikke laste eller lagre. Kontroller ledig lagringsplass og prøv igjen.</string> + <string name="game_env_invalid">Bruk et gyldig navn og en verdi på opptil 8192 tegn.</string> + <string name="game_env_unused">Standard Proton-kjøretid leser ikke denne variabelen.</string> + <string name="game_env_loading">Laster…</string> + <string name="game_env_feature">Overstyring av D3D12-funksjonsnivå. 12_2 rapporterer DirectX 12 Ultimate-funksjoner; ustøttede funksjoner kan feile. La stå uten verdi for automatisk oppdagelse.</string> + <string name="game_env_shader">Overstyring av D3D12-shadermodell. 6_9 krever en tilstrekkelig ny VKD3D og tilsvarende GPU-funksjoner. La stå uten verdi for automatisk oppdagelse.</string> + <string name="game_env_cache">Behold Mesa shader-mellomlagring aktivert for å redusere gjentatt kompilering. false aktiverer mellomlagring.</string> + <string name="game_env_cache_size">Maksimal størrelse på Mesa shader-mellomlageret, for eksempel 1G. Større mellomlagre bruker mer lagringsplass.</string> + <string name="game_env_vkd3d_config">VKD3D-innstillinger for D3D12. nodxr slår av ray tracing; bruk bare for spill som krever det.</string> + <string name="game_env_proton_log">Sett til 1 for en Proton-diagnostikklogg. Logging kan redusere ytelsen og bruke lagringsplass.</string> + <string name="game_env_wined3d">Sett til 1 for å bruke OpenGL-baserte WineD3D i stedet for DXVK til kompatibilitetstesting av D3D9–11.</string> + <string name="game_env_no_ntsync">Sett til 1 for å holde dette spillet unna ntsync (droiddeck-ntsync eller en kjernedriver); det bruker droiddeck-esync der en pakke finnes, ellers droiddeck-fsync.</string> + <string name="game_env_no_esync">Sett til 1 for å slå av droiddeck-esync for dette spillet; det bruker droiddeck-fsync i stedet, eller droiddeck-ntsync der fsync også er av.</string> + <string name="game_env_no_fsync">Sett til 1 for å holde dette spillet unna droiddeck-fsync; det bruker droiddeck-esync der en pakke finnes, ellers droiddeck-ntsync.</string> + <string name="game_env_fsync_first">Sett til 1 for å kjøre dette spillet på droiddeck-fsync, eller 0 for å sette droiddeck-esync først der en pakke passer Protonen.</string> + <string name="game_env_xalia">Sett til 0 for å slå av Protons hjelper for kontrollernavigasjon for dette spillet.</string> + <string name="game_env_hud">DXVK-overlegg for D3D9–11. fps viser bildefrekvens; uten verdi skjules det.</string> + <string name="game_env_dxvk_config">DXVK-innstillinger skilt med semikolon. Nyere versjoner bruker dxvk.maxFrameRate for bildefrekvensbegrensning; bruk heller sesjonens bildefrekvensbegrensning for vanlig spilling.</string> + <string name="game_env_vkd3d_limit">Bildefrekvensgrense for D3D12. 0 slår av grensen.</string> + <string name="game_env_glthread">Mesa OpenGL-tråding. true kan hjelpe CPU-begrensede OpenGL-spill; test per spill.</string> + <string name="sync_backend_title">Wine-synkronisering</string> + <string name="sync_backend_hint">Hvordan spill holder trådene sine i takt. Gjelder fra neste sesjon.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">FEX-forhåndsinnstilling</string> + <string name="fex_next_launch">Gjelder fra neste spilløkt.</string> + <string name="game_settings_title">Spill</string> + <string name="fex_default_label">FEX-standard</string> + <string name="fex_default_detail">Bruker FEX-standardinnstillinger.</string> + <string name="fex_stability_label">Stabilitet</string> + <string name="fex_stability_detail">Tryggeste, saktest innstillinger.</string> + <string name="fex_compatibility_label">Kompatibilitet</string> + <string name="fex_compatibility_detail">Full TSO med multiblokk.</string> + <string name="fex_intermediate_label">Middels</string> + <string name="fex_intermediate_detail">Balansert TSO med redusert x87-presisjon.</string> + <string name="fex_performance_label">Ytelse</string> + <string name="fex_performance_detail">All TSO av; multiblokk.</string> + <string name="fex_performance_tso_label">Ytelse + TSO</string> + <string name="fex_performance_tso_detail">Ytelsesinnstillinger med hoved-TSO aktivert.</string> + <string name="fex_extreme_label">Ekstrem</string> + <string name="fex_extreme_detail">Slår av kontroller for selvmodifiserende kode. JIT-spill og DRM kan feile.</string> + <string name="fex_extreme_tso_label">Ekstrem + TSO</string> + <string name="fex_extreme_tso_detail">Ekstreminnstillinger med hoved-TSO aktivert.</string> + <string name="fex_extreme_gn_label">Ekstrem-gn</string> + <string name="fex_extreme_gn_detail">Ytelse med TSC og metadatahint. Prøv før Ekstrem.</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">Ytelse med fulle kodekontroller og hypervisor-biten skjult.</string> + <string name="game_env_custom_value">Egendefinert verdi</string> + <string name="game_env_select_value">Velg verdi</string> + <string name="user_apps_section">Emulatorer & apper</string> + <string name="user_apps_add">Legg til</string> + <string name="user_apps_add_detail">Din egen app eller spill</string> + <string name="user_apps_kind_script">Skript</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d%%</string> + <string name="user_apps_starting">Starter…</string> + <string name="user_apps_added">%1$s lagt til</string> + <string name="user_apps_removed">%1$s fjernet</string> + <string name="user_apps_back">Skrivebord</string> + <string name="user_apps_gone">Appen finnes ikke lenger. Velg den på nytt fra Skrivebord.</string> + <string name="user_apps_open">Åpne %1$s</string> + <string name="user_apps_remove">Fjern</string> + <string name="user_apps_remove_confirm">Trykk igjen for å fjerne</string> + <string name="user_apps_stop_session">Avslutt sesjonen for å fjerne</string> + <string name="user_apps_script_linked">Kjører %1$s fra mappen sin, lenket inn i sesjonen der den ligger. Fjerning av appen lar mappen være i fred.</string> + <string name="user_apps_script_copied">Kjører %1$s fra en kopi av mappen sin inne i DroidDeck. Fjerning av appen sletter kopien, ikke originalen.</string> + <string name="user_apps_runtime_required">Installer Linux-kjøretiden først</string> + <string name="user_apps_file_gone">Filen finnes ikke lenger</string> + <string name="user_apps_no_space">Ikke nok ledig plass: å kopiere mappen krever %1$s</string> + <string name="user_apps_copying">Kopierer %1$s</string> + <string name="user_apps_failed">Kunne ikke legge til appen</string> + <string name="user_apps_github_checking">Søker etter en utgivelse av %1$s</string> + <string name="user_apps_github_unreachable">Kunne ikke lese utgivelsene til %1$s</string> + <string name="user_apps_github_no_appimage">Ingen utgivelse av %1$s har en ARM64- eller x86_64-AppImage</string> + <string name="user_apps_downloading">Laster ned %1$s</string> + <string name="user_apps_download_failed">Nedlastingen mislyktes</string> + <string name="user_apps_update_failed">Oppdatering av appen mislyktes</string> + <string name="user_apps_icon_failed">Dette ikonet kunne ikke leses</string> + <string name="user_apps_saved">%1$s lagret</string> + <string name="user_apps_edit">Rediger</string> + <string name="user_apps_check_updates">Se etter oppdateringer</string> + <string name="user_apps_checking_updates">Ser etter oppdateringer…</string> + <string name="user_apps_update_to">Oppdater til %1$s</string> + <string name="user_apps_up_to_date">Oppdatert · %1$s</string> + <string name="user_apps_updated">%1$s oppdatert</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">Legg til en app</string> + <string name="add_app_tab_script">Skript</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">En .sh-fil. Den kjører fra sin egen mappe, i fullskjerm under gamescope.</string> + <string name="add_app_appimage_hint">En ARM64 (aarch64) AppImage, eller en x86_64-AppImage som kjører via FEX. Den pakkes ut i Linux-kjøretiden én gang.</string> + <string name="add_app_github_hint">Installerer ARM64-AppImage-filen fra den nyeste utgivelsen i repositoriet, eller x86_64-filen via FEX.</string> + <string name="app_fex_title">Bruk FEX</string> + <string name="app_fex_auto">Auto</string> + <string name="app_fex_on">På</string> + <string name="app_fex_off">Av</string> + <string name="app_fex_hint">FEX kjører x86-Linux-programmer på denne ARM64-enheten. Auto bruker det når programmet er bygget for x86.</string> + <string name="app_fex_x86">%1$s via FEX</string> + <string name="app_fex_not_ready">Dette programmet er bygget for %1$s-PC-er og kjører via FEX, som ikke er satt opp ennå. Åpne Steam én gang mens du er innlogget: DroidDeck lar den laste ned FEX og Steam Linux Runtime den trenger, og så åpnes dette.</string> + <string name="session_end_program">Dette programmet</string> + <string name="session_end_fex_missing">%1$s er bygget for x86-PC-er og kjører via FEX, som ikke er installert ennå. Start Steam én gang mens du er innlogget, og la den laste ned FEX og Steam Linux Runtime, og start dette på nytt.</string> + <string name="add_app_flatpak_hint">En Flathub-app-ID eller dens Flathub-side. Flatpak settes opp først hvis det ikke allerede er gjort.</string> + <string name="add_app_file">Fil</string> + <string name="add_app_no_file">Ingen fil valgt</string> + <string name="add_app_choose_file">Velg fil</string> + <string name="add_app_change_file">Endre</string> + <string name="add_app_pick_script">Velg et skript</string> + <string name="add_app_pick_appimage">Velg en AppImage</string> + <string name="add_app_pick_icon">Velg et ikon</string> + <string name="add_app_repository">Repositorium</string> + <string name="add_app_repository_placeholder">eier/repo eller en github.com-lenke</string> + <string name="add_app_repository_invalid">Ikke et GitHub-repositorium</string> + <string name="add_app_flatpak_id">App-ID</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">Ikke en gyldig Flathub-app-ID</string> + <string name="add_app_flatpak_bundle_hint">Eller en .flatpak-fil, for en app som ikke er på Flathub.</string> + <string name="add_app_pick_flatpak">Velg en .flatpak-fil</string> + <string name="add_app_source">Kilde</string> + <string name="add_app_appearance">På skrivebordet ditt</string> + <string name="add_app_preview_name">Ny app</string> + <string name="add_app_name">Navn</string> + <string name="add_app_name_placeholder">Fra appen</string> + <string name="add_app_icon">Ikon</string> + <string name="add_app_icon_default">Standardikon</string> + <string name="add_app_choose_icon">Velg ikon</string> + <string name="add_app_clear_icon">Fjern</string> + <string name="add_app_icon_suggestions">Fra repositoriet</string> + <string name="add_app_icon_searching">Søker etter ikoner…</string> + <string name="add_app_icon_reset">Standard</string> + <string name="edit_app_title">Rediger %1$s</string> + <string name="edit_app_save">Lagre</string> + <string name="add_app_checking_folder">Kontrollerer mappen…</string> + <string name="add_app_folder_linked">Mappen er lenket inn i sesjonen: %1$s</string> + <string name="add_app_folder_copied">Mappen inneholder Linux-programmer, som delt lagring ikke kan kjøre, så den kopieres inn i DroidDeck (%1$s).</string> + <string name="add_app_confirm">Legg til</string> + <string name="add_app_cancel">Avbryt</string> + <string name="add_app_runtime_required">Installer Linux-kjøretiden fra Oppsett først.</string> + <string name="add_app_busy">Vent til %1$s er ferdig.</string> + <string name="steam_compat_label">Kompatibel</string> + <string name="proton_compatible_note">Builder merket «%1$s» i Steams Kompatibilitet-meny har DroidDecks prefiksfiksing og starter spill her. Standarden er Proton Experimental ARM64 (%1$s), eller Proton 11.0 ARM64 (%1$s) når den nyere builden er installert. Valves egne Proton-oppføringer uten merket kan ikke starte et spill på denne enheten.</string> + <string name="proton_installed_compatible">Installert %1$s · %2$s</string> + <string name="common_cancel">Avbryt</string> + <string name="common_auto">Auto</string> + <string name="common_never">Aldri</string> + <string name="common_always">Alltid</string> + <string name="common_uninstall">Avinstaller</string> + <string name="common_check">Se etter</string> + <string name="common_update">Oppdater</string> + <string name="common_checking">Ser etter…</string> + <string name="common_working">Arbeider…</string> + <string name="common_forget">Glem</string> + <string name="common_add_ellipsis">Legg til…</string> + <string name="common_use">Bruk</string> + <string name="common_applies_next_session">Gjelder fra neste sesjon.</string> + <string name="mode_steam_title">Steam-sesjon</string> + <string name="mode_desktop_title">Skrivebordssesjon</string> + <string name="mode_display">Skjerm</string> + <string name="mode_resolution">Oppløsning</string> + <string name="mode_res_720">Opptil 720p</string> + <string name="mode_res_900">Opptil 900p</string> + <string name="mode_res_1080">Opptil 1080p</string> + <string name="mode_res_native">Nativ</string> + <string name="mode_res_default">%1$s (standard)</string> + <string name="mode_res_custom_value">Egendefinert · %1$d×%2$d</string> + <string name="mode_res_custom">Egendefinert…</string> + <string name="mode_res_note">720p kan gjøre menyer mer responsive.</string> + <string name="mode_ratio">Skjermforhold</string> + <string name="mode_ratio_custom">Angis av den egendefinerte oppløsningen.</string> + <string name="mode_ratio_auto">Auto bruker minst 16:9.</string> + <string name="mode_fps">Bildefrekvensgrense</string> + <string name="mode_fps_note">Begrenser hele sesjonen. Skjermen bytter til en oppdateringsfrekvens som grensen går opp i.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">HDR10-utgang</string> + <string name="mode_hdr_unavailable">Ikke tilgjengelig: %1$s.</string> + <string name="mode_hdr_restart">Start appen på nytt for å bruke dette.</string> + <string name="mode_drivers">Drivere</string> + <string name="mode_gpu_drivers">GPU-drivere</string> + <string name="mode_gpu_drivers_hint">Delt av Steam og Skrivebord. Angis på Komponenter-siden.</string> + <string name="mode_touch_controls">Berøring og kontroller</string> + <string name="mode_touch">Berøring</string> + <string name="mode_touch_touchpad">Styreflate</string> + <string name="mode_touch_direct">Direkte</string> + <string name="mode_touch_note">Auto: styreflate på Skrivebord, direkte i Steam.</string> + <string name="mode_osc">Skjermkontroller</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">Auto viser kontrollene når ingen kontroller er tilkoblet.</string> + <string name="osc_steam_touch">Steam-berøring (beta)</string> + <string name="osc_style_next_session">Steams berøringskontroller starter med neste Steam-sesjon</string> + <string name="osc_steam_layout">Steam-berøringslayout</string> + <string name="osc_steam_layout_hint">Flytt, endre størrelse på eller skjul dette spillets berøringskontroller; lagres til Steam</string> + <string name="mode_controller">Kontroller</string> + <string name="mode_controller_hint">Hvordan Steam ser kontrolleren din. Gjelder fra neste sesjon.</string> + <string name="mode_controller_deck">Steam Deck-kontroller</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Xbox 360-kontroller</string> + <string name="mode_controller_note">Deck: Hurtigmeny-knapp og gyro. Steam Controller: det samme, med gripesensor på bakknappene. DualShock 4 og DualSense: gyro, og styreflatene som styreflate. Xbox 360: åpne Hurtigmenyen med Guide+A.</string> + <string name="mode_back">Tilbake</string> + <string name="back_menu_then_qam">Meny → QAM</string> + <string name="back_qam_then_menu">QAM → Meny</string> + <string name="mode_session">Sesjon</string> + <string name="mode_suspend">Bakgrunnsoppførsel</string> + <string name="mode_suspend_hint">Når appen er skjult eller skjermen slås av.</string> + <string name="mode_suspend_manual">Manuell</string> + <string name="mode_suspend_native">Nativ</string> + <string name="mode_suspend_steam_note">Auto setter på pause og gjenopptar når den vises. Nativ forbereder Steam på hvilemodus først. Manuell venter på Gjenoppta. Aldri lar den fortsette å kjøre.</string> + <string name="mode_background_downloads">La nedlastinger fortsette</string> + <string name="mode_background_downloads_hint">Utsetter bakgrunnspause mens Steam laster aktivt ned.</string> + <string name="mode_suspend_note">Auto setter på pause i bakgrunnen og gjenopptar når den vises. Manuell venter på Gjenoppta. Aldri lar den fortsette å kjøre.</string> + <string name="mode_startup">Oppstart</string> + <string name="mode_steam_startup">Start Steam med DroidDeck</string> + <string name="mode_steam_startup_hint">Åpner Steam-sesjonen når DroidDeck starter.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">Ser etter utgivelser…</string> + <string name="mode_decky_ready">Klar til å installere %1$s.</string> + <string name="mode_decky_none">Ingen kompatibel build funnet. Prøv igjen senere.</string> + <string name="mode_decky_installed">Installert · %1$s</string> + <string name="mode_decky_update">Oppdatering tilgjengelig · %1$s</string> + <string name="mode_decky_current">Oppdatert · %1$s</string> + <string name="mode_decky_loader">Loader</string> + <string name="mode_decky_install_latest">Installer nyeste</string> + <string name="mode_decky_on">Starter med Steam. Mens på, kan andre apper på denne enheten kanskje styre Steam.</string> + <string name="mode_decky_off">Av. Steams fjernsøkeport for feilsøking forblir stengt.</string> + <string name="mode_decky_remove_title">Avinstaller Decky Loader?</string> + <string name="mode_decky_remove_text">Fjerner loaderen. Programtilleggene og innstillingene dine beholdes.</string> + <string name="mode_client">Klient</string> + <string name="mode_mangoapp">Ytelsesoverlegg</string> + <string name="mode_mangoapp_hint">Slå av hvis spill krasjer eller blir svarte i Deck-modus. Gjelder fra neste sesjon.</string> + <string name="mode_steam_repair">Reparer Steam-klienten</string> + <string name="mode_steam_repair_hint">Laster ned Steam-klienten igjen neste gang Steam starter. Bruk dette hvis Steam blir hengende på lasteskjermen. Spill, innlogging og innstillinger beholdes.</string> + <string name="mode_steam_repair_queued">Steam-klienten lastes ned igjen neste gang Steam starter.</string> + <string name="mode_steam_repair_button">Reparer</string> + <string name="mode_added_art">Grafikk</string> + <string name="mode_sgdb_key">SteamGridDB API-nøkkel</string> + <string name="mode_sgdb_key_yours">Nøkkelen din</string> + <string name="mode_sgdb_edit">Rediger…</string> + <string name="mode_added_art_hint">Fyller inn manglende grafikk fra Steam, deretter SteamGridDB. Din egen cover.jpg, hero.jpg, logo.png eller icon.png i spillmappen har forrang.</string> + <string name="summary_remove_game">Fjern %1$s</string> + <string name="mode_added_remove_confirm">Fjerne?</string> + <string name="games_add">Legg til spill</string> + <string name="games_add_title">Legg til et spill:</string> + <string name="games_pick_exe_title">Velg exe-filen:</string> + <string name="games_add_unreachable">Denne plasseringen er ikke tilgjengelig i sesjonen</string> + <string name="mode_added_exe_check">Kontroller denne exe-filen</string> + <string name="wincomp_title">Windows-komponenter</string> + <string name="wincomp_pad_hint">LB/RB hopper mellom delene.</string> + <string name="wincomp_card_none">Ingen</string> + <string name="wincomp_applies">Windows-DLL-er Proton ikke inkluderer, fra komponentlisten. Slå på de dette spillet trenger: de legges inn i spillets egen Windows-mappe, aldri i filene det kom med, og blir med uansett hvilken Proton det kjører på.</string> + <string name="wincomp_offline">Kunne ikke nå komponentlisten. Komponenter som allerede er lastet ned, kan fremdeles slås av og på.</string> + <string name="wincomp_recommended">Anbefalt for dette spillet</string> + <string name="wincomp_checking">Ser på hva dette spillet trenger…</string> + <string name="wincomp_when_online">Tilgjengelig når du er tilkoblet</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">Slår på %1$d komponent…</item> + <item quantity="other">Slår på %1$d komponenter…</item> + </plurals> + <string name="wincomp_auto_section">På for dette spillet</string> + <string name="wincomp_auto_steam">Steam installerer %1$s med %2$s (app %3$s)</string> + <string name="wincomp_auto_gog">Fra GOGs liste for dette spillet</string> + <string name="wincomp_auto_epic">Fra Epics forutsetninger for dette spillet</string> + <string name="wincomp_auto_amazon">Fra Amazons installasjonssteg for dette spillet</string> + <string name="wincomp_steam_by_name">Steam installerer %1$s med app %2$s, matchet etter navn</string> + <string name="wincomp_suggestions">Også oppført</string> + <string name="wincomp_suggestion_reason">Oppført av %1$s; ikke en komponent her</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">Spillet leveres med %1$s</string> + <string name="wincomp_found_shipped">Spillet har %1$s (fungerer ofte som det er)</string> + <string name="wincomp_already_here">Allerede i dette spillet</string> + <string name="wincomp_no_recommendation">Ingenting i spillets mappe peker på en komponent.</string> + <string name="wincomp_active">Installert for dette spillet (%1$d)</string> + <string name="wincomp_all">Alle komponenter (%1$d)</string> + <string name="wincomp_installers">Krever et Windows-installasjonsprogram (%1$d)</string> + <string name="wincomp_installers_note">Disse er egne installasjonsprogrammer uten en pakke inni å ta fra hverandre, og ingen oversikt over hva de installerer ennå. De fleste er gamle kjøretider som Mono inne i Proton allerede dekker; oversikter legges til etter hvert som de blir laget.</string> + <string name="wincomp_show">Vis</string> + <string name="wincomp_hide">Skjul</string> + <string name="wincomp_needs_installer">Kjører sitt eget installasjonsprogram (ingen oversikt ennå)</string> + <string name="wincomp_unsupported">Ikke støttet her ennå</string> + <string name="wincomp_loading">Laster komponentene…</string> + <string name="wincomp_failed">Kunne ikke installere: %1$s</string> + <string name="wincomp_downloaded">Lastet ned</string> + <string name="wincomp_downloading">Laster ned %1$s</string> + <string name="wincomp_installing">Installerer: %1$s</string> + <string name="wincomp_reverting">Slår av…</string> + <string name="wincomp_download_size">%1$s nedlasting</string> + <string name="wincomp_next_launch">Trer i kraft neste gang spillet starter.</string> + <string name="mode_stretch169">Strekk 16:9-spill</string> + <string name="mode_stretch169_hint">På skjermer som er mer kvadratiske enn 16:9, strekkes spill til å fylle skjermen i stedet for å vise sorte striper. Gjelder fra neste sesjon.</string> + <string name="mode_audio">Lyd</string> + <string name="mode_directaudio">DirectAudio for spill</string> + <string name="mode_directaudio_hint">Går forbi PulseAudio for lavere ventetid.</string> + <string name="mode_mic">Mikrofon</string> + <string name="mode_mic_hint">For talechat.</string> + <string name="mode_network">Nettverk</string> + <string name="mode_wifi_discovery">Wi-Fi-oppdagelse</string> + <string name="mode_wifi_discovery_hint">Vis Wi-Fi-navn og nettverk i nærheten i Steam.</string> + <string name="mode_wifi_location_off">Posisjonstjenester er av.</string> + <string name="mode_wifi_permission_denied">Posisjonstillatelse ikke gitt.</string> + <string name="mode_wifi_open_settings">Åpne innstillinger</string> + <string name="mode_wifi_continue">Fortsett</string> + <string name="mode_wifi_explain_title">Slå på Wi-Fi-oppdagelse?</string> + <string name="mode_wifi_explain_text">Android krever posisjonstillatelse for å lese Wi-Fi-navn og oppdage nettverk i nærheten. DroidDeck bruker denne tillatelsen til å vise disse nettverkene i Steam. Vi følger ikke med på posisjonen din. Internett-tilgang fungerer uten denne tillatelsen.</string> + <string name="mode_storage">Spillagring</string> + <string name="mode_storage_auto">Automatisk – SD-kortet når det finnes, pluss Spill-mappene dine</string> + <string name="mode_storage_auto_none">Automatisk – Spill-mappene dine, pluss SD-kortet når det finnes (ingen nå)</string> + <string name="mode_storage_internal">Bare intern</string> + <string name="mode_storage_folder">Mappe: %1$s</string> + <string name="mode_second_library">Andre bibliotek</string> + <string name="mode_storage_note">Spill som strømmer ressurser fra SD-kort eller delt lagring kan hakke.</string> + <string name="mode_storage_diagnostics">Lagringsdiagnostikk</string> + <string name="mode_storage_diagnostics_hint">Lagrer tildelingsresultater og tidsmålinger for lagring i Del logger. Kan gjøre installasjoner tregere mens dette er på. Gjelder fra neste sesjon.</string> + <string name="mode_choose_folder">Velg en mappe…</string> + <string name="mode_custom_res_title">Egendefinert oppløsning</string> + <string name="mode_custom_res_text">Erstatter grensen og forholdet. En størrelse som ikke passer skjermen, gir sorte striper.</string> + <string name="mode_width">Bredde</string> + <string name="mode_height">Høyde</string> + <string name="mode_custom_res_range">Mellom 320×240 og 3840×2160.</string> + <plurals name="mode_added_count"> + <item quantity="one">%1$d spill</item> + <item quantity="other">%1$d spill</item> + </plurals> + <string name="common_ok">OK</string> + <string name="common_delete">Slett</string> + <string name="common_restore">Gjenopprett</string> + <string name="common_download">Last ned</string> + <string name="common_delete_named">Slett %1$s</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Kjøretidsdriver</string> + <string name="comp_runtime_driver_hint">Brukes av Steam, spill og skrivebordet. Gjelder fra neste sesjon.</string> + <string name="comp_import_turnip">Importer Turnip-zip…</string> + <string name="comp_display_driver">Skjermdriver</string> + <string name="comp_display_driver_hint">Tegner bilder til skjermen i begge moduser. Start appen på nytt for å bruke dette.</string> + <string name="comp_import_adrenotools">Importer en AdrenoTools-zip…</string> + <string name="comp_never">aldri</string> + <string name="comp_nightlies_checked">Nightlies sjekket %1$s</string> + <string name="comp_title">Komponenter</string> + <string name="comp_about">Om Komponenter</string> + <string name="comp_check_drivers">Se etter nye drivere</string> + <string name="comp_check_nightlies">Se etter nye pakker på Nightlies</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">Protonen som velges her, kjører hvert spill som ikke har valgt sin egen i Steam, og en endring gjort i Steam vises også her. Steam erstatter Valves Proton når den oppdaterer; originalene beholdes per build.</string> + <string name="comp_scope">Konfigurasjon</string> + <string name="comp_profile_configure">Konfigurer profil</string> + <string name="comp_profile">Profil</string> + <string name="comp_scope_general">Standardprofil</string> + <string name="comp_scope_general_detail">Brukes automatisk av spill uten overstyringer.</string> + <string name="comp_profile_game_detail">Lagrede innstillinger brukes automatisk når %1$s starter.</string> + <string name="comp_game_hint">Velg bare innstillingene dette spillet trenger. Alt annet fortsetter å bruke standardprofilen.</string> + <string name="comp_game_fex">FEX-forhåndsinnstilling</string> + <string name="comp_game_fex_inherited">Bruker forhåndsinnstillingen fra standardprofilen.</string> + <string name="comp_game_fex_override">Egendefinert forhåndsinnstilling lagret for dette spillet.</string> + <string name="comp_game_fex_custom">Egendefinerte FEX-variabler</string> + <string name="comp_game_use_general">Bruk standard</string> + <string name="comp_game_compatibility">Spillkompatibilitet</string> + <string name="comp_game_environment_hint">Proton-, DXVK-, VKD3D- og synkroniseringsalternativer for dette spillet.</string> + <string name="comp_game_proton_inherited">Bruker Protonen fra standardprofilen.</string> + <string name="comp_game_proton_override">Egendefinert Proton lagret for dette spillet via Steams kompatibilitetstilordning.</string> + <string name="comp_no_proton_short">Ingen Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Bruker komponentpakken fra standardprofilen fra %1$s, med mindre dette spillet velger en annen pakke.</string> + <string name="comp_game_component_general">Bruker standardprofilens komponentpakke.</string> + <string name="comp_game_component_override">Bruker bare %1$s for dette spillet.</string> + <string name="comp_game_runtime_options">Kjøretidsalternativer</string> + <string name="comp_game_environment_other_hint">Andre spillspesifikke Proton-, DXVK-, VKD3D-Proton- og synkroniseringsalternativer.</string> + <string name="comp_game_saved_next">Profilen til %1$s er lagret for neste spilløkt.</string> + <string name="game_no_steam_title">Start uten Steam</string> + <string name="game_no_steam_detail">For et spill som lukkes med én gang: starter det uten Protons Steam-oppsett. Steam-funksjoner som overlay er slått av for det.</string> + <string name="game_no_steam_on">På</string> + <string name="comp_game_proton_live">Proton-valget til %1$s er lagret og tilgjengelig i Steam nå.</string> + <string name="comp_game_proton_next">Proton-valget til %1$s er lagret. Start Steam på nytt før du starter spillet.</string> + <string name="comp_game_proton_not_runnable">%1$s kan ikke bruke den valgte Protonen på denne enheten.</string> + <string name="comp_game_load_failed">Kunne ikke laste profilen til %1$s: %2$s</string> + <string name="comp_game_save_failed">Spillprofilen kunne ikke lagres: %1$s</string> + <string name="comp_game_reset">Nullstill spillprofil</string> + <string name="comp_game_reset_title">Nullstille profilen til %1$s?</string> + <string name="comp_game_reset_body">Fjerne dette spillets overstyringer for Proton, FEX, komponentpakke, miljø, teksturfiltrering og Windows-komponenter? Det vil bruke standardprofilen.</string> + <string name="comp_game_reset_done">%1$s bruker nå standardprofilen.</string> + <string name="comp_game_reset_failed">Spillprofilen kunne ikke nullstilles helt: %1$s</string> + <string name="comp_default_live">%1$s kjører nå hvert spill som ikke har valgt sin egen Proton i Steam.</string> + <string name="comp_default_next">%1$s vil kjøre hvert spill som ikke har valgt sin egen Proton, fra neste gang Steam starter.</string> + <string name="comp_default_not_runnable">%1$s kan ikke starte spill her, så den kan ikke være standarden.</string> + <string name="comp_default_failed">Standard-Protonen kunne ikke lagres: %1$s</string> + <string name="comp_swapped">%1$s · %2$d byttet ut</string> + <string name="comp_all_original">%1$s · alt originalt</string> + <string name="comp_game_running_suffix">%1$s · spillet kjører</string> + <string name="comp_esync_pack">%1$s · droiddeck-esync-pakke</string> + <string name="comp_esync_wanted">%1$s · ingen droiddeck-esync-pakke ennå</string> + <string name="comp_esync_builtin">%1$s · esync innebygd, ingen pakke nødvendig</string> + <string name="comp_gpu_drivers">GPU-drivere</string> + <string name="comp_reapplied">%1$s · brukt på nytt ved start %2$s</string> + <string name="comp_next_queued">Neste: %1$s · etter at spillet avsluttes</string> + <string name="comp_in_use">%1$s i bruk: %2$s</string> + <string name="comp_waits">Venter til spillet avsluttes</string> + <string name="comp_applies_next_game">Gjelder neste gang et spill starter</string> + <string name="comp_import_zip">Importer .zip</string> + <string name="comp_reading">Leser Protoner…</string> + <string name="comp_no_proton">Ingen Proton installert ennå. Start Steam én gang for å laste ned dens ARM64-Proton, eller legg til GE-Proton, CachyOS eller DroidDeck-Proton fra Oppsett.</string> + <string name="comp_original">Original · %1$s</string> + <string name="comp_this_build">%1$s · denne builden</string> + <string name="comp_earlier_build">%1$s · tidligere build</string> + <string name="comp_installed_count">Installert · %1$d</string> + <string name="comp_nothing_stored">Ingenting lagret for %1$s ennå. Denne Protonens egne filer lagres som dens original.</string> + <string name="comp_waits_long">Et spill kjører på denne Protonen, så dette venter til det avsluttes.</string> + <string name="comp_applies_long">Gjelder neste gang et spill starter.</string> + <string name="comp_swap_title">Bytte ut %1$s?</string> + <string name="comp_swap_body">Sette %1$s inn i %2$s? De originale filene forblir lagret. %3$s</string> + <string name="comp_restore_title">Gjenopprette %1$s?</string> + <string name="comp_restore_body">Sette %1$s-originalen tilbake i %2$s? %3$s</string> + <string name="comp_delete_package_body">Fjerner pakken fra appen. Du kan laste den ned igjen.</string> + <string name="comp_delete_original_title">Slette denne originalen?</string> + <string name="comp_delete_original_body">Denne %1$s-originalen er fra en tidligere build av %2$s. Den gjeldende buildens original beholdes alltid.</string> + <string name="comp_nightlies_count">På Nightlies · %1$d</string> + <string name="comp_refresh_to_list">Oppdater for å liste pakker på Nightlies.</string> + <string name="comp_nothing_new">Ingenting nytt siden siste sjekk.</string> + <string name="comp_import_wcp">Importer .wcp</string> + <string name="comp_restore_original">Gjenopprett denne Protonens original</string> + <string name="comp_restore_original_body">Sette %1$ss original %2$s tilbake?</string> + <string name="comp_swap">Bytt ut</string> + <string name="comp_about_text">FEX, DXVK og VKD3D-Proton for hver Proton som Steam kjører spill med.\n\nVelg en installert rad for å bytte den inn. Det gjelder neste gang et spill starter, eller etter at det kjørende spillet avsluttes.\n\nValgene dine kontrolleres på nytt før hver start og settes tilbake hvis noe har endret dem. Hver Protons originale filer beholdes, så Steam-oppdateringer mister dem aldri.</string> + <string name="comp_tag_original">Original</string> + <string name="comp_tag_in_use">I bruk</string> + <string name="comp_tag_new">Ny</string> + <string name="comp_tag_stored">Lagret</string> + <string name="perf_title">Ytelse</string> + <string name="perf_lede">CPU-kjerner, sesjonsfikser og vertsnavnet. Gjelder fra neste sesjon.</string> + <string name="perf_client_cores_group">Steam-klientens kjerner</string> + <string name="perf_override">Overstyr Steams kjernevalg</string> + <string name="perf_override_hint">Fester Steam, dets grensesnitthjelper og gamescope til kjernene under.</string> + <string name="perf_client_cores">Klientkjerner</string> + <string name="perf_client_cores_on">Kjerner for Steam.</string> + <string name="perf_client_cores_off">Slå på overstyringen for å velge.</string> + <string name="perf_game_cores_group">Spillkjerner</string> + <string name="perf_game_cores">Spillkjerner</string> + <string name="perf_game_cores_hint">Kjerner for spill. Med alle valgt, planlegger Android fritt.</string> + <string name="perf_client_ui">Klientgrensesnitt</string> + <string name="perf_glthread">Trådet GL</string> + <string name="perf_glthread_hint">Kan gjøre Steam-menyer mer responsive.</string> + <string name="perf_zink">Zink: lazy descriptors</string> + <string name="perf_zink_hint">Anbefales for drivere uten descriptor buffers.</string> + <string name="perf_noglerror">Hopp over GL-feilkontroller</string> + <string name="perf_noglerror_hint">Slår av GL-validering per kall.</string> + <string name="perf_gsrealtime">gamescope: realtime GPU-kø</string> + <string name="perf_gsrealtime_hint">Prioriterer kompositørens GPU-arbeid. Kan jevne ut bildetakten, på bekostning av spillene.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">Hold GPU-en på toppfrekvens</string> + <string name="perf_gpuclock_hint">Bare Adreno. Færre hikk, mer batteribruk og varme.</string> + <string name="perf_fixes">Sesjonsfikser</string> + <string name="perf_sysmem">Turnip: sysmem-rendering</string> + <string name="perf_sysmem_hint">Raskere for Steam og de fleste spill. Påkrevd på Adreno 710/720/722.</string> + <string name="perf_xalia">Hopp over Steams xalia-hjelper</string> + <string name="perf_fastsync">droiddeck-ntsync (eksperimentell)</string> + <string name="perf_fastsync_hint">En erstatning i brukerrom for Linux-driveren ntsync, for testing. Mens dette er på, bruker Proton den i stedet for droiddeck-esync og droiddeck-fsync. Fungerer med Proton Experimental ARM64, GE-Proton og proton-cachyos. Gjelder fra neste sesjon.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync først</string> + <string name="perf_fsync_hint">Kjører Protons egen fsync på de uendrede Proton-filene, med futex_waitv levert i brukerrom, så den trenger ingen pakke og fortsetter å fungere i det Steam oppdaterer Proton. Mens dette er på, bruker spill den foran droiddeck-esync, med unntak av de få Proton holder utenfor fsync. Gjelder fra neste sesjon.</string> + <string name="perf_esync_hint">Spillenes Windows-tråder venter på hverandre gjennom eventfd og delt minne i stedet for rundturer via wineserver. Bruker en droiddeck-esync-pakke laget for nettopp den Proton-builden; pakker hentes automatisk. Til det finnes en for en ny Proton-build, steppes droiddeck-fsync inn i stedet. Gjelder fra neste sesjon.</string> + <string name="perf_xalia_hint">Slår av Protons hjelper for kontrollernavigasjon, som koster CPU i hvert spill.</string> + <string name="perf_seccomp">Kjør proot uten seccomp</string> + <string name="perf_seccomp_hint">Kan fikse feil om manglende systemkall på noen kjerner. Kan redusere ytelsen.</string> + <string name="perf_fastpath">proot-hurtigvei</string> + <string name="perf_fastpath_needs_seccomp">Krever prootens seccomp-filter.</string> + <string name="perf_fastpath_hint">Raskere filoppslag og mindre hakking. Slå av hvis et program ikke finner filene sine.</string> + <string name="perf_identity">Sesjonsidentitet</string> + <string name="perf_hostname">Vertsnavn</string> + <string name="perf_hostname_hint">Navnet Steam ser for denne enheten. Tomt gjenoppretter %1$s.</string> + <string name="perf_hostname_invalid">Bare bokstaver, tall og bindestreker inni navnet, opptil 63 tegn.</string> + <string name="perf_phantom_warning">Android er satt til å avslutte denne sesjonen</string> + <string name="driver_installed">Installert</string> + <string name="driver_available">Tilgjengelig for nedlasting</string> + <string name="driver_nothing_new">Ingenting nytt siden siste sjekk.</string> + <string name="driver_restore_builtin">Gjenopprett innebygde drivere</string> + <string name="driver_delete_builtin">Innebygd i appen: dette fjerner de utpakkede filene og skjuler den. Gjenopprett innebygde drivere bringer den tilbake.</string> + <string name="driver_delete_bundle">Halvparten av en Android + Linux-pakke: begge halvdelene fjernes. En driver i bruk faller tilbake til standarden.</string> + <string name="driver_delete_plain">Filene fjernes fra appen.</string> + <string name="driver_delete_in_use">Filene fjernes. Den er i bruk, så standarden tar over.</string> + <string name="drawer_page_display">Skjerm</string> + <string name="drawer_page_effects">Effekter</string> + <string name="drawer_page_controls">Kontroller</string> + <string name="drawer_page_components">Komponenter</string> + <string name="drawer_page_controller">Kontroller</string> + <string name="drawer_page_settings">Innstillinger</string> + <string name="drawer_page_desc">%1$s-side</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Skrivebord</string> + <string name="drawer_steam_menu">Steam-meny</string> + <string name="drawer_quick_access">Hurtigmeny</string> + <string name="drawer_open_qam">Åpne Hurtigmenyen</string> + <string name="drawer_prev_page">Forrige side</string> + <string name="drawer_next_page">Neste side</string> + <string name="drawer_hud">Ytelses-HUD</string> + <string name="drawer_scaling">Skaleringsmodus</string> + <string name="drawer_scaling_sharpness">Skaleringsskarphet</string> + <string name="drawer_effects">Skjermeffekter</string> + <string name="drawer_effects_hint">Live. Koster litt GPU-tid mens en effekt er på.</string> + <string name="drawer_look">Forhåndsinnstillinger</string> + <string name="drawer_look_custom">Egendefinert</string> + <string name="drawer_effects_advanced">Avansert</string> + <string name="widgets_expanded">Utvidet</string> + <string name="widgets_collapsed">Sammenslått</string> + <string name="drawer_cas">Skarphet (CAS)</string> + <string name="drawer_cas_level">Skarphetsnivå</string> + <string name="drawer_fake_hdr">Falsk HDR</string> + <string name="drawer_deband">Fargebåndfjerning</string> + <string name="drawer_deband_strength">Styrke på fargebåndfjerning</string> + <string name="drawer_brightness">Lysstyrke</string> + <string name="drawer_contrast">Kontrast</string> + <string name="drawer_gamma">Gamma</string> + <string name="drawer_saturation">Metning</string> + <string name="drawer_fxaa">FXAA (kantutjevning)</string> + <string name="drawer_toon">Tegnefilm-konturer</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Teksturfiltrering</string> + <string name="drawer_texture_hint">DirectX 9–11-spill, fra neste gang de starter.</string> + <string name="drawer_anisotropy">Anisotropisk filtrering</string> + <string name="drawer_texture_sharpness">Teksturskarphet</string> + <string name="drawer_texture_sharpness_note">En negativ mip-skjevhet: skarpere teksturer, litt glitring. Auto følger hvor mye sesjonen forstørres til skjermen.</string> + <string name="drawer_touch_auto">Auto (%1$s)</string> + <string name="drawer_keyboard">Tastatur</string> + <string name="drawer_pc_keyboard">PC-tastatur</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">Andre skjerm</string> + <string name="drawer_second_screen_mode">Modus</string> + <string name="drawer_second_screen_shows">Viser</string> + <string name="drawer_android_apps">Android-apper</string> + <string name="drawer_launch_app">Start en app</string> + <string name="common_hide">Skjul</string> + <string name="common_show">Vis</string> + <string name="drawer_no_apps">Ingen startbare apper</string> + <string name="drawer_session_behavior">Sesjonsoppførsel</string> + <string name="drawer_suspend_hint">Gjelder nå og for fremtidige sesjoner i denne modusen.</string> + <string name="drawer_next_session">Neste sesjon</string> + <string name="drawer_support">Support</string> + <string name="drawer_logs">Sesjonslogger</string> + <string name="drawer_logs_hint">Legg ved en feilrapport</string> + <string name="drawer_share_logs">Del logger</string> + <string name="drawer_background">Bakgrunn</string> + <string name="drawer_stop">Avslutt</string> + <string name="drawer_stop_title">Avslutte sesjonen?</string> + <string name="drawer_running_now">Kjører nå</string> + <string name="drawer_no_game">Ingen spill kjører på en Proton</string> + <string name="drawer_selected_profile">Spillprofil</string> + <string name="drawer_selected_profile_none">Profildetaljer er ikke tilgjengelige ennå.</string> + <string name="drawer_profile_refresh_failed_initial">Kunne ikke laste spillprofiler. Prøver automatisk igjen.</string> + <string name="drawer_profile_refresh_failed_stale">Kunne ikke oppdatere spillprofiler. Viser de siste tilgjengelige dataene; prøver automatisk igjen.</string> + <string name="drawer_profile_inspect_hint">Se og rediger de lagrede innstillingene som brukes neste gang.</string> + <string name="drawer_profile_default">Bruker standardprofilen</string> + <string name="drawer_profile_custom">Egendefinert profil</string> + <string name="drawer_component_inherited">Bruker %1$s fra standardprofilen.</string> + <string name="drawer_game_components_note">Endringer lagres i profilen til %1$s og gjelder neste gang spillet starter.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d spilloverstyring</item> + <item quantity="other">%1$d spilloverstyringer</item> + </plurals> + <string name="drawer_no_proton">Ingen Proton er installert ennå.</string> + <string name="drawer_game_running">spill kjører</string> + <string name="drawer_reapplied">brukt på nytt ved start %1$s</string> + <string name="drawer_original">Original</string> + <string name="drawer_components_note">Importer og slett på Komponenter-siden.</string> + <string name="setup_check_done">Ferdig</string> + <string name="setup_check_warn">Krever oppmerksomhet</string> + <string name="setup_check_busy">Arbeider</string> + <string name="setup_limit_adb_failed">Kontroller IP-adressen og porten for trådløs feilsøking, eller par på nytt hvis Android har fjernet denne enheten.</string> + <string name="setup_install">Installer</string> + <string name="setup_manage">Behandle</string> + <string name="setup_tab_overview">Oversikt</string> + <string name="setup_tab_controller">Kontroller</string> + <string name="setup_tab_session">Sesjon</string> + <string name="setup_tab_launcher">Launcher</string> + <string name="setup_title">Oppsett</string> + <string name="setup_all_set">● Alt klart</string> + <string name="setup_n_ready">%1$d av %2$d klare</string> + <string name="setup_system_check">Systemkontroll</string> + <string name="setup_gpu_supported">Enheten er støttet</string> + <string name="setup_gpu_untested">Utestet GPU</string> + <string name="setup_gpu_unsupported">GPU ikke støttet</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">Steam krever en Adreno-driver; %1$s kan vise en svart skjerm</string> + <string name="setup_runtime">Linux-kjøretid</string> + <string name="setup_runtime_progress">%1$s · %2$d%%</string> + <string name="setup_runtime_missing">Ikke installert · omtrent 3 GB, installeres ved første Spill</string> + <string name="setup_installed">Installert</string> + <string name="setup_runtime_update">%1$s · oppdatering tilgjengelig</string> + <string name="setup_runtime_current">%1$s · oppdatert</string> + <string name="setup_limit">Grense for underprosesser</string> + <string name="setup_limit_on">På · Android kan avslutte Steam. Det tar et minutt å slå den av.</string> + <string name="setup_limit_unset">Ikke angitt · Android kan avslutte Steam. Det tar et minutt å slå den av.</string> + <string name="setup_limit_unknown">Kunne ikke kontrolleres · det tar et minutt å slå den av.</string> + <string name="setup_fix_it">Fiks det</string> + <string name="setup_details">Detaljer</string> + <string name="setup_dev_options">Utvikleralternativer</string> + <string name="setup_use_wireless">Bruk trådløs feilsøking</string> + <string name="setup_check_again">Kontroller igjen</string> + <string name="setup_limit_turn_on">Slå på grensen</string> + <string name="setup_account">Steam-konto</string> + <string name="setup_signed_in_offline">Innlogget som %1$s · frakoblet modus</string> + <string name="setup_signed_in">Innlogget som %1$s</string> + <string name="setup_sign_in">Trykk på Spill og logg inn på Steam</string> + <string name="setup_tools">Verktøy</string> + <string name="setup_controller">Kontroller</string> + <string name="setup_controller_unavailable">Kontrollerinnstillinger er ikke tilgjengelige.</string> + <string name="setup_session">Sesjon</string> + <string name="setup_logs">Sesjonslogger</string> + <string name="setup_logs_hint">Lagret etter hver sesjon</string> + <string name="setup_agent_commands">Feilsøkingsverktøy</string> + <string name="setup_latest_logs">Nyeste sesjonslogger</string> + <string name="setup_saved_logs">Lagrede sesjonslogger</string> + <string name="setup_saved_logs_hint">Beholder de %1$d nyeste</string> + <string name="setup_clear_logs">Tøm logger</string> + <string name="setup_offline">Frakoblet modus</string> + <string name="setup_sign_in_first">Logg inn på Steam først</string> + <string name="setup_launcher">Launcher</string> + <string name="setup_language">Språk</string> + <string name="setup_language_hint">Steam bruker det også, fra neste gang den starter.</string> + <string name="setup_language_system">Systemstandard (%1$s)</string> + <string name="setup_language_system_short">Systemstandard</string> + <string name="setup_app_scale">Appskalering</string> + <string name="setup_app_scale_hint">Tekst og kontroller i DroidDeck</string> + <string name="setup_app_scale_default">%1$d%% (Standard)</string> + <string name="setup_theme">Tema</string> + <string name="setup_theme_hint">Launcherens utseende</string> + <string name="setup_home">Bruk som Hjem-skjerm</string> + <string name="setup_home_on">DroidDeck kan være Hjem-appen</string> + <string name="setup_home_off">DroidDeck tilbys aldri som en Hjem-app</string> + <string name="setup_orientation">Retning</string> + <string name="setup_orientation_auto">Automatisk liggende</string> + <string name="setup_orientation_landscape">Liggende</string> + <string name="setup_orientation_reverse">Omvendt liggende</string> + <string name="setup_fullscreen">Fullskjerm</string> + <string name="setup_fullscreen_on">Skjuler Androids status- og navigasjonslinjer</string> + <string name="setup_fullscreen_off">Viser Androids status- og navigasjonslinjer</string> + <string name="setup_default_home">Standard Hjem-app</string> + <string name="setup_choose_home">Velg en Hjem-app</string> + <string name="setup_choose">Velg</string> + <string name="setup_linux_apps">Linux-apper (beta)</string> + <string name="setup_tool_files">Filer</string> + <string name="setup_tool_files_hint">Bla gjennom og behandle filer</string> + <string name="setup_tool_protons">Proton-versjoner</string> + <string name="setup_tool_protons_hint">Installer ARM64 Proton-builder</string> + <string name="setup_tool_performance">Ytelse</string> + <string name="setup_tool_performance_hint">Tildeling av CPU-kjerner</string> + <string name="setup_tool_roms">ROM-mappe</string> + <string name="setup_tool_roms_hint">Hvor emulatorspill lagres</string> + <string name="setup_card_components">Komponenter</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Kontroller</string> + <string name="setup_card_controls_hint">Knappetilordning</string> + <string name="store_tab_discover">Utforsk</string> + <string name="store_tab_search">Søk</string> + <string name="store_tab_installed">Installert</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Butikk</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak klar</string> + <string name="store_needs_runtime">Installer Linux-kjøretiden fra Oppsett først.</string> + <string name="store_setup_title">Sett opp Flatpak</string> + <string name="store_setup_text">Legger til Flatpak og kobler det til Flathub (omtrent 20 MB). Den første appen laster også ned kjøretiden sin.</string> + <string name="store_setting_up">Setter opp…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Oppdateringer</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d%%</string> + <string name="store_starting">Starter…</string> + <string name="store_categories">Kategorier</string> + <string name="store_unreachable">Kunne ikke nå Flathub. Kontroller tilkoblingen og prøv igjen.</string> + <string name="store_try_again">Prøv igjen</string> + <string name="store_search_label">Søk på Flathub</string> + <string name="store_search_placeholder">Firefox, spill, emulatorer…</string> + <string name="store_search">Søk</string> + <string name="store_all">Alle</string> + <string name="store_searching">Søker…</string> + <string name="store_search_hint">Søk etter navn eller velg en kategori. Bare ARM64-apper er listet.</string> + <string name="store_no_results">Ingen ARM64-apper på Flathub matcher det.</string> + <string name="store_results">Resultater</string> + <string name="store_check_updates">Se etter oppdateringer</string> + <string name="store_update_all">Oppdater alle (%1$d)</string> + <string name="store_all_current">● Alt er oppdatert</string> + <string name="store_no_apps">Ingen apper ennå. Finn noen under Utforsk eller Søk.</string> + <string name="store_setup_first_note">Sett opp Flatpak for å installere apper.</string> + <string name="store_update_available_prefix">Oppdatering tilgjengelig · %1$s</string> + <string name="store_open">Åpne</string> + <string name="store_details">Detaljer</string> + <string name="store_remove">Fjern</string> + <string name="store_press_again">Trykk igjen for å fjerne</string> + <string name="store_installing">Installerer…</string> + <string name="store_install">Installer</string> + <string name="store_setup_first">Sett opp Flatpak først</string> + <string name="store_download_size">%1$s nedlasting</string> + <string name="store_installed_chip">● Installert</string> + <string name="store_details_failed">Kunne ikke laste denne appens Flathub-side.</string> + <string name="store_loading">Laster fra Flathub…</string> + <string name="store_no_arm">Ingen ARM64-build på Flathub, så den kan ikke installeres her.</string> + <string name="store_screenshots">Skjermbilder</string> + <string name="store_about">Om</string> + <string name="store_app_id">App-ID</string> + <string name="store_license">Lisens</string> + <string name="store_runtime">Kjøretid</string> + <string name="store_installed_size">Installert størrelse</string> + <string name="store_installed_size_value">%1$s (pluss den delte kjøretiden)</string> + <string name="store_website">Nettside</string> + <string name="store_verified">Verifisert</string> + <string name="upd_title">Oppdateringer</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Sjekket %1$s</string> + <string name="upd_not_checked">Ikke sjekket ennå</string> + <string name="upd_perm_title">La DroidDeck oppdatere seg selv</string> + <string name="upd_perm_text">På neste skjerm, slå på Tillat fra denne kilden, og kom så tilbake. Oppdateringen fortsetter av seg selv.</string> + <string name="upd_open_settings">Åpne innstillinger</string> + <string name="upd_not_now">Ikke nå</string> + <string name="upd_signed_status">Signert annerledes</string> + <string name="upd_signed_headline">Kan ikke oppdatere direkte</string> + <string name="upd_signed_detail">Denne kopien er ikke signert med DroidDecks utgivelsesnøkkel, så oppdateringer kan ikke installeres over den. Avinstaller den for å bytte.</string> + <string name="upd_available">Oppdatering tilgjengelig</string> + <string name="upd_blocked_status">Kan ikke installere</string> + <string name="upd_switch">Klar til å bytte</string> + <string name="upd_ahead_status">Foran Stable</string> + <string name="upd_ahead_headline">Du er foran Stable</string> + <string name="upd_ahead_detail">Denne builden er nyere enn den nyeste Stable. Du flyttes til Stable med dens neste utgivelse.</string> + <string name="upd_gone_status">Testen er avsluttet</string> + <string name="upd_gone_headline">Denne testen er avsluttet</string> + <string name="upd_gone_detail">Fiksen ble slått sammen eller forkastet. Følg Preview for de nyeste fiksene.</string> + <string name="upd_current_status">Oppdatert</string> + <string name="upd_current_headline">Du har den nyeste %1$s</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Oppdater%1$s</string> + <string name="upd_install_button">Installer%1$s</string> + <string name="upd_install_anyway">Installer Stable %1$s likevel</string> + <string name="upd_follow_preview">Følg Preview</string> + <string name="upd_stop_session">Avslutt den kjørende sesjonen for å oppdatere.</string> + <string name="upd_less">Mindre</string> + <string name="upd_more">Mer</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Ny Preview-build</string> + <string name="upd_new_test">PR #%1$d-testbuild</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">OPPDATERINGSKANAL</string> + <string name="upd_stable">Stable</string> + <string name="upd_stable_hint">Testede utgivelser, for de fleste</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Preview</string> + <string name="upd_preview_hint">Nyeste fikser, før Stable</string> + <string name="upd_preview_current">Oppdatert</string> + <string name="upd_tests">Testbuilder</string> + <string name="upd_tests_hint">Prøv en fiks før den blir utgitt</string> + <string name="upd_tests_none">Ingen akkurat nå</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Ikke bygget for denne kopien av DroidDeck</string> + <string name="upd_updated">Oppdatert %1$s</string> + <string name="upd_test_name">PR #%1$d-testen</string> + <string name="upd_while_ago">en stund siden</string> + <string name="upd_just_now">akkurat nå</string> + <plurals name="upd_tests_count"> + <item quantity="one">%1$d å prøve</item> + <item quantity="other">%1$d å prøve</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="one">Endringer siden din build · %1$d build bak</item> + <item quantity="other">Endringer siden din build · %1$d builder bak</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="one">Nylige endringer · %1$d build</item> + <item quantity="other">Nylige endringer · %1$d builder</item> + </plurals> + <string name="gate_title">En Android-innstilling å endre</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Android stopper apper som starter mange bakgrunnsprosesser, og Steam starter dusinvis. Til dette endres, avsluttes spill uten en feilmelding.</string> + <string name="gate_fix_for_me">Fiks det for meg</string> + <string name="gate_toggle_steps">Slå på «Disable child process restrictions», og kom så tilbake. La Utvikleralternativer stå på, ellers slår bryteren seg av igjen.</string> + <string name="gate_open_dev">Åpne Utvikleralternativer</string> + <string name="gate_no_toggle">Denne Android-versjonen har ingen bryter for det, så DroidDeck endrer det via trådløs feilsøking. Tar omtrent ett minutt, ingen datamaskin nødvendig.</string> + <string name="gate_needs_wifi">Trådløs feilsøking krever Wi-Fi. Hvilket nettverk som helst fungerer; det krever ikke internett.</string> + <string name="gate_needs_notifications">Koblingskoden kommer i en varsling. Tillat varslinger for DroidDeck, eller bruk Andre måter.</string> + <string name="gate_allow_notifications">Tillat varslinger</string> + <string name="gate_step1">Åpne Innstillinger → Utvikleralternativer → Trådløs feilsøking og slå det på</string> + <string name="gate_step2">Trykk på «Par enhet med koblingskode»</string> + <string name="gate_step3">Skriv koden inn i DroidDeck-varslingen</string> + <string name="gate_waiting">Venter på koblingsvarselet…</string> + <string name="gate_code_needed">Varsel funnet. Skriv inn koden i varslingen.</string> + <string name="gate_status">Status</string> + <string name="gate_polling">Kontrollerer hvert 2. sekund. Denne siden lukkes når det er av.</string> + <string name="gate_other_ways">Andre måter</string> + <string name="gate_other_toggle">Trådløs feilsøking, manuell adresse, ADB fra datamaskin</string> + <string name="gate_other_no_toggle">Manuell adresse, ADB fra datamaskin, Utvikleralternativer</string> + <string name="gate_use_wireless_instead">Bruk trådløs feilsøking i stedet</string> + <string name="gate_some_roms">Noen ROM-er legger til bryteren likevel. Se etter «child process» i Utvikleralternativer.</string> + <string name="gate_manual_hint">Hvis varslingen ikke fungerer, skriv inn varselets adresse og kode selv.</string> + <string name="gate_enter_manually">Skriv inn adresse manuelt</string> + <string name="gate_from_computer">Fra en datamaskin med ADB:</string> + <string name="gate_copy_command">Kopier kommando</string> + <string name="gate_choose_display">Velg hvilken skjerm Android-innstillinger skal åpnes på.</string> + <string name="gate_main_screen">Hovedskjerm</string> + <string name="wadb_finding_port">Koblet sammen. Finner porten for trådløs feilsøking…</string> + <string name="wadb_enter_port">Koblet sammen. Skriv inn porten fra Innstillinger.</string> + <string name="wadb_applying">Enhet funnet. Bruker innstillingen…</string> + <string name="wadb_apply_failed">Kunne ikke bruke den automatisk. Kontroller adressen under og prøv igjen. %1$s</string> + <string name="wadb_title_wireless">Trådløs feilsøking</string> + <string name="wadb_title_done">Grensen for underprosesser er oppdatert</string> + <string name="wadb_title">Endre grensen for underprosesser</string> + <string name="wadb_lede_compact0">Hvis alternativet er borte, par over trådløs feilsøking.</string> + <string name="wadb_lede_compact1">Koblet sammen. Kontroller IP-adressen og porten i Trådløs feilsøking.</string> + <string name="wadb_lede_done">Grensen for underprosesser ble oppdatert.</string> + <string name="wadb_lede0">Bruk steg 2 hvis Utvikleralternativer ikke har en innstilling for underprosesser. Åpne Utvikleralternativer → Trådløs feilsøking → Par enhet med koblingskode.</string> + <string name="wadb_lede1">Koblet sammen. Bruk adressen under, eller oppdater den fra Trådløs feilsøking → IP-adresse og port.</string> + <string name="wadb_lede_done_long">Grensen for underprosesser ble oppdatert via trådløs feilsøking.</string> + <string name="wadb_confirmed_on">Android bekreftet at grensen er på.</string> + <string name="wadb_confirmed_off">Android bekreftet at grensen er av.</string> + <string name="wadb_pair_group">2 · Koble sammen trådløs feilsøking</string> + <string name="wadb_pair_address">IP-adresse og port for koblingsvarsel</string> + <string name="wadb_pin">Koblingskode (PIN)</string> + <string name="wadb_pin_placeholder">6 tall</string> + <string name="wadb_pair">Koble sammen</string> + <string name="wadb_apply_group">2 · Bruk via trådløs feilsøking</string> + <string name="wadb_connect_address">IP-adresse og port for tilkobling</string> + <string name="wadb_pair_again">Koble sammen igjen</string> + <string name="wadb_turn_off">Slå av grensen</string> + <string name="wadb_other_ways">Andre måter å endre det</string> + <string name="wadb_dev_try_first">1 · Utvikleralternativer (prøv først)</string> + <string name="wadb_restrict_on">Slå på «Restrict child processes».</string> + <string name="wadb_restrict_off">Slå av «Restrict child processes».</string> + <string name="wadb_computer_last">3 · ADB fra datamaskin (siste utvei)</string> + <string name="wadb_only_if">Bare hvis trådløs feilsøking ikke er tilgjengelig.</string> + <string name="wadb_computer_group">3 · ADB fra datamaskin (siste utvei)</string> + <string name="ctrl_osc_hint">Når skjermkontrollene vises i en sesjon</string> + <string name="ctrl_color">Farge</string> + <string name="ctrl_color_hint">Fargetone på skjermknappene</string> + <string name="ctrl_custom">Egendefinert</string> + <string name="ctrl_opacity">Opasitet</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">Knappestørrelse</string> + <string name="ctrl_size_hint" formatted="false">100% er standardstørrelsen</string> + <string name="ctrl_stick_click">Stikke-klikk</string> + <string name="ctrl_stick_click_hint">Dobbelttrykk og hold en stikke for L3 eller R3</string> + <string name="ctrl_adaptive">Adaptive stikker</string> + <string name="ctrl_rumble">Spillvibrasjon</string> + <string name="ctrl_steam_button">Steam-knapp</string> + <string name="ctrl_qam_button">QAM-knapp</string> + <string name="ctrl_keyboard_button">Tastatur-knapp</string> + <string name="ctrl_select_steam">Trykk på Select for å åpne Steam-menyen</string> + <string name="ctrl_select_steam_hint">I stedet for View/Select.</string> + <string name="setup_animations">Animasjoner</string> + <string name="ctrl_adaptive_hint">Stikker vises der du trykker og skjules når du slipper</string> + <string name="ctrl_layout">Oppsett</string> + <string name="ctrl_layout_custom">Egendefinerte posisjoner lagret</string> + <string name="ctrl_layout_default">Tilpasset denne skjermen</string> + <string name="ctrl_edit">Rediger</string> + <string name="ctrl_reset">Nullstill</string> + <string name="ctrl_mapping">Knappetilordning</string> + <string name="ctrl_mapping_default">Hver knapp sender sin egen inndata</string> + <string name="ctrl_mapping_count">%1$d av %2$d knapper omtilordnet</string> + <string name="ctrl_configure">Konfigurer</string> + <string name="ctrl_reset_all">Nullstill kontroller</string> + <string name="ctrl_reset_all_hint">Gjenoppretter alle kontrollerinnstillinger og oppsettet</string> + <string name="ctrl_mapping_lede">Hva hver skjermknapp sender til spillet. Skjult fjerner knappen.</string> + <string name="ctrl_onscreen_buttons">Skjermknapper</string> + <string name="ctrl_defaults">Standardverdier</string> + <string name="ctrl_reset_mapping">Nullstill tilordning</string> + <string name="content_android_apps">Android-apper</string> + <string name="content_apps_count">%1$d apper</string> + <string name="content_no_android_apps">Fant ingen startbare Android-apper.</string> + <string name="content_desktop_installed">● Skrivebord installert</string> + <string name="content_desktop_first_open">Installeres ved første åpning</string> + <string name="content_available">Tilgjengelig for installasjon</string> + <string name="content_not_installed">Ikke installert.</string> + <string name="content_not_installed_suffix">%1$s · ikke installert</string> + <string name="content_open_named">Åpne %1$s</string> + <string name="content_games">Spill</string> + <string name="content_choose_roms">Velg en ROM-mappe.</string> + <string name="content_retroarch_roms">Bla til /root/ROMs i RetroArch.</string> + <string name="content_add_roms">Legg %1$s-spill til i ROMs/%2$s.</string> + <string name="content_install_named">Installer %1$s</string> + <string name="content_stop_to_install">Avslutt sesjonen for å installere</string> + <string name="content_runtime_required">Kjøretid nødvendig</string> + <string name="content_loading_details">Laster installasjonsdetaljer…</string> + <string name="content_details_unavailable">Installasjonsdetaljer er ikke tilgjengelige. Prøv igjen senere.</string> + <string name="content_needs_runtime">Installer Linux-kjøretiden fra Oppsett først.</string> + <string name="content_stop_first">Avslutt den kjørende sesjonen for å installere skrivebordsapper.</string> + <string name="content_install_into">Installerer %1$s i Linux-skrivebordet.</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">Å skjule den fjerner den fra Skrivebord. Filene ligger fortsatt i Linux-kjøretiden.</string> + <string name="content_rom_gone">Det spillet er ikke lenger i ROM-mappen.</string> + <string name="content_desktop_system">Skrivebord · %1$s</string> + <string name="content_launch_in">Start i %1$s</string> + <string name="content_also_in">Også i %1$s</string> + <string name="content_page_gone">Den siden finnes ikke lenger. Velg en del til venstre.</string> + <string name="content_open_desktop">Åpne skrivebord</string> + <string name="content_install_open_desktop">Installer og åpne skrivebord</string> + <string name="desktop_remove_title">Avinstaller skrivebord</string> + <string name="desktop_remove_text">Fjern skrivebordet og appene det kom med. Behold spill, tillagte apper, innstillinger og delte biblioteker.</string> + <string name="desktop_removing">Fjerner skrivebord</string> + <string name="desktop_removed">Skrivebord fjernet</string> + <string name="desktop_cleaning_legacy">Fjerner gamle LXQt-filer</string> + <string name="desktop_removal_unknown">Oppdater DroidDeck før du fjerner denne skrivebordsversjonen.</string> + <string name="content_linux_desktop">Linux-skrivebord</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox og emulatorene dine</string> + <string name="games_empty">Spill du installerer i Steam, vises her etter sesjonen.</string> + <string name="games_play_steam">Spill Steam</string> + <string name="games_launch_settings">Startinnstillinger</string> + <string name="games_launch">Start</string> + <string name="games_files">Spillfiler</string> + <string name="games_prefix">Proton-prefiks</string> + <string name="games_saves_looking">Ser etter lagringer…</string> + <string name="games_saves_none">Fant ingen lagringsmappe på navn. Eksport tar hele brukermappen.</string> + <string name="games_saves_folders">%1$d lagringsmapper</string> + <string name="games_saves_summary">%1$s · %2$d filer · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">Behandle lagringer</string> + <string name="games_saves">Spilllagringer</string> + <string name="games_import_saves">Importer lagringer…</string> + <string name="games_export_gamehub">Eksporter som GameHub-zip…</string> + <string name="games_export_winlator">Eksporter som Winlator-zip…</string> + <string name="games_open_save_folder">Åpne lagringsmappe</string> + <string name="games_never_played">Aldri spilt</string> + <string name="games_runtime_busy_percent">Kjøretid opptatt · %1$d%%</string> + <string name="games_runtime_busy">Kjøretid opptatt</string> + <string name="games_runtime_first_play">Kjøretiden installeres ved første Spill</string> + <string name="games_runtime_update">Kjøretidsoppdatering tilgjengelig</string> + <string name="games_runtime_ready">● Kjøretid klar</string> + <string name="games_last_played">Sist spilt %1$s</string> + <string name="games_internal">Intern lagring</string> + <string name="games_added">Lagt til spill</string> + <string name="games_custom">Egendefinert spill</string> + <string name="games_store_storage">Spillagring</string> + <string name="gpu_auto_hint">Det beste paret for denne GPU-en, holdt oppdatert</string> + <string name="gpu_manual_hint">Velg et par selv</string> + <string name="gpu_checking">Ser etter drivere…</string> + <string name="gpu_none_listed">Ingen drivere listet ennå. Oppdater for å se etter dem.</string> + <string name="gpu_hide_other">Skjul drivere for andre GPU-er</string> + <string name="gpu_show_other">Vis drivere for andre GPU-er</string> + <string name="gpu_advanced">Avansert</string> + <string name="gpu_set_with_display">Angis sammen med skjermdriveren: %1$s bærer begge.</string> + <string name="gpu_set_with_runtime">Angis sammen med kjøretidsdriveren: %1$s bærer begge.</string> + <string name="gpu_runtime_hint">Brukes av spill og skrivebordet.</string> + <string name="gpu_runtime_default">Kjøretidsstandard</string> + <string name="gpu_display_hint">Tegner bilder til skjermen. Gjelder etter at DroidDeck starter på nytt.</string> + <string name="gpu_auto_by_gpu">Auto – valgt etter GPU</string> + <string name="gpu_manage">Last ned, importer eller slett…</string> + <string name="gpu_in_use_caps">I BRUK</string> + <string name="gpu_builtin">Driverne innebygd i DroidDeck</string> + <string name="gpu_matched_pair">Kjøretids- og skjermdrivere angitt sammen som et matchet par.</string> + <string name="gpu_recommended_for">Anbefalt for denne GPU-en: %1$s.</string> + <string name="gpu_refresh_hint">Oppdater for å se etter drivere for denne GPU-en.</string> + <string name="gpu_recommended_caps">ANBEFALT</string> + <string name="gpu_unavailable">Ikke tilgjengelig</string> + <string name="load_runtime">Linux-kjøretid</string> + <string name="load_runtime_detail">Gjør Linux-kjøretiden klar</string> + <string name="load_session">Sesjon</string> + <string name="load_session_detail">Starter skjerm og lyd</string> + <string name="load_steam">Steam-klient</string> + <string name="load_steam_detail">Kontrollerer Steam og kompatibilitetsverktøyene</string> + <string name="load_opening_steam">Åpner Steam</string> + <string name="load_opening_steam_detail">Venter på Steams første bilde</string> + <string name="load_desktop">Skrivebord</string> + <string name="load_desktop_detail">Gjør skrivebordet klart</string> + <string name="load_opening">Åpner</string> + <string name="load_opening_detail">Venter på det første bildet</string> + <string name="load_starting_steam">Starter Steam</string> + <string name="load_ended">Sesjonen ble avsluttet</string> + <string name="load_step">Steg %1$d av %2$d</string> + <string name="load_steam_paused">Steam er pauset</string> + <string name="deck_tab_grips">Grep</string> + <string name="deck_tab_left">Venstre plate</string> + <string name="deck_tab_right">Høyre plate</string> + <string name="deck_tab_both">Begge</string> + <string name="deck_tab_gamepad">Kontroller</string> + <string name="deck_surface_stick">Stikke</string> + <string name="deck_surface_trackpad">Styreflate</string> + <string name="deck_surface_switch">Bytt mellom stikke og styreflate</string> + <string name="deck_more_controls">Flere Deck-kontroller</string> + <string name="deck_panels">PANELER</string> + <string name="deck_close">Lukk kontroller for andre skjerm</string> + <string name="deck_open_steam_menu">Åpne Steam-menyen</string> + <string name="deck_left_upper">Venstre · øvre</string> + <string name="deck_left_lower">Venstre · nedre</string> + <string name="deck_right_upper">Høyre · øvre</string> + <string name="deck_right_lower">Høyre · nedre</string> + <string name="deck_click">Klikk</string> + <string name="deck_right_trackpad">Høyre styreflate</string> + <string name="deck_left_trackpad">Venstre styreflate</string> + <string name="second_trackpad">Styreflate</string> + <string name="second_show_keyboard">Vis tastatur</string> + <string name="second_hide_keyboard">Skjul tastatur</string> + <string name="second_terminal">Linux-terminal</string> + <string name="second_terminal_not_ready">Linux-sesjonen er ikke klar for en terminal ennå.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Spill</string> + <string name="rail_desktop">Skrivebord</string> + <string name="rail_store">Butikk</string> + <string name="rail_components">Komponenter</string> + <string name="rail_apps">Apper</string> + <string name="rail_setup">Oppsett</string> + <string name="rail_updates">Oppdateringer</string> + <string name="rail_percent">%1$d%%</string> + <string name="rail_resume_named">Gjenoppta %1$s</string> + <string name="roms_text">Vises som /root/ROMs i hver sesjon. Intern lagring er på /root/Storage.</string> + <string name="roms_none">Ingen mappe valgt</string> + <string name="roms_applies">Gjelder fra neste sesjon. SD-kort fungerer.</string> + <string name="roms_choose">Velg mappe</string> + <string name="roms_change">Endre mappe</string> + <string name="proton_lede">Installer en ARM64 Proton-build, og velg den så per spill i Steam → Egenskaper → Kompatibilitet.</string> + <string name="proton_stop_first">Avslutt den kjørende sesjonen for å installere eller fjerne et kompatibilitetsverktøy.</string> + <string name="proton_available">Tilgjengelige builder</string> + <string name="proton_installed">Installert %1$s</string> + <string name="proton_queued">Venter fra en tidligere forespørsel</string> + <string name="proton_not_installed">Ikke installert</string> + <string name="proton_update_available">Oppdatering tilgjengelig → %1$s</string> + <string name="proton_remove_older">Fjern gamle %1$s-versjoner</string> + <string name="proton_remove_older_body">Fjerner %1$s. Spill satt til disse versjonene må byttes til en annen Proton.</string> + <string name="proton_remove_body">Spill satt til denne versjonen må byttes til en annen Proton.</string> + <string name="proton_install_now">Installer nå</string> + <string name="proton_cancel_queued">Avbryt i kø</string> + <string name="pckb_android">Android-tastatur</string> + <string name="pckb_hide">Skjul ✕</string> + <string name="main_launch_named">Start %1$s</string> + <string name="main_choose_screen">Velg en skjerm. Sekundær: %1$s.</string> + <string name="main_secondary_gone">Den sekundære skjermen er ikke tilgjengelig lenger.</string> + <string name="main_primary">Hovedskjerm</string> + <string name="main_secondary">Sekundær skjerm</string> + <string name="widgets_prev_tab">Forrige fane</string> + <string name="widgets_next_tab">Neste fane</string> + <string name="widgets_on">På</string> + <string name="widgets_all_n">Alle %1$d</string> + <string name="widgets_none">Ingen</string> + <string name="widgets_n_of_m">%1$d av %2$d</string> + <string name="widgets_off">Av</string> + <string name="screen_bottom">Nedre skjerm</string> + <string name="game_env_applies">Gjelder fra neste spilløkt. En spillprofil overstyrer den delte.</string> + <string name="game_env_other">Et annet spill etter app-ID…</string> + <string name="game_env_done">Ferdig</string> + <string name="game_env_default">Standard</string> + <string name="game_env_off">Av</string> + <string name="game_env_change">Endre…</string> + <string name="game_env_change_title">Endre variabel</string> + <string name="game_env_restore_default">Tilbake til standard</string> + <string name="game_env_eyebrow">Proton-spill</string> + <string name="pip_title">Bilde-i-bilde</string> + <string name="pip_auto">Gå automatisk inn i PiP</string> + <string name="unfolded_controls">Kontroller på den utbrettede skjermen</string> + <string name="unfolded_controls_hint">Utbrettet sitter spillet over Steam Deck-styreflatene og bakgrepene. Sammenslått er spillet i fullskjerm.</string> + <string name="pip_suspended">Satt på pause</string> + <string name="pip_not_ready">Bilde-i-bilde er tilgjengelig når sesjonen er klar.</string> + <string name="pip_permission">Tillat bilde-i-bilde for DroidDeck i Android-innstillingene.</string> + <string name="pip_unavailable">Android kunne ikke åpne bilde-i-bilde.</string> + <string name="game_shortcut">Snarvei</string> + <string name="game_shortcut_add">Legg til på hjem-skjermen</string> + <string name="game_shortcut_choose">Velg spill</string> + <string name="game_shortcut_scanning">Skanner installerte spill…</string> + <string name="game_link_copy">Kopier oppstartslenke</string> + <string name="game_link_copied">Oppstartslenke kopiert</string> + <string name="game_shortcut_unsupported">Denne launcheren kan ikke legge til snarveier. Bruk Kopier oppstartslenke i stedet.</string> + <string name="game_link_invalid">Ugyldig oppstartslenke for spill.</string> + <string name="game_link_missing">Dette spillet er ikke lenger i DroidDeck-biblioteket ditt.</string> + <string name="game_link_session_busy">Avslutt den nåværende sesjonen før du starter et Steam-spill.</string> + <string name="game_link_failed">Kunne ikke sende spillet til Steam. Prøv å starte det igjen.</string> + <string name="setup_retry_removal">Prøv fjerning igjen</string> + <string name="runtime_removing">Fjerner</string> + <string name="runtime_removal_incomplete">Fjerning ikke fullført</string> + <string name="game_file_export">Eksporter spillfil</string> + <string name="game_file_sync">Synkroniser alle spillfiler til mappe</string> + <string name="game_file_stop_sync">Stopp synkronisering av spillfiler</string> + <string name="game_file_folder">Velg en mappe for frontend-spillfiler</string> + <string name="game_file_exported">Spillfiler lagret i %1$s</string> + <string name="game_frontend_files">Frontend-filer</string> + + <string name="main_wait_runtime">Vent til kjøretidsoperasjonen er ferdig</string> + <string name="main_wait_proton_install">Vent til installasjonen av kompatibilitetsverktøyet er ferdig</string> + <string name="main_wait_desktop_install">Vent til installasjonen av skrivebordsappen er ferdig</string> + <string name="main_wait_install_game">Vent til installasjonen er ferdig før du starter et spill</string> + <string name="main_saves_close_session">Lukk Steam-sesjonen først, så spillet ikke kan lagre over importen</string> + <string name="main_saves_importing">Importerer til %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="one">Importerte %1$d fil fra %2$s til %3$s</item> + <item quantity="other">Importerte %1$d filer fra %2$s til %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">Importerte %1$d fil fra %2$s til %3$s. Gamle lagringer sikkerhetskopiert til Download/DroidDeck/Saves/backups</item> + <item quantity="other">Importerte %1$d filer fra %2$s til %3$s. Gamle lagringer sikkerhetskopiert til Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Velg en lagrings-zip for %1$s</string> + <string name="main_saves_exporting">Eksporterer %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="one">Eksporterte %1$d fil som en %2$s: %3$s</item> + <item quantity="other">Eksporterte %1$d filer som en %2$s: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Velg hvor du vil lagre %1$s (%2$s)</string> + <string name="main_action_failed">%1$s mislyktes: %2$s</string> + <string name="main_no_logs_yet">Ingen sesjonslogger ennå: kjør en sesjon først.</string> + <string name="main_no_logs_to_clear">Ingen sesjonslogger å tømme.</string> + <plurals name="main_logs_cleared"> + <item quantity="one">Tømte %1$d sesjonsloggmappe.</item> + <item quantity="other">Tømte %1$d sesjonsloggmapper.</item> + </plurals> + <string name="main_wadb_pair_failed">Kobling for trådløs feilsøking mislyktes</string> + <string name="main_wadb_command_failed">Kommando for trådløs feilsøking mislyktes</string> + <string name="main_wadb_unavailable">Trådløs feilsøking er ikke tilgjengelig</string> + <string name="main_adb_copied">ADB-kommando kopiert</string> + <string name="main_pick_roms">Velg ROM-mappen</string> + <string name="main_non_adreno_title">Ikke en Adreno-GPU</string> + <string name="main_non_adreno_text">Turnip støtter Adreno-GPU-er. På %1$s kan Steam vise en svart skjerm. Nedlasting: %2$.0f MB.</string> + <string name="main_install_anyway">Installer likevel</string> + <string name="main_remove_runtime_title">Fjern Linux-kjøretid</string> + <string name="main_remove_runtime_text">Dette sletter kjøretiden, Steam-klienten inni den, og hvert spill installert der.</string> + <string name="main_applied">Brukt: %1$s</string> + <string name="android_app_open_failed_primary">Kunne ikke åpne %1$s på hovedskjermen</string> + <string name="android_app_open_failed_on">Kunne ikke åpne %1$s på %2$s</string> + <string name="android_app_open_failed_display">Kunne ikke åpne %1$s på skjerm %2$d</string> + <string name="comp_busy_importing">Importerer</string> + <string name="comp_busy_swapping">Bytter ut</string> + <string name="comp_busy_restoring">Gjenoppretter</string> + <string name="comp_busy_cancelling">Avbryter</string> + <string name="comp_busy_deleting">Sletter</string> + <string name="comp_cannot_read">kan ikke lese filen</string> + <string name="comp_imported">Importerte %1$s</string> + <string name="comp_swap_cancelled">Den ventende byttingen ble avbrutt.</string> + <string name="comp_pick_wcp">Velg en komponentpakke (-linux .wcp)</string> + <string name="comp_nightlies_unreachable">Kunne ikke nå Nightlies</string> + <string name="comp_stored">Lagret %1$s</string> + <string name="comp_download_failed">Nedlasting mislyktes: %1$s</string> + <string name="drivers_pick_linux">Velg en Linux-kjøretidsdriver (-Linux-zip eller Android + Linux-pakke)</string> + <string name="drivers_pick_android">Velg en skjermdriver (AdrenoTools-zip eller Android + Linux-pakke)</string> + <string name="drivers_pick_any">Velg en driver-zip eller en Android + Linux-pakke</string> + <string name="main_pkg_problem">%1$s: %2$s</string> + <string name="main_removing_named">Fjerner %1$s…</string> + <string name="main_forgetting_named">Glemmer %1$s…</string> + <string name="main_pick_game_storage">Velg spillagringsmappen</string> + <string name="main_pick_decky_zip">Velg en Decky-programtillegg-ZIP</string> + <string name="main_storage_not_usable">Ikke brukbar: %1$s</string> + <string name="main_running_program">Program</string> + <string name="main_removing_runtime">Fjerner Linux-kjøretid</string> + <string name="main_settings_main_screen">Kunne ikke åpne Innstillinger på den nedre skjermen; åpnet den på hovedskjermen.</string> + <string name="main_open_dev_options">Åpne Utvikleralternativer i Innstillinger</string> + + <string name="upd_unknown_apps_failed">Kunne ikke åpne Androids innstilling «Installer ukjente apper»</string> + <string name="upd_couldnt_check">Kunne ikke sjekke: %1$s</string> + <string name="upd_no_download">Denne builden har ingen nedlasting for denne kopien av DroidDeck</string> + <string name="upd_stop_first">Avslutt den kjørende sesjonen først, og oppdater så</string> + <string name="upd_stage_downloading">Laster ned</string> + <string name="upd_stage_installing">Installerer</string> + <string name="upd_stage_confirm">Venter på at du bekrefter</string> + <string name="upd_failed">Oppdateringen mislyktes</string> + <string name="upd_not_offered">Android tilbød ikke å installere oppdateringen</string> + + <string name="decky_install_failed">Decky-installasjon mislyktes: %1$s</string> + <string name="decky_plugin_starting">Starter import av programtillegg…</string> + <string name="decky_plugin_failed">Import av programtillegg mislyktes: %1$s</string> + <string name="decky_plugin_installed">Programtillegg installert. Start Steam på nytt for å laste det.</string> + + <string name="drivers_not_checked">Ikke sjekket ennå – trykk oppdater for å se etter nye drivere</string> + <string name="drivers_bundle_sets_display">Android + Linux: setter skjermdriveren med den</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: setter kjøretidsdriveren med den</string> + <string name="drivers_runtime_builtin">Turnipen innebygd i kjøretiden</string> + <string name="drivers_system_vulkan">system-Vulkan: ingen bunnet build for denne GPU-en</string> + <string name="drivers_bundled">%1$s (medfølgende)</string> + <string name="drivers_manual">Manuell</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">Bare den ene halvdelen er publisert akkurat nå</string> + <string name="drivers_pair_size">%1$.0f MB for begge</string> + <string name="drivers_none_to_set">Ingen drivere å angi: %1$s</string> + <string name="drivers_checking_latest">Ser etter de nyeste driverne…</string> + <string name="drivers_up_to_date_check_failed">Oppdatert (kunne ikke sjekke: %1$s)</string> + <string name="drivers_recommended_unpublished">De anbefalte driverne er ikke publisert akkurat nå</string> + <string name="drivers_downloading_pair">Laster ned %1$s %2$s…</string> + <string name="drivers_updated_to">Oppdatert til %1$s %2$s.</string> + <string name="drivers_updated_to_restart">Oppdatert til %1$s %2$s. Skjermdriveren gjelder etter at DroidDeck starter på nytt.</string> + <string name="drivers_switched_to">Byttet til %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Byttet til %1$s %2$s. Skjermdriveren gjelder etter at DroidDeck starter på nytt.</string> + <string name="drivers_using">Bruker %1$s %2$s.</string> + <string name="drivers_using_restart">Bruker %1$s %2$s. Skjermdriveren gjelder etter at DroidDeck starter på nytt.</string> + <string name="drivers_not_a_driver">Ikke en driver-zip: verken en AdrenoTools-driver, en -Linux Turnip eller en Android + Linux-pakke</string> + <string name="drivers_import_failed">Import mislyktes: %1$s</string> + <string name="drivers_imported_unnamed">Importert driver</string> + <string name="drivers_using_bundle">Bruker %1$s som både kjøretids- og skjermdriver.</string> + <string name="drivers_using_bundle_restart">Bruker %1$s som både kjøretids- og skjermdriver. Skjermdriveren gjelder etter at DroidDeck starter på nytt.</string> + <string name="drivers_deleted_bundle">Slettet %1$s (begge driverne)</string> + <string name="drivers_deleted">Slettet %1$s</string> + <string name="drivers_release_status">Nyeste: %1$s · sjekket %2$s</string> + <string name="drivers_release_status_failed">Nyeste: %1$s · %2$s utilgjengelig · sjekket %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">%1$d min siden</item> + <item quantity="other">%1$d min siden</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">%1$d t siden</item> + <item quantity="other">%1$d t siden</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">%1$d dag siden</item> + <item quantity="other">%1$d dager siden</item> + </plurals> + <string name="drivers_checking_sources">Sjekker Banners-Turnip, WinNative og DroidDeck…</string> + <string name="drivers_check_failed">sjekk mislyktes</string> + <string name="drivers_installed_pick_either">Installert %1$s – velg den i begge lister for å angi begge driverne</string> + <string name="drivers_installed_pick">Installert %1$s – velg den i menyen</string> + + <string name="session_wait_removal">Vent til fjerning av kjøretiden er ferdig</string> + <string name="session_title_game">Spill</string> + <string name="session_touch_auto_touchpad">styreflate</string> + <string name="session_touch_auto_direct">direkte</string> + <string name="session_swap_failed">Bytting mislyktes: %1$s</string> + <string name="session_profile_save_failed">Lagring av profil mislyktes: %1$s</string> + <string name="session_desktop_paused">Skrivebordet er pauset</string> + <string name="session_named_paused">%1$s er pauset</string> + <string name="session_program_paused">Programmet er pauset</string> + <string name="session_starting_desktop">Starter skrivebordet</string> + <string name="session_starting_named">Starter %1$s</string> + <string name="session_starting_program">Starter programmet</string> + <string name="session_no_logs">Ingen logger for denne sesjonen.</string> + <string name="session_logs_archive_failed">Kunne ikke opprette sesjonsloggarkivet.</string> + <string name="session_logs_share_failed">Kunne ikke dele sesjonsloggene.</string> + <string name="session_starting_session">Starter sesjonen…</string> + <string name="session_runtime_downloading">laster ned Linux-kjøretiden</string> + <string name="session_runtime_downloading_mb">laster ned Linux-kjøretiden · %1$d av %2$d MB</string> + <string name="session_runtime_checking">kontrollerer Linux-kjøretiden</string> + <string name="session_runtime_unpacking">pakker ut Linux-kjøretiden</string> + <string name="session_desktop_downloading">laster ned skrivebordet</string> + <string name="session_desktop_downloading_mb">laster ned skrivebordet · %1$d av %2$d MB</string> + <string name="session_desktop_checking">kontrollerer skrivebordet</string> + <string name="session_desktop_unpacking">pakker ut skrivebordet</string> + <string name="session_proton_downloading">laster ned Proton Experimental (ARM64)</string> + <string name="session_proton_downloading_mb">laster ned Proton Experimental (ARM64) · %1$d av %2$d MB</string> + <string name="session_proton_checking">kontrollerer Proton Experimental (ARM64)</string> + <string name="session_proton_unpacking">pakker ut Proton Experimental (ARM64)</string> + <string name="session_runtime_catalog_unreachable">Kunne ikke nå kjøretidskatalogen. Kontroller tilkoblingen og trykk Spill igjen.</string> + <string name="session_runtime_install_failed">Linux-kjøretiden ble ikke installert. Kontroller kjøretiden i Oppsett og trykk Spill igjen.</string> + <string name="session_desktop_install_failed">Skrivebordet ble ikke installert (%1$s). Kontroller tilkoblingen og trykk Skrivebord igjen.</string> + <string name="session_ended_named">%1$s stoppet uventet. Del loggene med en feilrapport, eller prøv igjen.</string> + <string name="session_ended_program">Programmet stoppet uventet. Del loggene med en feilrapport, eller prøv igjen.</string> + <string name="session_ended_desktop">Skrivebordet stoppet uventet. Del loggene med en feilrapport, eller prøv igjen.</string> + <string name="session_ended_detail">Avslutningsstatus %1$d · %2$s</string> + <string name="session_ended_no_log">ingen logg</string> + <plurals name="session_hint_enosys"> + <item quantity="one">Loggen viser %1$d «Function not implemented»-feil: prootens seccomp-akselerasjon feiler en hjelper på denne enheten. Prøv %2$s → «%3$s» (eller «%4$s») og start på nytt.</item> + <item quantity="other">Loggen viser %1$d «Function not implemented»-feil: prootens seccomp-akselerasjon feiler en hjelper på denne enheten. Prøv %2$s → «%3$s» (eller «%4$s») og start på nytt.</item> + </plurals> + + <string name="mode_decky_plugins">Programtillegg</string> + <string name="mode_decky_plugins_hint">Installer en programtillegg-ZIP for neste Steam-sesjon.</string> + <string name="mode_decky_plugin_downloading">Laster ned…</string> + <string name="mode_decky_plugin_install_zip">Installer fra ZIP</string> + <string name="store_cat_games">Spill</string> + <string name="store_cat_audio_video">Lyd og video</string> + <string name="store_cat_graphics">Grafikk</string> + <string name="store_cat_internet">Internett</string> + <string name="store_cat_office">Kontor</string> + <string name="store_cat_developer">Utvikler</string> + <string name="store_cat_education">Utdanning</string> + <string name="store_cat_science">Vitenskap</string> + <string name="store_cat_system">System</string> + <string name="store_cat_utilities">Verktøy</string> + <string name="store_section_popular">Populært</string> + <string name="store_section_trending">Trender</string> + <string name="store_section_new">Nytt</string> + <string name="store_section_updated">Oppdatert</string> + <string name="store_section_loading">laster…</string> + <string name="store_flatpak_setup">Flatpak-oppsett</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Kunne ikke sjekke Flathub for oppdateringer</string> + <string name="content_rom_installed">installert</string> + <string name="content_rom_folder">mappe</string> + <string name="theme_graphite">Grafitt</string> + <string name="theme_graphite_detail">Grafittflater, med logoens egen kuleblå farge for alt som er valgt, fokusert eller trykket.</string> + <string name="theme_paper">Papir på svart</string> + <string name="theme_paper_detail">Hvitt er primærfargen, ikonets blå er signalet for linjer, ringer og prikker.</string> + <string name="theme_icon_blue">Ikonblå</string> + <string name="theme_icon_blue_detail">Svart bakgrunn, hvit skrift, det blå for alt som er valgt, fokusert eller trykket.</string> + <string name="theme_electric">Elektrisk marineblå</string> + <string name="theme_electric_detail">Det blå lysere, på flater som lener mer mot marineblått enn grått.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Space</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <string name="game_sync_err_runtime">Installer kjøretiden først</string> + <string name="game_sync_err_steam_library">Steam-biblioteket er ikke tilgjengelig</string> + <string name="game_sync_err_previous_sd">Det forrige SD-biblioteket er ikke tilgjengelig</string> + <string name="game_sync_err_game_storage">Spillagringen er ikke tilgjengelig</string> + <string name="game_sync_err_added_games">Mappen for tillagte spill er ikke tilgjengelig</string> + <string name="game_sync_err_manifests">Steam-manifestene er ikke tilgjengelige</string> + <string name="content_system_many">Mange systemer</string> + <string name="fm_drive_internal">Intern</string> + <string name="fm_drive_linux">Linux-system</string> + <string name="fm_drive_sd">SD-kort</string> + <string name="fm_drive_storage">Lagring</string> + <string name="fm_select">Velg</string> + <string name="fm_properties">Egenskaper</string> + <string name="fm_favorite_add">Legg til i favoritter</string> + <string name="fm_favorite_remove">Fjern fra favoritter</string> + <string name="fm_rename">Gi nytt navn</string> + <string name="fm_copy">Kopier</string> + <string name="fm_cut">Klipp ut</string> + <string name="fm_actions">Handlinger</string> + <string name="fm_favorites">Favoritter</string> + <string name="fm_pin_current">Fest denne mappen</string> + <string name="fm_no_favorites">Ingen favoritter ennå – fest en mappe med ⋮-menyen for å komme raskt tilbake hit.</string> + <string name="fm_unpin_favorite">Fjern fra favoritter</string> + <string name="fm_size_zero">0 byte</string> + <string name="fm_unit_bytes">byte</string> + <string name="fm_prop_location">Plassering</string> + <string name="fm_prop_type">Type</string> + <string name="fm_prop_size">Størrelse</string> + <string name="fm_prop_modified">Endret</string> + <string name="fm_type_folder">Mappe</string> + <string name="fm_type_file">Fil</string> + <string name="fm_type_ext_file">%1$s-fil</string> + <string name="fm_read_only">Skrivebeskyttet</string> + <string name="fm_read_only_hint">Stopper spill (og Wine) fra å overskrive eller slette denne filen.</string> + <string name="fm_read_only_failed">Kunne ikke endre Skrivebeskyttet</string> + <string name="fm_hidden">Skjult</string> + <string name="fm_hidden_unsupported">Denne lagringen kan ikke lagre den skjulte-flagget.</string> + <string name="fm_hidden_hint">Markerer filen som skjult i Windows (Wines skjult-attributt).</string> + <string name="fm_hidden_failed">Kunne ikke endre Skjult på denne lagringen</string> + <string name="fm_done">Ferdig</string> + <string name="fm_op_deleting">Sletter…</string> + <string name="fm_delete_failed">Sletting mislyktes</string> + <string name="fm_op_moving">Flytter…</string> + <string name="fm_op_copying">Kopierer…</string> + <string name="fm_op_moving_item">Flytter – %1$s</string> + <string name="fm_op_copying_item">Kopierer – %1$s</string> + <string name="fm_op_moving_batch">Flytter %1$d/%2$d – %3$s</string> + <string name="fm_op_copying_batch">Kopierer %1$d/%2$d – %3$s</string> + <string name="fm_paste_failed">%1$d ferdig, %2$d mislyktes</string> + <string name="fm_paste_skipped">%1$d ferdig, %2$d hoppet over</string> + <plurals name="fm_paste_moved"> + <item quantity="one">%1$d element flyttet</item> + <item quantity="other">%1$d elementer flyttet</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">%1$d element kopiert</item> + <item quantity="other">%1$d elementer kopiert</item> + </plurals> + <string name="fm_already_exists">«%1$s» finnes allerede</string> + <string name="fm_op_renaming">Gir nytt navn…</string> + <string name="fm_rename_failed">Kunne ikke gi nytt navn</string> + <string name="fm_op_creating_folder">Oppretter mappe…</string> + <string name="fm_create_folder_failed">Kunne ikke opprette mappe</string> + <string name="fm_new_folder">Ny mappe</string> + <string name="fm_folder_name">Mappenavn</string> + <string name="fm_create">Opprett</string> + <string name="fm_new_name">Nytt navn</string> + <string name="fm_delete_title">Slette?</string> + <string name="fm_delete_text">Slette «%1$s» permanent?</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">Slette %1$d element?</item> + <item quantity="other">Slette %1$d elementer?</item> + </plurals> + <string name="fm_cannot_undo">Dette kan ikke angres.</string> + <plurals name="fm_and_more"> + <item quantity="one">…og %1$d til</item> + <item quantity="other">…og %1$d til</item> + </plurals> + <string name="fm_op_deleting_batch">Sletter %1$d/%2$d – %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">%1$d kunne ikke slettes</item> + <item quantity="other">%1$d kunne ikke slettes</item> + </plurals> + <string name="fm_conflict_merge_hint">Slå sammen legger til og erstatter filer inne i den eksisterende mappen.</string> + <string name="fm_conflict_overwrite_hint">Overskriv erstatter den eksisterende filen.</string> + <string name="fm_conflict_apply_all">Bruk på alle konflikter</string> + <string name="fm_conflict_merge">Slå sammen</string> + <string name="fm_conflict_overwrite">Overskriv</string> + <string name="fm_conflict_keep_both">Behold begge</string> + <string name="fm_conflict_skip">Hopp over</string> + <string name="fm_select_this_folder">Velg denne mappen</string> + <string name="fm_back">Tilbake</string> + <string name="fm_drive_not_readable">%1$s er montert, men ikke lesbar akkurat nå</string> + <string name="fm_list_view">Listevisning</string> + <string name="fm_grid_view">Rutenettvisning</string> + <string name="fm_search">Søk</string> + <string name="fm_sort">Sorter</string> + <string name="fm_sort_name">Navn</string> + <string name="fm_sort_date">Dato endret</string> + <string name="fm_rows_comfortable">Luftige rader</string> + <string name="fm_rows_compact">Kompakte rader</string> + <string name="fm_hide_hidden">Skjul skjulte filer</string> + <string name="fm_show_hidden">Vis skjulte filer</string> + <string name="fm_hide_favorites">Skjul favoritter</string> + <string name="fm_show_favorites">Vis favoritter</string> + <string name="fm_filter_folder">Filtrer denne mappen</string> + <string name="fm_free_space">%1$s ledig</string> + <string name="fm_selected_count">%1$d valgt</string> + <string name="fm_select_none">Ingen</string> + <string name="fm_select_all">Alle</string> + <string name="fm_paste">Lim inn</string> + <plurals name="fm_items"> + <item quantity="one">%1$d element</item> + <item quantity="other">%1$d elementer</item> + </plurals> + <string name="fm_paste_here">Lim inn %1$s her</string> + <string name="fm_paste_move_here">Lim inn %1$s (flytt) her</string> + <string name="fm_rail_title">Filer</string> + <string name="fm_rail_storage">LAGRING</string> + <string name="fm_rail_quick">RASK</string> + <string name="fm_rail_favorites">FAVORITTER</string> + <string name="fm_rail_downloads">Nedlastinger</string> + <string name="fm_rail_downloads_short">Nedlast</string> + <string name="fm_rail_roms">ROM-er</string> + <string name="fm_rail_session_logs">Sesjonslogger</string> + <string name="fm_rail_pictures">Bilder</string> + <string name="fm_favorite_added">La til «%1$s» i favoritter</string> + <string name="fm_favorite_removed">Fjernet «%1$s» fra favoritter</string> + <string name="fm_empty_directory">Tom mappe</string> + <string name="fm_up_one_level">Opp ett nivå</string> + <string name="fm_choose_folder">Velg en mappe</string> + <string name="fm_choose_file">Velg en fil</string> + <string name="ctrl_tint_steam_blue">Steam-blå</string> + <string name="ctrl_tint_sky">Himmel</string> + <string name="ctrl_tint_white">Hvit</string> + <string name="ctrl_tint_violet">Fiolett</string> + <string name="ctrl_tint_green">Grønn</string> + <string name="ctrl_tint_amber">Amber</string> + <string name="ctrl_tint_red">Rød</string> + <string name="ctrl_tint_pink">Rosa</string> + <string name="ctrl_button_a">A-knapp</string> + <string name="ctrl_button_b">B-knapp</string> + <string name="ctrl_button_x">X-knapp</string> + <string name="ctrl_button_y">Y-knapp</string> + <string name="ctrl_button_lb">Venstre skulderknapp</string> + <string name="ctrl_button_rb">Høyre skulderknapp</string> + <string name="ctrl_button_lt">Venstre trigger</string> + <string name="ctrl_button_rt">Høyre trigger</string> + <string name="ctrl_button_view">View-knapp</string> + <string name="ctrl_button_menu">Menu-knapp</string> + <string name="ctrl_target_view">View</string> + <string name="ctrl_target_menu">Menu</string> + <string name="ctrl_target_up">D-pad opp</string> + <string name="ctrl_target_down">D-pad ned</string> + <string name="ctrl_target_left">D-pad venstre</string> + <string name="ctrl_target_right">D-pad høyre</string> + <string name="ctrl_target_hidden">Skjult</string> + <string name="ctrl_editor_drag">Dra for å flytte</string> + <string name="ctrl_editor_snap">Fest</string> + <string name="ctrl_editor_save">Lagre</string> + <string name="second_mode_none">Ingen</string> + <string name="second_mode_keyboard_trackpad">Tastatur + styreflate</string> + <string name="second_mode_terminal">Terminal</string> + <string name="second_mode_deck_controls">Deck-grep + styreflater</string> + + <string name="rtinst_removing">Fjerner Linux-kjøretid</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">Fjerner Linux-kjøretid · %1$d oppføring</item> + <item quantity="other">Fjerner Linux-kjøretid · %1$d oppføringer</item> + </plurals> + <string name="rtinst_removed">Linux-kjøretiden fjernet</string> + <string name="rtinst_removal_failed">Kunne ikke fullføre fjerningen av Linux-kjøretiden. Prøv fjerning igjen i Oppsett.</string> + <string name="rtinst_removing_leftovers">Fjerner kjøretidsrester</string> + <string name="rtinst_downloading">Laster ned</string> + <string name="rtinst_verifying">Verifiserer</string> + <string name="rtinst_extracting">Pakker ut</string> + <string name="rtinst_channel_description">Vises mens Linux-kjøretiden installeres eller fjernes</string> + <string name="rtinst_title_installing">Installerer Linux-kjøretiden</string> + <string name="rtinst_title_removing">Fjerner Linux-kjøretiden</string> + + <string name="deskpkg_runtime_missing">Linux-kjøretiden er ikke installert</string> + <string name="deskpkg_no_checksum">Katalogen har ingen sjekksum for %1$s</string> + <string name="deskpkg_checksum_mismatch">Sjekksum stemmer ikke – ingenting ble endret</string> + <string name="deskpkg_installing">Installerer %1$s</string> + <string name="deskpkg_place_failed">Kunne ikke plassere AppImage-filen</string> + <string name="deskpkg_extract_failed">Utpakking mislyktes</string> + <string name="deskpkg_install_failed">Installasjon mislyktes</string> + + <string name="flatpakmgr_setup_failed">Flatpak-oppsett mislyktes (avslutningskode %1$d)</string> + <string name="flatpakmgr_setup_unfinished">Flatpak-oppsettet ble ikke fullført</string> + <string name="flatpakmgr_busy">En annen butikkoppgave kjører</string> + <string name="flatpakmgr_not_ready">Sett opp Flatpak først</string> + <string name="flatpakmgr_starting">Starter</string> + <string name="flatpakmgr_exit_status">Flatpak avsluttet med status %1$d</string> + <string name="flatpakmgr_installing">Installerer %1$s</string> + <string name="flatpakmgr_updating">Oppdaterer %1$s</string> + <string name="flatpakmgr_removing">Fjerner %1$s</string> + <string name="flatpakmgr_installing_step">Installerer %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_updating_step">Oppdaterer %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_removing_step">Fjerner %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_ref_translations">oversettelser</string> + <string name="flatpakmgr_ref_graphics_drivers">grafikkdrivere</string> + <string name="flatpakmgr_ref_media_codecs">mediekodeker</string> + <string name="flatpakmgr_ref_runtime">%1$s-kjøretid</string> + <string name="flatpakmgr_ref_runtime_branch">%1$s-kjøretid %2$s</string> + + <string name="appimg_too_small">Filen er for liten til å være en AppImage</string> + <string name="appimg_unreadable">Filen kan ikke leses: %1$s</string> + <string name="appimg_not_linux">Dette er ikke en AppImage (det er ikke et Linux-program)</string> + <string name="appimg_wrong_arch">Denne AppImage-filen er bygget for verken ARM64 (aarch64) eller x86-PC-er</string> + <string name="appimg_not_appimage">Dette er et Linux-program, men ikke en AppImage</string> + <string name="appimg_wrong_type">Bare type 2-AppImages kan importeres (denne er type %1$d)</string> + <string name="appimg_no_space_import">Ikke nok ledig plass: import krever omtrent %1$s</string> + <string name="appimg_no_space_update">Ikke nok ledig plass: oppdatering krever omtrent %1$s</string> + <string name="appimg_import_failed">Import mislyktes</string> + <string name="appimg_gone">Den AppImage-filen finnes ikke lenger</string> + <string name="appimg_update_failed">Oppdatering mislyktes</string> + <string name="appimg_extracting">Pakker ut %1$s</string> + <string name="appimg_extract_failed">AppImage-filen kunne ikke pakkes ut</string> + <string name="appimg_extract_failed_detail">AppImage-filen kunne ikke pakkes ut: %1$s</string> + + <string name="deckymgr_stop_session_loader">Avslutt den aktive sesjonen før du installerer Decky Loader</string> + <string name="deckymgr_download_failed">Nedlasting av Decky Loader mislyktes</string> + <string name="deckymgr_size_mismatch">Decky Loader-størrelsen stemte ikke med utgivelsens metadata</string> + <string name="deckymgr_checksum_download_failed">Kunne ikke laste ned utgivelsens sjekksum</string> + <string name="deckymgr_checksum_missing">Utgivelsens sjekksum mangler</string> + <string name="deckymgr_checksum_invalid">Utgivelsens sjekksum er ugyldig</string> + <string name="deckymgr_checksum_mismatch">Sjekksummen til Decky Loader stemmer ikke</string> + <string name="deckymgr_not_elf">Utgivelsesfilen er ikke en kompatibel 64-bit Linux-kjørbar fil</string> + <string name="deckymgr_session_started_loader">En sesjon startet under nedlastingen; avslutt den før du installerer Decky Loader</string> + <string name="deckymgr_loader_not_executable">Kunne ikke merke PluginLoader som kjørbar</string> + <string name="deckymgr_cef_failed">Kunne ikke slå på Steams CEF-fjernsøking for feilsøking</string> + <string name="deckymgr_stage_failed">Kunne ikke klargjøre PluginLoader</string> + <string name="deckymgr_loader_preserve_failed">Kunne ikke bevare den eksisterende PluginLoader</string> + <string name="deckymgr_loader_activate_failed">Kunne ikke aktivere den nye PluginLoader</string> + <string name="deckymgr_stop_session_plugin">Avslutt den aktive sesjonen før du installerer et programtillegg</string> + <string name="deckymgr_loader_required">Installer Decky Loader før du importerer et programtillegg</string> + <string name="deckymgr_zip_unreadable">Kunne ikke lese den valgte ZIP-filen</string> + <string name="deckymgr_plugins_dir_failed">Kunne ikke opprette mappen for Decky-programtillegg</string> + <string name="deckymgr_extracting_zip">Pakker ut programtillegg-ZIP</string> + <string name="deckymgr_zip_one_folder">Programtillegg-ZIP-en må inneholde én mappe på toppnivå</string> + <string name="deckymgr_zip_missing_files">Programtillegg-ZIP-en mangler plugin.json, package.json eller dist/index.js</string> + <string name="deckymgr_plugin_name_missing">Programtilleggets navn mangler</string> + <string name="deckymgr_session_started_plugin">En sesjon startet under installasjonen; avslutt den før du installerer et programtillegg</string> + <string name="deckymgr_plugin_installed">Programtillegg installert · start Steam på nytt for å laste det</string> + <string name="deckymgr_plugin_install_failed">Installasjon av programtillegg-ZIP mislyktes</string> + <string name="deckymgr_zip_too_many">Programtillegg-ZIP-en inneholder for mange oppføringer</string> + <string name="deckymgr_zip_invalid_path">Programtillegg-ZIP-en inneholder en ugyldig sti</string> + <string name="deckymgr_zip_duplicate_paths">Programtillegg-ZIP-en inneholder like stier</string> + <string name="deckymgr_zip_unsafe_path">Programtillegg-ZIP-en inneholder en utrygg sti</string> + <string name="deckymgr_folder_failed">Kunne ikke opprette programtillegg-mappen</string> + <string name="deckymgr_zip_too_large">Programtillegg-ZIP-en pakker ut til mer enn 1 GiB-grensen</string> + <string name="deckymgr_bundled_not_executable">Kunne ikke merke den medfølgende programtilleggsfilen som kjørbar</string> + <string name="deckymgr_remote_entry_invalid">Ugyldig remote_binary-oppføring</string> + <string name="deckymgr_remote_name_invalid">Ugyldig navn på ekstern kjørbar fil</string> + <string name="deckymgr_remote_url_https">URL-en til den eksterne kjørbare filen må bruke HTTPS</string> + <string name="deckymgr_remote_hash_invalid">Den eksterne kjørbare filen %1$s har en ugyldig SHA-256-hash</string> + <string name="deckymgr_bin_dir_failed">Kunne ikke opprette bin-mappen for programtillegget</string> + <string name="deckymgr_downloading_binary">Laster ned programtilleggets kjørbare fil · %1$s</string> + <string name="deckymgr_binary_download_failed">Kunne ikke laste ned programtilleggets kjørbare fil %1$s</string> + <string name="deckymgr_binary_checksum_mismatch">Sjekksummen til den kjørbare filen stemmer ikke · %1$s</string> + <string name="deckymgr_binary_not_executable">Kunne ikke merke den kjørbare filen som kjørbar · %1$s</string> + <string name="deckymgr_binary_replace_failed">Kunne ikke erstatte den kjørbare filen · %1$s</string> + <string name="deckymgr_binary_install_failed">Kunne ikke installere den kjørbare filen · %1$s</string> + <string name="deckymgr_folder_taken">Programtillegg-mappen tilhører et annet programtillegg</string> + <string name="deckymgr_preserve_failed">Kunne ikke bevare det eksisterende programtillegget</string> + <string name="deckymgr_activate_failed">Kunne ikke aktivere det importerte programtillegget</string> + <string name="deckymgr_settings_failed">Kunne ikke oppdatere innstillingene for Decky-programtillegget</string> + <string name="deckymgr_restore_failed">Kunne ikke gjenopprette det eksisterende programtillegget</string> + + <string name="adbfix_save_failed">Kunne ikke lagre identiteten for trådløs feilsøking</string> + <string name="adbfix_command_failed">ADB-kommandoen mislyktes (%1$d)</string> + <string name="adbfix_not_confirmed">Android bekreftet ikke at grensen for underprosesser ble endret: %1$s</string> + <string name="adbfix_no_pairing">Ingen kobling for trådløs feilsøking er lagret</string> + <string name="adbfix_enter_address">Skriv inn dagens IP-adresse og port for trådløs feilsøking</string> + + <string name="adbpair_timed_out">Tidsavbrudd ved venting på koblingsvarselet. Prøv igjen fra DroidDeck.</string> + <string name="adbpair_discovery_failed_code">Kunne ikke se etter koblingsvarselet (%1$d). Kontroller at Wi-Fi er på.</string> + <string name="adbpair_discovery_failed">Kunne ikke se etter koblingsvarselet: %1$s</string> + <string name="adbpair_six_digits">Skriv inn alle 6 tallene i koden</string> + <string name="adbpair_pairing">Kobler sammen…</string> + <string name="adbpair_code_rejected">Den koden fungerte ikke. Åpne koblingsvarselet igjen og skriv inn den nye koden.</string> + <string name="adbpair_connecting">Koblet sammen. Kobler til…</string> + <string name="adbpair_port_not_found">Koblet sammen, men porten for trådløs feilsøking ble ikke funnet. La trådløs feilsøking stå på og prøv igjen.</string> + <string name="adbpair_applying">Bruker innstillingen…</string> + <string name="adbpair_command_failed">Kommandoen for trådløs feilsøking mislyktes</string> + <string name="adbpair_channel_name">Oppsett av trådløs feilsøking</string> + <string name="adbpair_channel_description">Ber om koblingskoden for trådløs feilsøking mens Innstillinger er åpen</string> + <string name="adbpair_title_pair">Koble sammen trådløs feilsøking</string> + <string name="adbpair_waiting_text">Slå på trådløs feilsøking, og trykk så på «Par enhet med koblingskode».</string> + <string name="adbpair_waiting_long">Slå på trådløs feilsøking, og trykk så på «Par enhet med koblingskode». Hold deg i Innstillinger: koden kommer i denne varslingen.</string> + <string name="adbpair_code_label">6-tallers koblingskode</string> + <string name="adbpair_code_hint">Trykk på Skriv inn kode og skriv de 6 tallene som vises i Innstillinger.</string> + <string name="adbpair_title_code">Skriv inn Wi-Fi-koblingskoden</string> + <string name="adbpair_title_retry">Prøv koden igjen</string> + <string name="adbpair_enter_code">Skriv inn kode</string> + <string name="adbpair_title_working">Setter opp Steam</string> + <string name="adbpair_title_done">Steam er klar</string> + <string name="adbpair_done_text">Trykk for å gå tilbake til DroidDeck. Du kan slå av trådløs feilsøking nå.</string> + <string name="adbpair_title_failed">Oppsettet for trådløs feilsøking stoppet</string> + + <string name="selfinst_http">Nedlastingen svarte HTTP %1$d</string> + <string name="selfinst_size_mismatch">Nedlastingens størrelse stemte ikke med den publiserte APK-en og ble forkastet – prøv igjen</string> + <string name="selfinst_checksum_mismatch">Nedlastingen stemte ikke med sjekksummen og ble forkastet – prøv igjen</string> + <string name="selfinst_wrong_package">Oppdateringen er for Android-pakken %1$s, ikke %2$s</string> + <string name="selfinst_metadata_signer">Oppdateringens metadata nevner ikke DroidDecks utgivelsessigneringsnøkkel</string> + <string name="selfinst_unreadable">Android kunne ikke lese den nedlastede APK-en</string> + <string name="selfinst_apk_wrong_package">Den nedlastede APK-en er for %1$s, ikke %2$s</string> + <string name="selfinst_version_mismatch">Den nedlastede APK-en sier versionCode %1$d, ikke den publiserte %2$d</string> + <string name="selfinst_downgrade">Android vil ikke installere versionCode %1$d over den installerte %2$d</string> + <string name="selfinst_no_signing">Den nedlastede APK-en har ingen signeringsinformasjon</string> + <string name="selfinst_wrong_signer">Den nedlastede APK-en er ikke signert med DroidDecks utgivelsesnøkkel</string> + <string name="appupd_http">Oppdateringskatalogen svarte HTTP %1$d</string> + + <string name="loadstate_starting">Starter sesjonen…</string> + <string name="loadstate_elapsed">%1$d:%2$02d gått · fortsatt i gang</string> + <string name="loadstate_steam_update">Laster ned Steam-klientoppdateringen · %1$d%%</string> + <string name="loadstate_steam_client">Laster ned Steam-klienten · %1$s</string> + + <string name="pextra_busy">En installasjon av et kompatibilitetsverktøy kjører allerede</string> + <string name="pextra_stop_session">Avslutt den aktive sesjonen før du installerer kompatibilitetsverktøy</string> + <string name="pextra_no_release">Fant ingen ARM64-utgivelse for %1$s</string> + <string name="pextra_no_space">Minst 4 GB ledig pluss nedlastingens størrelse er nødvendig for å installere %1$s</string> + <string name="pextra_downloading">Laster ned %1$s %2$s</string> + <string name="pextra_retrying">Prøver nedlastingen igjen · forsøk %1$d av %2$d</string> + <string name="pextra_interrupted">Nedlastingen ble avbrutt</string> + <string name="pextra_download_failed">Nedlastingen mislyktes; den delvise filen beholdes slik at et nytt forsøk kan fortsette</string> + <string name="pextra_too_big">Nedlastingen overskred utgivelsens størrelse og ble slettet</string> + <string name="pextra_incomplete">Nedlastingen var ikke fullført; den delvise filen beholdes for et gjenopptakende nytt forsøk</string> + <string name="pextra_verifying">Verifiserer nedlastingen</string> + <string name="pextra_checksum_unreadable">Kunne ikke lese utgivelsens sjekksum; prøv igjen</string> + <string name="pextra_no_checksum">%1$s %2$s publiserer ingen sjekksum; ingenting ble installert</string> + <string name="pextra_checksum_mismatch">Sjekksummen stemmer ikke; nedlastingen ble slettet</string> + <string name="pextra_session_started">En sesjon startet under nedlastingen; avslutt den før du installerer kompatibilitetsverktøy</string> + <string name="pextra_installing">Installerer %1$s</string> + <string name="pextra_install_failed">Installasjonen av %1$s mislyktes (avslutningskode %2$d)</string> + <string name="pextra_could_not_install">Kunne ikke installere %1$s</string> + <string name="esync_fetching">Henter droiddeck-esync-pakke</string> + + <string name="cmgr_fex_linux_helpers">med Linux-hjelpere</string> + <string name="cmgr_fex_dlls_only">Bare DLL-er</string> + <string name="cmgr_original_proton_updated">Original (Proton oppdatert siden siste bytte)</string> + <string name="cmgr_changed_outside">Endret utenfor Komponenter</string> + <string name="cmgr_not_present">ikke til stede</string> + <string name="cmgr_original_version">Original %1$s</string> + <string name="cmgr_original_from">Original fra %1$s</string> + <string name="cmgr_swap_queued">%1$s %2$s settes inn i %3$s når spillet som kjører på den avsluttes.</string> + <string name="cmgr_swap_done">%1$s %2$s er nå i %3$s. Det gjelder neste gang et spill starter.</string> + <string name="cmgr_original_missing">Den originale pakken er ikke lagret</string> + <string name="cmgr_restore_queued">%1$s av %2$s går tilbake til sin original når spillet som kjører på den avsluttes.</string> + <string name="cmgr_restore_done">%1$s av %2$s gjenopprettet fra sin %3$s-original.</string> + <string name="cmgr_package_in_use">%1$s er i bruk i %2$s. Bytt ut den Protonen med noe annet først.</string> + <string name="cmgr_package_not_stored">%1$s er ikke lagret</string> + <string name="cmgr_deleted">Slettet %1$s.</string> + <string name="cmgr_original_kept">Originalen til den installerte builden beholdes: det er veien tilbake.</string> + <string name="cmgr_not_stored">Ikke lagret</string> + <string name="cmgr_deleted_original">Slettet %1$s-originalen av %2$s.</string> + <string name="cmgr_nightlies_only">Nedlastinger kommer bare fra Nightlies-utgivelsene</string> + <string name="cmgr_list_predates_checksums">Denne pakkelisten er eldre enn sjekksummene – oppdater den og prøv igjen</string> + <string name="cmgr_checksum_mismatch">Sjekksummen stemmer ikke</string> + + <string name="sprefs_upscaler_linear">Lineær</string> + <string name="sprefs_upscaler_nearest">Nærmeste</string> + <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (kvalitet)</string> + <string name="sprefs_upscaler_sharpen">Bare skarphet</string> + <string name="sprefs_lod_bias_auto">Auto (følger skalering)</string> + + <string name="look_off_desc">Ingenting brukt.</string> + <string name="look_game_clarity">Spillklarhet</string> + <string name="look_game_clarity_desc">Skarphet med et snev kontrast – den for hver dag.</string> + <string name="look_vivid">Levende</string> + <string name="look_vivid_desc">Farger skrudd opp. Fargebåndfjerning på, så himmelen ikke striper.</string> + <string name="look_cinematic">Filmatisk</string> + <string name="look_cinematic_desc">Dypere kontrast, kjøligere farger, kanter utjevnet.</string> + <string name="look_competitive">Konkurranse</string> + <string name="look_competitive_desc">Skygger hevet og kraftig skarphet – se folk først.</string> + <string name="look_adaptive_sharpen">Adaptiv skarphet</string> + <string name="look_adaptive_sharpen_desc">Bare skarphet, farger urørt.</string> + <string name="look_filmic">Filmaktig</string> + <string name="look_filmic_desc">Filmlignende kurve, litt dempet.</string> + <string name="look_arcade">Arkade</string> + <string name="look_arcade_desc">Høylytt og kraftfull.</string> + <string name="look_retro_crt">Retro CRT</string> + <string name="look_retro_crt_desc">CRT-rørets og det analoge signalet går sammen.</string> + <string name="look_upscale_sharp">Oppskalert skarp</string> + <string name="look_upscale_sharp_desc">For å kjøre under skjermens oppløsning.</string> + <string name="look_pixel_clean">Piksel-ren</string> + <string name="look_pixel_clean_desc">Skarpe piksler for 2D og gamle titler.</string> + <string name="look_anime_edge">Anime-kant</string> + <string name="look_anime_edge_desc">Tegnefilm-konturer pluss skarphet.</string> + + <string name="gpuinfo_not_adreno">Ikke en Adreno-GPU</string> + <string name="gpuinfo_supported">Støttet</string> + <string name="gpuinfo_experimental">Eksperimentell: driverne er testbuilder</string> + <string name="gpuinfo_below_tested">Under testet maskinvare (Adreno 725 og nyere): den kan ikke kjøre</string> + <string name="gpuinfo_unsupported">Ikke støttet: DroidDeck krever en Adreno (Snapdragon) GPU</string> + + <string name="turnip_rate_limited">GitHubs hastighetsgrense ble nådd – prøv igjen senere</string> + <string name="turnip_github_http">GitHub svarte HTTP %1$d</string> + <string name="turnip_download_http">nedlastingen svarte HTTP %1$d</string> + <string name="turnip_list_predates_checksums">Denne driverlisten er eldre enn sjekksummene – oppdater den og prøv igjen</string> + <string name="turnip_checksum_mismatch">Sjekksummen stemmer ikke – nedlastingen ble forkastet</string> + <string name="turnip_not_adrenotools">Ikke en AdrenoTools-driver: %1$s</string> + <string name="turnip_reject_no_meta">ingen meta.json</string> + <string name="turnip_reject_meta_unreadable">meta.json kan ikke leses (%1$s)</string> + <string name="turnip_reject_linux_driver">dette er en Linux-kjøretidsdriver (importer den under «Linux-kjøretidsdriver»)</string> + <string name="turnip_reject_wayland">dette er en Wayland-spilldriver, som denne appen ikke har bruk for</string> + <string name="turnip_reject_no_library_name">meta.json nevner ingen libraryName, så ingenting kunne gis til AdrenoTools</string> + <string name="turnip_reject_library_missing">meta.json nevner %1$s, som ikke finnes i zip-filen</string> + <string name="turnip_reject_not_aarch64">%1$s er ikke et 64-bit AArch64 ELF-delt bibliotek</string> + <string name="turnip_reject_glibc">%1$s lenker glibc – en «-Linux»-Turnip, som hører under «Linux-kjøretidsdriver»</string> + <string name="lvd_no_library">Ingen libvulkan_freedreno*.so i denne zip-filen. En Android- (AdrenoTools-) eller -Wayland-Turnip-zip er ikke en Linux-kjøretidsdriver.</string> + <string name="lvd_not_aarch64">%1$s er ikke et 64-bit AArch64 ELF-delt bibliotek.</string> + <string name="lvd_not_glibc">%1$s er ikke en glibc-driver – den lenker Androids libc. Linux-kjøretiden krever en «-Linux»-zip; en vanlig eller «-Wayland»-Turnip kan ikke lastes av Steam-klienten i det hele tatt.</string> + <string name="bundle_no_manifest">Ikke en driverpakke: ingen manifest.json som nevner en android- og en linux-driver</string> + <string name="bundle_entry_outside">Ikke en driverpakke: %1$s peker utenfor den</string> + <string name="bundle_too_large">Ikke en driverpakke: den pakkes ut til mer enn %1$d MB</string> + + <string name="gstore_sd_card">SD-kort</string> + <string name="gstore_folder">Mappe</string> + + <string name="logshare_chooser">Del sesjonslogger</string> + + <string name="hdr_needs_android_10">krever Android 10 eller nyere (skjermlag med et fargerom)</string> + <string name="hdr_none">denne skjermen rapporterer ingen HDR-støtte</string> + <string name="hdr_not_hdr10">denne skjermen støtter %1$s, ikke HDR10</string> + + <string name="gpu_this_gpu">denne GPU-en</string> + <string name="phantom_title_on">Grensen for underprosesser er på</string> + <string name="phantom_title_unset">Grensen for underprosesser er ikke angitt</string> + <string name="phantom_title_unreadable">Grensen for underprosesser kunne ikke kontrolleres</string> + <string name="phantom_title_off">Grensen for underprosesser er av</string> + <string name="phantom_title_not_required">Grensen for underprosesser ikke nødvendig</string> + <string name="phantom_instructions_fix">%1$s Hvis det ikke er mulig, koble denne enheten til en datamaskin med ADB og kjør kommandoen under. Kom tilbake hit og kontroller igjen.</string> + <string name="phantom_instructions_off">Steam-sesjoner kan starte.</string> + <string name="phantom_instructions_not_applicable">Denne Android-versjonen bruker ikke denne grensen.</string> + <string name="phantom_fix_toggle">Slå på «Disable child process restrictions» i Utvikleralternativer, og la Utvikleralternativer stå på etterpå.</string> + <string name="phantom_fix_no_toggle">Denne Android-versjonen har ingen bryter for det. Bruk trådløs feilsøking for å endre det fra denne enheten.</string> + <string name="upd_block_no_apk">Denne builden har ingen nedlasting for denne kopien av DroidDeck.</string> + <string name="upd_block_version_code">Android vil ikke installere den over denne builden fordi dens versionCode %1$d er under den installerte %2$d.</string> + <string name="upd_fail_downgrade">Android vil ikke installere en eldre versjon over en nyere. Bli på denne builden til kanalen tar den igjen.</string> + <string name="upd_fail_signature">Denne kopien av DroidDeck er signert annerledes (en lokal build?), så Android vil ikke oppdatere den direkte.</string> + <string name="upd_fail_storage">Det er ikke nok ledig plass for oppdateringen.</string> + <string name="upd_fail_aborted">Oppdateringen ble avbrutt.</string> + <string name="upd_fail_with_message">Oppdateringen ble ikke installert: %1$s</string> + <string name="upd_fail_generic">Oppdateringen ble ikke installert.</string> + <string name="saves_layout_zip">%1$s-zip</string> + <string name="saves_zip">zip</string> + <string name="saves_not_launched">%1$s har ikke blitt startet ennå</string> + <string name="saves_none_found">Fant ingen lagringsfiler for %1$s</string> + <string name="saves_not_save_zip">%1$s har ingen drive_c-mappe, så det er ikke en GameHub- eller Winlator-lagrings-zip</string> + <string name="gstore_cannot_create">kan ikke opprette mapper i %1$s</string> + <string name="gstore_cannot_write">kan ikke skrive i %1$s</string> + <string name="gstore_cannot_write_detail">kan ikke skrive i %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">Mappen er ikke tilgjengelig</string> + <string name="gfiles_name_taken">En annen fil bruker allerede %1$s</string> + <string name="gfiles_art_unavailable">Grafikkmappen er ikke tilgjengelig</string> + <string name="gfiles_unrelated_inventory">Mappen har en urelatert synkroniseringsoversikt</string> + <string name="gfiles_remove_failed">Kunne ikke fjerne %1$s</string> + <string name="gfiles_outside_folder">Filen ligger utenfor eksportmappen</string> + <string name="cmgr_fex_unnamed">FEX (build uten versjonsnavn)</string> + <string name="cmgr_no_profile">ingen profile.json i %1$s</string> + <string name="cmgr_not_component_package">%1$s er ikke en Linux-komponentpakke (type %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s ledig</string> + <string name="mode_storage_auto_short">Automatisk</string> + <string name="appupd_catalog_rejected">Oppdateringskatalogen mislyktes i sine kontroller (%1$s)</string> + <string name="main_adb_clip_label">DroidDeck-innstilling for underprosesser</string> + <string name="setup_signed_in_offline_unnamed">Innlogget · frakoblet modus</string> + <string name="setup_signed_in_unnamed">Innlogget</string> + <string name="driver_tag_bundled">MEDFØLGER</string> + <string name="driver_tag_downloaded">NEDLASTET</string> + <string name="driver_tag_imported">IMPORTERT</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Sesjonsskjerm</string> + <string name="display_resolution">Oppløsning</string> + <string name="display_game_fit">16:9-spilltilpasning</string> + <string name="display_game_fit_hint">Hvordan 16:9-spill tilpasses denne skjermen. Fra neste sesjon.</string> + <string name="display_game_fit_unavailable">Velg Tilpass skjerm eller en egendefinert størrelse smalere enn 16:9.</string> + <string name="display_preserve">Behold proporsjoner</string> + <string name="display_stretch">Strekk til skjermen</string> + <string name="display_image_scaling">Bildeskalering</string> + <string name="display_filter">Skaleringsfilter</string> + <string name="display_filter_hint">Delt av Steam og Skrivebord.</string> + <string name="display_filter_live">Gjelder live. Delt av Steam og Skrivebord.</string> + <string name="display_filter_note">FSR, GSR og NIS gjelder ved forstørring av sesjonsbildet. Bare skarphet fungerer også uten forstørring.</string> + <string name="display_sharpness">Filterskarphet</string> + <string name="display_frame_rate">Bildefrekvens</string> + <string name="display_look_filter">%1$s Setter skalering til %2$s.</string> + <string name="display_match_screen">Tilpass skjerm (%1$d×%2$d)</string> + <string name="display_follow_screen">Følg skjermen (endrer størrelse ved sammenslåing eller utbretting)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Vis enhetsdetaljer</string> + <string name="device_info_hide">Skjul enhetsdetaljer</string> + <string name="device_info_loading">Leser enheten…</string> + <string name="device_info_copy">Kopier</string> + <string name="device_info_copy_hint">Kopier disse detaljene inn i en feilrapport.</string> + <string name="device_info_copied">Kopiert.</string> + <string name="force_ssbs_title">Tvungen SSBS</string> + <string name="force_ssbs_hint">Holder en CPU-hastighetsinnstilling som Wine stadig slår av. Kan gjøre Windows-spill raskere på Snapdragon 8 Elite; lite endring sett på eldre brikker. Gjelder fra neste spilløkt.</string> + + <string name="rail_stores">Butikker</string> + <string name="source_steam">Steam</string> + <string name="source_added">Egendefinert</string> + <string name="setup_stores">Butikker</string> + <string name="setup_stores_open_on">Åpne en butikk på</string> + <string name="setup_stores_speed">Nedlastingshastighetsnivå</string> + <string name="setup_stores_engine">Butikkmotor</string> + <string name="setup_stores_engine_chip_ready">● Motor klar</string> + <string name="setup_stores_engine_chip_missing">Ikke bygget</string> + <string name="stores_tier_balanced">Balansert</string> + <string name="stores_tier_fast">Rask</string> + <string name="stores_tier_max">Maks</string> + <string name="stores_title">Butikker</string> + <string name="stores_settings">Butikkinnstillinger</string> + <string name="stores_settings_eyebrow">Innstillinger</string> + <string name="stores_chip_signed_out">(ikke innlogget)</string> + <string name="stores_tab_store">Butikk</string> + <string name="stores_tab_installed">Installert</string> + <string name="stores_tab_library">Bibliotek</string> + <string name="stores_tab_all">Alle</string> + <string name="stores_tab_downloads">Nedlastinger</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Logg inn</string> + <string name="stores_signout_named">%1$s · logg ut</string> + <string name="stores_signout_title">Logge ut av %1$s?</string> + <string name="stores_signout_note">Installerte spill blir liggende. Biblioteket hentes på nytt når du logger inn igjen.</string> + <string name="stores_signout_stay">Fortsett innlogget</string> + <string name="stores_signout_go">Logg ut</string> + <string name="stores_search_catalog">Søk i %1$s-katalogen…</string> + <string name="stores_search_installed">Søk i installerte spill…</string> + <string name="stores_search_library">Søk i biblioteket ditt…</string> + <string name="stores_search_open">Søk</string> + <string name="stores_installed_empty">Ingenting installert</string> + <string name="stores_library_loading">Laster</string> + <string name="stores_nothing_matches">Ingen spill</string> + <string name="stores_shelves_loading">Laster butikken…</string> + <string name="stores_amazon_no_catalog">Ingen katalog</string> + <string name="stores_shelf_new">Nytt</string> + <string name="stores_shelf_deals">Tilbud</string> + <string name="stores_shelf_free">Gratisspill</string> + <string name="stores_shelf_trending">Trender</string> + <string name="stores_shelf_library">Biblioteket ditt</string> + <string name="stores_play">Spill</string> + <string name="stores_install">Installer</string> + <string name="stores_resume_install">Gjenoppta installasjon</string> + <string name="stores_dl_interrupted">Avbrutt</string> + <string name="stores_install_size">Installer · %1$s</string> + <string name="stores_get_free">Hent gratis</string> + <string name="stores_view_on">Vis på %1$s</string> + <string name="stores_buy_price">Kjøp · %1$s</string> + <string name="stores_installed_chip">Installert</string> + <string name="stores_in_library">I biblioteket</string> + <string name="stores_free">Gratis</string> + <string name="stores_resume">Gjenoppta</string> + <string name="stores_refresh">Oppdater</string> + <string name="stores_show_mature">Vis innhold for voksne</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Frakoblet</string> + <string name="epic_card_none">Av</string> + <string name="epic_eos">Epic-innlogging (EOS)</string> + <string name="epic_offline">Start frakoblet</string> + <string name="epic_resolve">Løs Epic-innlogging</string> + <string name="store_cloud_saves">Skylagringer</string> + <string name="cloud_upload">Last opp til skyen</string> + <string name="cloud_download">Last ned fra skyen</string> + <string name="cloud_none">Ingen skylagringer</string> + <string name="cloud_conflict">Skykonflikt</string> + <string name="cloud_conflict_files">Skykonflikt · %1$d filer</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Behold skyversjon</string> + <string name="cloud_keep_local">Behold lokal versjon</string> + <string name="cloud_checking">Kontrollerer skyen…</string> + <string name="cloud_last_sync">Siste synkronisering %1$s</string> + <string name="cloud_never">Ikke synkronisert ennå</string> + <string name="cloud_done">%1$d filer</string> + <string name="stores_eyebrow_installed">installert</string> + <string name="stores_eyebrow_library">i biblioteket ditt</string> + <string name="stores_eyebrow_store">butikk</string> + <string name="achievements_title">Prestasjoner</string> + <string name="achievements_count">%1$d av %2$d låst opp</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Låst opp · %1$d</string> + <string name="achievements_group_locked">Låst · %1$d</string> + <string name="achievements_group_hidden">Skjult · %1$d</string> + <string name="achievements_hidden">Skjult prestasjon</string> + <string name="achievements_hidden_detail">Detaljer vises når den er låst opp.</string> + <string name="achievements_all">Alle prestasjoner låst opp</string> + <string name="achievements_reading">Laster…</string> + <string name="achievements_signed_out">Logg inn på %1$s for å se prestasjonene dine.</string> + <string name="achievements_unreachable">Kunne ikke nå %1$s. Sjekk igjen når du er på nett.</string> + <string name="achievements_none">%1$s har ingen prestasjoner for dette spillet.</string> + <string name="achievements_show_all">Vis alle %1$d</string> + <string name="achievements_dialog_title">Prestasjoner · %1$s</string> + <string name="stores_in_steam">✓ I Steam</string> + <string name="stores_uninstall">Avinstaller</string> + <string name="stores_uninstalling">Avinstallerer…</string> + <string name="stores_uninstall_title">Avinstallere %1$s?</string> + <string name="stores_uninstall_body_size">Frigjør %1$s og fjerner det fra Steam.</string> + <string name="stores_uninstall_body">Fjerner filene og Steam-snarveien.</string> + <string name="stores_launch_not_registered">Steam plukker opp spillet neste gang det starter; start det derfra, eller start Steam nå.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Steams valg per spill</string> + <string name="stores_card_audio">Lyd</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Komponenter</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Motor</string> + <string name="stores_card_engine_value">%1$s · nativ</string> + <string name="stores_card_engine_java">%1$s · innebygd</string> + <string name="stores_how_it_runs">Hvordan det kjører</string> + <string name="stores_kv_fetched">Hentet fra</string> + <string name="stores_kv_fetched_value">%1$s, med kontoen din</string> + <string name="stores_kv_installed_to">Installert til</string> + <string name="stores_kv_installed_to_value">Spillagring (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Startet av</string> + <string name="stores_kv_launched_by_value">Steam og Proton, samme sti som Steam-spill</string> + <string name="stores_kv_launched_by_epic">Steam og Proton · Epic-innlogging per start, EOS-overlegg av</string> + <string name="stores_kv_launched_by_amazon">Steam og Proton · Amazon Games Services-variabler satt</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">lagt til i Steam ved installasjon (Deck-grensesnitt + Steam Input)</string> + <string name="stores_downloads_empty">Ingen nedlastinger</string> + <string name="stores_dl_paused">Pauset</string> + <string name="stores_dl_queued">I kø</string> + <string name="stores_dl_queued_at">I kø · #%1$d</string> + <string name="stores_dl_cancelled">Avbrutt</string> + <string name="stores_dl_failed">Mislyktes</string> + <string name="stores_dl_failed_reason">Mislyktes · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Pause</string> + <string name="stores_dl_resume">Gjenoppta</string> + <string name="stores_dl_cancel">Avbryt</string> + <string name="stores_dl_retry">Prøv igjen</string> + <string name="stores_dl_clear">Fjern</string> + <string name="stores_dl_clear_all">Fjern fullførte</string> + <string name="stores_stage_manifest">Manifest</string> + <string name="stores_stage_download">Nedlasting</string> + <string name="stores_stage_verify">Verifisering</string> + <string name="stores_stage_install">Installasjon</string> + <string name="stores_stage_done">Ferdig</string> + <string name="stores_stage_checking">Kontrollerer</string> + <string name="stores_stage_downloading">Laster ned</string> + <string name="stores_dl_cancel_confirm">Slette nedlastingen?</string> + <string name="stores_dl_failed_kept">Mislyktes · %1$s beholdt</string> + <string name="stores_stage_verifying">Verifiserer</string> + <string name="stores_stage_installing">Installerer</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d filer</string> + <string name="stores_download_manager">Nedlastingsbehandling</string> + <string name="stores_parallel">Samtidige nedlastinger</string> + <string name="stores_install_where">Installer til</string> + <string name="stores_install_title">Installer %1$s:</string> + <string name="stores_target_internal">Intern</string> + <string name="stores_target_free">%1$s ledig</string> + <string name="stores_notification_channel">Butikknedlastinger</string> + <string name="stores_notification_channel_hint">Viser nedlastingsfremgang for spill og holder nedlastinger i gang i bakgrunnen</string> + <string name="stores_notification_one">Laster ned</string> + <string name="stores_notification_many">%1$d nedlastinger</string> + <string name="stores_notification_waiting">Forbereder…</string> + <string name="stores_notification_done_channel">Fullførte butikknedlastinger</string> + <string name="stores_notification_installed">%1$s installert</string> + <string name="stores_notification_failed">Nedlasting av %1$s mislyktes</string> + <string name="stores_notification_delete">Slett</string> + <string name="stores_notification_keep">Behold</string> + <string name="stores_login_title">Logg inn på %1$s</string> + <string name="stores_login_provider_title">Logg inn med leverandøren din</string> + <string name="stores_login_reload">Last på nytt</string> + <string name="stores_login_close">Lukk</string> + <string name="stores_login_retry">Prøv igjen</string> + <string name="stores_login_finishing">Fullfører innlogging…</string> + <string name="stores_login_error_network">Innloggingssiden kunne ikke lastes: %1$s</string> + <string name="stores_login_error_idp_blocked">Denne leverandøren tillater ikke innlogging fra en innebygd side. Logg inn med butikkpassordet ditt i stedet.</string> + <string name="stores_login_error_http">Innloggingssiden svarte med en feil (HTTP %1$d).</string> + <string name="stores_login_error_ssl">Tilkoblingen til innloggingssiden er ikke sikker.</string> + <string name="stores_login_error_verification">Innloggingen kunne ikke verifiseres. Prøv igjen.</string> + <string name="stores_login_error_generic">Innlogging mislyktes. Prøv igjen.</string> + <string name="common_close">Lukk</string> + <string name="common_back">Tilbake</string> + <string name="common_edit">Rediger</string> + <string name="games_add_all">Legg til alle spill i denne mappen</string> + <string name="games_added_none">Ingenting nytt i denne mappen</string> + <string name="edit_name">Navn</string> + <string name="edit_target">Mål</string> + <string name="edit_start_in">Start i</string> + <string name="edit_launch_options">Startalternativer</string> + <string name="edit_artwork">Grafikk</string> + <string name="edit_other_exe">Annen exe…</string> + <string name="edit_remove">Fjern</string> + <string name="art_cover">Omslag</string> + <string name="art_background">Bakgrunn</string> + <string name="art_logo">Logo</string> + <string name="art_icon">Ikon</string> + <string name="art_src_folder">Spillmappe</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">Din fil</string> + <string name="art_src_exe">Fra exe-filen</string> + <string name="art_src_auto">Automatisk</string> + <string name="art_pick_image">Velg bilde…</string> + <string name="art_pick_title">Velg et bilde: %1$s</string> + <string name="art_reset">Tilbakestill til automatisk</string> + <plurals name="games_added_n"> + <item quantity="one">%1$d spill lagt til</item> + <item quantity="other">%1$d spill lagt til</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="one">%1$d allerede i Spill</item> + <item quantity="other">%1$d allerede i Spill</item> + </plurals> + <string name="hop_back">B · Tilbake til %1$s</string> + <string name="hop_from_games">spillet</string> + <string name="hop_from_play">Spillinnstillinger</string> + <string name="sheet_graphics_drivers">Grafikkdrivere</string> + <string name="comp_this_proton">Denne Protonen</string> + <string name="comp_every_proton">Hver Proton</string> + <string name="comp_for_every_proton">For hver Proton</string> + <string name="comp_available_count">Tilgjengelig · %1$d</string> + <string name="comp_checked">Sjekket %1$s</string> + <string name="comp_check_all">Se etter nye komponenter og drivere</string> + <string name="comp_get">Hent</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton og FEX under hører til denne Protonen</string> + <string name="comp_device_tested">%1$s · testet</string> + <string name="comp_device_untested">%1$s · ikke testet</string> + <string name="comp_device_unsupported">%1$s · ikke støttet</string> + <string name="gpu_auto_keeps">Holder det anbefalte paret oppdatert</string> + <string name="gpu_bar_auto">Auto · %1$s</string> + <string name="gpu_separately">Kjøretids- og skjermdrivere separat</string> + <string name="gpu_use_title">Bruke %1$s?</string> + <string name="proton_remove_title">Fjerne %1$s?</string> + <string name="setup_tab_diagnostics">Diagnostikk</string> + <string name="setup_ready">Klar til å spille</string> + <string name="setup_not_ready">Ikke klar til å spille ennå</string> + <string name="setup_ready_runtime">kjøretid %1$s</string> + <string name="setup_ready_limit_off">prosessgrense av</string> + <string name="setup_ready_signed_in">innlogget som %1$s</string> + <string name="setup_needs_you">Når noe trenger deg</string> + <string name="setup_group_look">Utseende</string> + <string name="setup_group_device">Denne enheten</string> + <string name="setup_group_art">Biblioteksgrafikk</string> + <string name="flathub_eyebrow">LINUX-APPER</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Skrivebordsapper for Linux-skrivebordet, åpnet fra Skrivebord.</string> + <string name="flathub_setup">Sett opp Flathub</string> + <string name="flathub_remove_title">Fjerne %1$s?</string> + <string name="flathub_remove_note">Innstillingene og filene beholdes.</string> + <string name="flathub_keep">Behold</string> + <string name="desktop_size">Størrelse på disk</string> + <string name="desktop_uninstall_title">Avinstallere skrivebordet?</string> + <string name="desktop_all_systems">Alle</string> + <string name="gog_achievements">GOG-prestasjoner</string> + <string name="gog_achievements_hint">Synkroniserer prestasjoner for støttede GOG-spill. Gjelder fra neste oppstart.</string> + <string name="gog_achievements_off">Prestasjoner av</string> + <string name="gog_achievements_on">Prestasjoner på</string> + <string name="gog_achievements_signin">Logg inn på GOG igjen</string> + <string name="gog_achievements_uploaded">Prestasjoner lastet opp</string> + <string name="gog_achievements_pending">Opplasting av prestasjon venter</string> + <string name="gog_achievements_enable_store">Slå på GOG-prestasjoner i Store-innstillinger</string> + <string name="gog_achievements_unavailable">Prestasjoner ikke tilgjengelig; sjekk sesjonsloggene</string> + <string name="achievement_unlocked">Prestasjon låst opp</string> + <string name="touch_paste">Lim inn</string> + <string name="touch_dpad">Styrekors</string> + <string name="touch_left_stick">Venstre stikke</string> + <string name="touch_right_stick">Høyre stikke</string> + <string name="touch_menu">Meny</string> + <string name="touch_keyboard">Tastatur</string> + <string name="touch_dpad_up">Styrekors opp</string> + <string name="touch_dpad_down">Styrekors ned</string> + <string name="touch_dpad_left">Styrekors venstre</string> + <string name="touch_dpad_right">Styrekors høyre</string> + <string name="touch_title">Berøringskontroller</string> + <string name="touch_controller">Berøringskontroll</string> + <string name="touch_mouse">Mus</string> + <string name="touch_on">På</string> + <string name="touch_off">Av</string> + <string name="touch_speed_slow">Sakte</string> + <string name="touch_speed_normal">Normal</string> + <string name="touch_speed_fast">Rask</string> + <string name="touch_edit_layout">Rediger oppsett</string> + <string name="touch_droiddeck_menu">DroidDeck-meny</string> + <string name="touch_drag_hint">Dra kontroller ut på skjermen</string> + <string name="touch_arrange">Ordne</string> + <string name="touch_discard">Forkast</string> + <string name="touch_saving">Lagrer…</string> + <string name="touch_icon">Ikon</string> + <string name="touch_narrower">B−</string> + <string name="touch_wider">B+</string> + <string name="touch_shorter">H−</string> + <string name="touch_taller">H+</string> + <string name="touch_hide">Skjul</string> + <string name="touch_color_opacity">Farge og opasitet…</string> + <string name="touch_copy_layout">Kopier dette oppsettet</string> + <string name="touch_layout_copied">Oppsett kopiert; lim det inn i berøringsoppsettet til et hvilket som helst spill</string> + <string name="touch_paste_layout">Lim inn kopiert oppsett</string> + <string name="touch_reset_layout">Tilbakestill til Steams standard</string> + <string name="touch_no_binding">Denne kontrollen har ingen binding i Steam å gi et ikon</string> + <string name="touch_icon_for">Ikon for %1$s</string> + <string name="touch_layout_color">Farge på berøringsoppsett</string> + <string name="touch_done">Fullført</string> + <string name="touch_save_failed">Kunne ikke lagre berøringsoppsettet</string> + <string name="touch_saved">Berøringsoppsett lagret til Steam</string> + <string name="touch_app_id">App %1$d</string> + <string name="touch_label_hint">Etikett (valgfritt)</string> + <string name="touch_icons_missing">Steams ikoner ble ikke funnet</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d ikon fra Steam</item> + <item quantity="other">%1$d ikoner fra Steam</item> + </plurals> + <string name="touch_icon_color">Ikonfarge</string> + <string name="touch_button_color">Knappefarge</string> + <string name="touch_no_icon">Ikke noe ikon</string> + + <string name="stores_sync_sign_in">Kontrollerer innlogging…</string> + <string name="stores_sync_library">Henter biblioteket ditt…</string> + <string name="stores_sync_game_list">Henter spilliste…</string> + <string name="stores_sync_details">Laster spilldetaljer… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">Synkroniserer %1$d spill…</item> + <item quantity="other">Synkroniserer %1$d spill…</item> + </plurals> + <string name="stores_sync_progress">Synkroniserer… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Henter omslagsbilder…</string> + <string name="stores_sync_failed">Synkronisering mislyktes</string> + <string name="stores_sync_no_games">Fant ingen spill i %1$s-biblioteket ditt</string> + <string name="stores_sync_amazon_no_answer">Amazon Games svarte ikke for biblioteket ditt: oppdater, eller logg inn på nytt</string> + <string name="stores_sync_gog_unreachable">Kunne ikke nå GOG</string> + <string name="stores_sync_gog_unreadable">Kunne ikke lese GOG-biblioteket ditt</string> + <string name="stores_registering">Registrerer hos Steam…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Cardboard-modus</string> + <string name="cardboard_mode_hint">Bilder for venstre og høyre øye til en VR-brille for mobil.</string> + <string name="cardboard_source">Bilde</string> + <string name="cardboard_source_screen">Virtuell skjerm</string> + <string name="cardboard_source_sbs">Side-ved-side-3D</string> + <string name="cardboard_viewer">Briller</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Ingen linse</string> + <string name="cardboard_ipd">Øyeavstand</string> + <string name="cardboard_screen_width">Skjermbredde</string> + <string name="cardboard_screen_distance">Skjermavstand</string> + <string name="cardboard_head_tracking">Hodesporing</string> + <string name="cardboard_no_sensor">Denne enheten har ingen rotasjonssensor.</string> + <string name="cardboard_recenter">Sentrer visningen</string> + <string name="cardboard_recenter_button">Sentrer</string> + <string name="cardboard_steamvr">SteamVR (eksperimentell)</string> + <string name="cardboard_steamvr_install">Installer</string> + <string name="cardboard_steamvr_install_hint">Laster ned Valves SteamVR for ARM64 (571 MB).</string> + <string name="cardboard_steamvr_installing">Laster ned og pakker ut SteamVR …</string> + <string name="cardboard_steamvr_start">Start</string> + <string name="cardboard_steamvr_ready_hint">Kjører SteamVR med hodet ditt som headset; bildet bytter til side-ved-side-3D.</string> + <string name="cardboard_steamvr_stop">Stopp</string> + <string name="cardboard_steamvr_running_hint">SteamVR kjører.</string> + <string name="cardboard_steamvr_unavailable">Ikke tilgjengelig i denne økten.</string> + <string name="cardboard_steamvr_failed">SteamVR svarte ikke; sjekk sesjonsloggen.</string> + <string name="drawer_component_downloads">Last ned komponenter</string> + <string name="drawer_component_downloads_hint">Nedlastinger deles med Komponenter-siden. Velg en nedlastet versjon ovenfor for å bruke den fra neste spilløkt.</string> + <string name="drawer_component_choose_download">Velg en versjon å laste ned</string> + <string name="stores_fact_developer">Utvikler</string> + <string name="stores_fact_publisher">Utgiver</string> + <string name="stores_fact_released">Utgitt</string> + <string name="stores_hltb_main">Hovedhistorie</string> + <string name="stores_hltb_extras">+ Ekstra</string> + <string name="stores_hltb_completionist">Komplettering</string> + <string name="stores_hltb_hours">%1$s t</string> + <string name="stores_hltb_minutes">%1$d min</string> +</resources> diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml new file mode 100644 index 000000000..e34010cf8 --- /dev/null +++ b/app/src/main/res/values-nl/strings.xml @@ -0,0 +1,2235 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>Bij de eerste start wordt Steam gedownload. Dit kan een paar minuten duren.</item> + <item>Standaard wordt Steam gepauzeerd als je de app verlaat; als je terugkomt, gaat het verder waar je was.</item> + <item>Je controller wordt gezien als Xbox 360-controller. Gebruik anders de schermbediening.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>Bij de eerste start wordt de desktop ingericht. Dit kan een paar minuten duren.</item> + <item>Standaard wordt de sessie gepauzeerd als je de app verlaat; als je terugkomt, gaat die verder waar je was.</item> + <item>Aanraken werkt als touchpad: sleep om de aanwijzer te verplaatsen, tik om te klikken.</item> + <item>Je controller wordt gezien als Xbox 360-controller. Gebruik anders de schermbediening.</item> + </string-array> + <string name="session_channel">Sessie</string> + <string name="session_channel_description">Wordt getoond zolang er een Steam- of desktopsessie open is</string> + <string name="session_notification">Steam is actief</string> + <string name="session_notification_downloads">Steam-download actief</string> + <string name="session_notification_paused">Sessie gepauzeerd - tik op Hervatten als je klaar bent</string> + <string name="resume_session">Hervatten</string> + <string name="suspend_now">Nu onderbreken</string> + <string name="stop_session">Sessie stoppen</string> + <string name="frame_gen_title">Framegeneratie</string> + <string name="frame_gen_hint">Kies de modus voor framegeneratie</string> + <string name="frame_gen_off">Uit</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG adaptief %1$d</string> + <string name="lsfg_needs_steam">Vereist Lossless Scaling op Steam</string> + <string name="lsfg_needs_install">Installeer Lossless Scaling in Steam of importeer Lossless.dll</string> + <string name="lsfg_import">Lossless.dll importeren…</string> + <string name="lsfg_using_steam">De kopie uit Steam wordt gebruikt</string> + <string name="lsfg_using_import">Een geïmporteerde kopie wordt gebruikt</string> + <string name="lsfg_pick_title">Kies Lossless.dll</string> + <string name="lsfg_imported">Lossless.dll geïmporteerd</string> + <string name="lsfg_import_unchanged">Deze Lossless.dll wordt al gebruikt</string> + <string name="lsfg_import_not_owned">Lossless Scaling staat niet op dit Steam-account</string> + <string name="lsfg_import_invalid">Dit bestand is niet de Lossless.dll van Lossless Scaling</string> + <string name="lsfg_import_failed">Kan LSFG niet voorbereiden met deze Lossless.dll</string> + <string name="lsfg_problem_missing">LSFG vereist Lossless Scaling uit Steam of een geïmporteerde Lossless.dll</string> + <string name="lsfg_problem_failed">LSFG kan zijn shaders niet voorbereiden uit Lossless.dll</string> + <string name="hud_fg_unsupported">%1$s: niet ondersteund</string> + <string name="hud_fg_failed">%1$s: engine mislukt</string> + <string name="hud_fg_needs_lossless">%1$s: vereist Lossless Scaling</string> + <string name="hud_fg_starting">%1$s %2$s · starten</string> + <string name="second_library_import_hint">Voegt een Steam-bibliotheek toe en scant die op Windows-games. Games die je bezit gebruiken hun Steam-items; de rest wordt een snelkoppeling voor een niet-Steam-game.</string> + <string name="game_env_title">Game-omgeving</string> + <string name="game_env_hint">Omgevingsvariabelen voor Proton-games.</string> + <string name="game_env_edit">Bewerken</string> + <string name="game_env_shared">Alle Proton-games</string> + <string name="game_env_scope">Profiel</string> + <string name="game_env_app_id">Steam-app-ID</string> + <string name="game_env_open">Openen</string> + <string name="game_env_profile">Game %1$s</string> + <string name="game_env_add">Variabele toevoegen</string> + <string name="game_env_name">Naam van variabele</string> + <string name="game_env_value">Waarde</string> + <string name="game_env_save">Opslaan</string> + <string name="game_env_remove">Verwijderen</string> + <string name="game_env_restore">Weer overnemen</string> + <string name="game_env_reset">Omgevingsoverschrijvingen wissen</string> + <string name="game_env_unset">Niet instellen voor dit profiel</string> + <string name="game_env_empty">Geen variabelen ingesteld.</string> + <string name="game_env_inherited">Overgenomen</string> + <string name="game_env_custom">Aangepaste variabele</string> + <string name="game_env_error">Kan niet laden of opslaan. Controleer de vrije opslagruimte en probeer het opnieuw.</string> + <string name="game_env_invalid">Gebruik een geldige naam en een waarde van maximaal 8192 tekens.</string> + <string name="game_env_unused">De standaard Proton-runtime leest deze variabele niet.</string> + <string name="game_env_loading">Laden…</string> + <string name="game_env_feature">Overschrijving van het D3D12-featureniveau. 12_2 meldt DirectX 12 Ultimate-mogelijkheden; niet-ondersteunde functies kunnen mislukken. Niet instellen voor automatische detectie.</string> + <string name="game_env_shader">Overschrijving van het D3D12-shadermodel. 6_9 vereist een voldoende recente VKD3D en bijpassende GPU-functies. Niet instellen voor automatische detectie.</string> + <string name="game_env_cache">Houd de Mesa-shadercache ingeschakeld om herhaald compileren te beperken. false schakelt caching in.</string> + <string name="game_env_cache_size">Maximale grootte van de Mesa-shadercache, zoals 1G. Grotere caches gebruiken meer opslag.</string> + <string name="game_env_vkd3d_config">VKD3D-opties voor D3D12. nodxr schakelt raytracing uit; gebruik dit alleen voor een game die dat nodig heeft.</string> + <string name="game_env_proton_log">Stel in op 1 voor een diagnostisch Proton-log. Loggen kan de prestaties verlagen en kost opslag.</string> + <string name="game_env_wined3d">Stel in op 1 om voor compatibiliteitstests met D3D9–11 het op OpenGL gebaseerde WineD3D te gebruiken in plaats van DXVK.</string> + <string name="game_env_no_ntsync">Stel in op 1 om deze game niet op ntsync (droiddeck-ntsync of een kerneldriver) te draaien; dan wordt droiddeck-esync gebruikt als er een pakket is, anders droiddeck-fsync.</string> + <string name="game_env_no_esync">Stel in op 1 om droiddeck-esync uit te zetten voor deze game; dan wordt droiddeck-fsync gebruikt, of droiddeck-ntsync als fsync ook uit staat.</string> + <string name="game_env_no_fsync">Stel in op 1 om deze game niet op droiddeck-fsync te draaien; dan wordt droiddeck-esync gebruikt als er een pakket is, anders droiddeck-ntsync.</string> + <string name="game_env_fsync_first">Stel in op 1 om deze game op droiddeck-fsync te draaien, of op 0 om droiddeck-esync voorrang te geven als er een pakket bij de Proton-versie past.</string> + <string name="game_env_xalia">Stel in op 0 om de gamepadnavigatiehulp van Proton voor deze game uit te schakelen.</string> + <string name="game_env_hud">DXVK-overlay voor D3D9–11. fps toont de framerate; niet ingesteld verbergt de overlay.</string> + <string name="game_env_dxvk_config">DXVK-instellingen, gescheiden door puntkomma’s. Recente builds gebruiken dxvk.maxFrameRate om de framerate te begrenzen; gebruik voor normaal spelen liever de framelimiet van de sessie.</string> + <string name="game_env_vkd3d_limit">Framerate-limiet voor D3D12. 0 schakelt de limiet uit.</string> + <string name="game_env_glthread">OpenGL-threading van Mesa. true kan helpen bij CPU-beperkte OpenGL-games; test per game.</string> + <string name="sync_backend_title">Wine-synchronisatie</string> + <string name="sync_backend_hint">Hoe games hun threads gelijk laten lopen. Geldt vanaf de volgende sessie.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">FEX-voorinstelling</string> + <string name="fex_next_launch">Geldt bij de volgende start van een game.</string> + <string name="game_settings_title">Games</string> + <string name="fex_default_label">FEX-standaard</string> + <string name="fex_default_detail">Gebruikt de standaardinstellingen van FEX.</string> + <string name="fex_stability_label">Stabiliteit</string> + <string name="fex_stability_detail">Veiligste, traagste instellingen.</string> + <string name="fex_compatibility_label">Compatibiliteit</string> + <string name="fex_compatibility_detail">Volledige TSO met multiblock.</string> + <string name="fex_intermediate_label">Gemiddeld</string> + <string name="fex_intermediate_detail">Gebalanceerde TSO met lagere x87-precisie.</string> + <string name="fex_performance_label">Prestaties</string> + <string name="fex_performance_detail">Alle TSO uitgeschakeld; multiblock.</string> + <string name="fex_performance_tso_label">Prestaties + TSO</string> + <string name="fex_performance_tso_detail">Prestatie-instellingen met de hoofd-TSO ingeschakeld.</string> + <string name="fex_extreme_label">Extreem</string> + <string name="fex_extreme_detail">Schakelt controles op zelfwijzigende code uit. JIT-games en DRM kunnen stukgaan.</string> + <string name="fex_extreme_tso_label">Extreem + TSO</string> + <string name="fex_extreme_tso_detail">Extreme instellingen met de hoofd-TSO ingeschakeld.</string> + <string name="fex_extreme_gn_label">Extreem-gn</string> + <string name="fex_extreme_gn_detail">Prestaties met TSC- en metadatahints. Probeer dit vóór Extreem.</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">Prestaties met volledige codecontroles en verborgen hypervisorbit.</string> + <string name="game_env_custom_value">Aangepaste waarde</string> + <string name="game_env_select_value">Kies een waarde</string> + <string name="user_apps_section">Emulators & apps</string> + <string name="user_apps_add">Toevoegen</string> + <string name="user_apps_add_detail">Je eigen app of game</string> + <string name="user_apps_kind_script">Script</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d%%</string> + <string name="user_apps_starting">Starten…</string> + <string name="user_apps_added">%1$s toegevoegd</string> + <string name="user_apps_removed">%1$s verwijderd</string> + <string name="user_apps_back">Desktop</string> + <string name="user_apps_gone">Die app bestaat niet meer. Kies hem opnieuw via Desktop.</string> + <string name="user_apps_open">%1$s openen</string> + <string name="user_apps_remove">Verwijderen</string> + <string name="user_apps_remove_confirm">Druk nogmaals om te verwijderen</string> + <string name="user_apps_stop_session">Stop de sessie om te verwijderen</string> + <string name="user_apps_script_linked">Voert %1$s uit vanuit de eigen map, die in de sessie wordt gekoppeld waar hij staat. Als je de app verwijdert, blijft die map ongemoeid.</string> + <string name="user_apps_script_copied">Voert %1$s uit vanuit een kopie van de map in DroidDeck. Als je de app verwijdert, wordt de kopie gewist, niet het origineel.</string> + <string name="user_apps_runtime_required">Installeer eerst de Linux-runtime</string> + <string name="user_apps_file_gone">Het bestand bestaat niet meer</string> + <string name="user_apps_no_space">Niet genoeg vrije ruimte: voor het kopiëren van de map is %1$s nodig</string> + <string name="user_apps_copying">%1$s kopiëren</string> + <string name="user_apps_failed">App toevoegen mislukt</string> + <string name="user_apps_github_checking">Zoeken naar een release van %1$s</string> + <string name="user_apps_github_unreachable">Kan de releases van %1$s niet lezen</string> + <string name="user_apps_github_no_appimage">Geen enkele release van %1$s heeft een ARM64- of x86_64-AppImage</string> + <string name="user_apps_downloading">%1$s downloaden</string> + <string name="user_apps_download_failed">Downloaden mislukt</string> + <string name="user_apps_update_failed">App updaten mislukt</string> + <string name="user_apps_icon_failed">Dat pictogram kan niet worden gelezen</string> + <string name="user_apps_saved">%1$s opgeslagen</string> + <string name="user_apps_edit">Bewerken</string> + <string name="user_apps_check_updates">Controleren op updates</string> + <string name="user_apps_checking_updates">Controleren op updates…</string> + <string name="user_apps_update_to">Updaten naar %1$s</string> + <string name="user_apps_up_to_date">Up-to-date · %1$s</string> + <string name="user_apps_updated">%1$s geüpdatet</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">App toevoegen</string> + <string name="add_app_tab_script">Script</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">Een .sh-bestand. Het draait vanuit de eigen map, op volledig scherm onder gamescope.</string> + <string name="add_app_appimage_hint">Een ARM64-AppImage (aarch64), of een x86_64-versie die via FEX draait. Die wordt eenmalig uitgepakt in de Linux-runtime.</string> + <string name="add_app_github_hint">Installeert de ARM64-AppImage uit de nieuwste release van de repository, of de x86_64-versie via FEX.</string> + <string name="app_fex_title">FEX gebruiken</string> + <string name="app_fex_auto">Automatisch</string> + <string name="app_fex_on">Aan</string> + <string name="app_fex_off">Uit</string> + <string name="app_fex_hint">FEX draait x86-Linux-programma’s op dit ARM64-apparaat. Bij Automatisch wordt FEX gebruikt als het programma voor x86 is gemaakt.</string> + <string name="app_fex_x86">%1$s via FEX</string> + <string name="app_fex_not_ready">Dit programma is gemaakt voor %1$s-pc’s en draait via FEX, dat nog niet is ingesteld. Open Steam één keer terwijl je bent ingelogd: DroidDeck laat het dan FEX en de benodigde Steam Linux Runtime downloaden. Daarna opent dit.</string> + <string name="session_end_program">Dit programma</string> + <string name="session_end_fex_missing">%1$s is gemaakt voor x86-pc’s en draait via FEX, dat nog niet is geïnstalleerd. Start Steam één keer terwijl je bent ingelogd en laat het FEX en de Steam Linux Runtime downloaden. Start dit daarna opnieuw.</string> + <string name="add_app_flatpak_hint">Een Flathub-app-ID of de Flathub-pagina ervan. Flatpak wordt eerst ingesteld als dat nog niet is gebeurd.</string> + <string name="add_app_file">Bestand</string> + <string name="add_app_no_file">Geen bestand gekozen</string> + <string name="add_app_choose_file">Bestand kiezen</string> + <string name="add_app_change_file">Wijzigen</string> + <string name="add_app_pick_script">Kies een script</string> + <string name="add_app_pick_appimage">Kies een AppImage</string> + <string name="add_app_pick_icon">Kies een pictogram</string> + <string name="add_app_repository">Repository</string> + <string name="add_app_repository_placeholder">eigenaar/repo of een github.com-link</string> + <string name="add_app_repository_invalid">Geen GitHub-repository</string> + <string name="add_app_flatpak_id">App-ID</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">Geen Flathub-app-ID</string> + <string name="add_app_flatpak_bundle_hint">Of een .flatpak-bestand, voor een app die niet op Flathub staat.</string> + <string name="add_app_pick_flatpak">Kies een .flatpak-bestand</string> + <string name="add_app_source">Bron</string> + <string name="add_app_appearance">Op je desktop</string> + <string name="add_app_preview_name">Nieuwe app</string> + <string name="add_app_name">Naam</string> + <string name="add_app_name_placeholder">Uit de app</string> + <string name="add_app_icon">Pictogram</string> + <string name="add_app_icon_default">Standaardpictogram</string> + <string name="add_app_choose_icon">Pictogram kiezen</string> + <string name="add_app_clear_icon">Wissen</string> + <string name="add_app_icon_suggestions">Uit de repository</string> + <string name="add_app_icon_searching">Zoeken naar pictogrammen…</string> + <string name="add_app_icon_reset">Standaard</string> + <string name="edit_app_title">%1$s bewerken</string> + <string name="edit_app_save">Opslaan</string> + <string name="add_app_checking_folder">Map controleren…</string> + <string name="add_app_folder_linked">De map wordt in de sessie gekoppeld: %1$s</string> + <string name="add_app_folder_copied">De map bevat Linux-programma’s, die vanaf gedeelde opslag niet kunnen draaien, dus wordt hij naar DroidDeck gekopieerd (%1$s).</string> + <string name="add_app_confirm">Toevoegen</string> + <string name="add_app_cancel">Annuleren</string> + <string name="add_app_runtime_required">Installeer eerst de Linux-runtime via Installatie.</string> + <string name="add_app_busy">Wacht tot %1$s klaar is.</string> + <string name="steam_compat_label">Compatibel</string> + <string name="proton_compatible_note">Builds die in het menu Compatibiliteit van Steam zijn gemarkeerd met “%1$s” hebben de prefixfixes van DroidDeck en starten hier games. Standaard is dat Proton Experimental ARM64 (%1$s), of Proton 11.0 ARM64 (%1$s) zodra die nieuwere build is geïnstalleerd. Valves eigen Proton-items zonder die markering kunnen op dit apparaat geen game starten.</string> + <string name="proton_installed_compatible">%1$s geïnstalleerd · %2$s</string> + <string name="common_cancel">Annuleren</string> + <string name="common_auto">Automatisch</string> + <string name="common_never">Nooit</string> + <string name="common_always">Altijd</string> + <string name="common_uninstall">Verwijderen</string> + <string name="common_check">Controleren</string> + <string name="common_update">Updaten</string> + <string name="common_checking">Controleren…</string> + <string name="common_working">Bezig…</string> + <string name="common_forget">Vergeten</string> + <string name="common_add_ellipsis">Toevoegen…</string> + <string name="common_use">Gebruiken</string> + <string name="common_applies_next_session">Geldt vanaf de volgende sessie.</string> + <string name="mode_steam_title">Steam-sessie</string> + <string name="mode_desktop_title">Desktopsessie</string> + <string name="mode_display">Scherm</string> + <string name="mode_resolution">Resolutie</string> + <string name="mode_res_720">Tot 720p</string> + <string name="mode_res_900">Tot 900p</string> + <string name="mode_res_1080">Tot 1080p</string> + <string name="mode_res_native">Oorspronkelijk</string> + <string name="mode_res_default">%1$s (standaard)</string> + <string name="mode_res_custom_value">Aangepast · %1$d×%2$d</string> + <string name="mode_res_custom">Aangepast…</string> + <string name="mode_res_note">Met 720p kunnen menu’s sneller reageren.</string> + <string name="mode_ratio">Beeldverhouding</string> + <string name="mode_ratio_custom">Ingesteld door de aangepaste resolutie.</string> + <string name="mode_ratio_auto">Automatisch gebruikt minimaal 16:9.</string> + <string name="mode_fps">Framelimiet</string> + <string name="mode_fps_note">Begrenst de hele sessie. Het scherm schakelt naar een verversingssnelheid die een veelvoud van de limiet is.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">HDR10-uitvoer</string> + <string name="mode_hdr_unavailable">Niet beschikbaar: %1$s.</string> + <string name="mode_hdr_restart">Start de app opnieuw om dit toe te passen.</string> + <string name="mode_drivers">Drivers</string> + <string name="mode_gpu_drivers">GPU-drivers</string> + <string name="mode_gpu_drivers_hint">Gedeeld door Steam en Desktop. In te stellen op de pagina Componenten.</string> + <string name="mode_touch_controls">Aanraken & bediening</string> + <string name="mode_touch">Aanraken</string> + <string name="mode_touch_touchpad">Touchpad</string> + <string name="mode_touch_direct">Direct</string> + <string name="mode_touch_note">Automatisch: touchpad op Desktop, direct in Steam.</string> + <string name="mode_osc">Schermbediening</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">Automatisch toont de bediening als er geen controller is verbonden.</string> + <string name="osc_steam_touch">Steam-aanraakbediening (bèta)</string> + <string name="osc_style_next_session">De aanraakbediening van Steam start bij de volgende Steam-sessie</string> + <string name="osc_steam_layout">Steam-aanraakindeling</string> + <string name="osc_steam_layout_hint">Verplaats, schaal of verberg de aanraakbediening van deze game; opgeslagen in Steam</string> + <string name="mode_controller">Controller</string> + <string name="mode_controller_hint">Hoe Steam je controller ziet. Geldt vanaf de volgende sessie.</string> + <string name="mode_controller_deck">Steam Deck-controller</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Xbox 360-controller</string> + <string name="mode_controller_note">Deck: knop Snelle toegang en gyro. Steam Controller: hetzelfde, met gripsensor op de achterknoppen. DualShock 4 en DualSense: gyro, en de trackpads als hun touchpad. Xbox 360: open Snelle toegang met Guide+A.</string> + <string name="mode_back">Terug</string> + <string name="back_menu_then_qam">Menu → QAM</string> + <string name="back_qam_then_menu">QAM → Menu</string> + <string name="mode_session">Sessie</string> + <string name="mode_suspend">Gedrag op de achtergrond</string> + <string name="mode_suspend_hint">Als de app verborgen is of het scherm uitgaat.</string> + <string name="mode_suspend_manual">Handmatig</string> + <string name="mode_suspend_native">Systeemeigen</string> + <string name="mode_suspend_steam_note">Automatisch pauzeert en hervat zodra de app weer zichtbaar is. Systeemeigen maakt Steam eerst klaar voor de slaapstand. Handmatig wacht op Hervatten. Nooit laat alles doorlopen.</string> + <string name="mode_background_downloads">Downloads laten doorlopen</string> + <string name="mode_background_downloads_hint">Stel onderbreken op de achtergrond uit zolang Steam actief downloadt.</string> + <string name="mode_suspend_note">Automatisch pauzeert op de achtergrond en hervat zodra de app weer zichtbaar is. Handmatig wacht op Hervatten. Nooit laat alles doorlopen.</string> + <string name="mode_startup">Opstarten</string> + <string name="mode_steam_startup">Steam starten met DroidDeck</string> + <string name="mode_steam_startup_hint">Opent de Steam-sessie als DroidDeck start.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">Zoeken naar releases…</string> + <string name="mode_decky_ready">Klaar om %1$s te installeren.</string> + <string name="mode_decky_none">Geen compatibele build gevonden. Probeer het later opnieuw.</string> + <string name="mode_decky_installed">Geïnstalleerd · %1$s</string> + <string name="mode_decky_update">Update beschikbaar · %1$s</string> + <string name="mode_decky_current">Up-to-date · %1$s</string> + <string name="mode_decky_loader">Loader</string> + <string name="mode_decky_install_latest">Nieuwste installeren</string> + <string name="mode_decky_on">Start met Steam. Zolang dit aan staat, kunnen andere apps op dit apparaat Steam mogelijk bedienen.</string> + <string name="mode_decky_off">Uit. De poort voor foutopsporing op afstand van Steam blijft dicht.</string> + <string name="mode_decky_remove_title">Decky Loader verwijderen?</string> + <string name="mode_decky_remove_text">Verwijdert de loader. Je plug-ins en instellingen blijven behouden.</string> + <string name="mode_client">Client</string> + <string name="mode_mangoapp">Prestatie-overlay</string> + <string name="mode_mangoapp_hint">Zet dit uit als games crashen of zwart worden in de Deck-modus. Geldt vanaf de volgende sessie.</string> + <string name="mode_steam_repair">Steam-client repareren</string> + <string name="mode_steam_repair_hint">Downloadt de Steam-client opnieuw bij de volgende start van Steam. Gebruik dit als Steam op het laadscherm blijft hangen. Games, login en instellingen blijven behouden.</string> + <string name="mode_steam_repair_queued">De Steam-client wordt opnieuw gedownload bij de volgende start van Steam.</string> + <string name="mode_steam_repair_button">Repareren</string> + <string name="mode_added_art">Afbeeldingen</string> + <string name="mode_sgdb_key">SteamGridDB-API-sleutel</string> + <string name="mode_sgdb_key_yours">Je eigen sleutel</string> + <string name="mode_sgdb_edit">Bewerken…</string> + <string name="mode_added_art_hint">Vult ontbrekende afbeeldingen aan uit Steam en daarna uit SteamGridDB. Je eigen cover.jpg, hero.jpg, logo.png of icon.png in de gamemap heeft voorrang.</string> + <string name="summary_remove_game">%1$s verwijderen</string> + <string name="mode_added_remove_confirm">Verwijderen?</string> + <string name="games_add">Game toevoegen</string> + <string name="games_add_title">Game toevoegen:</string> + <string name="games_pick_exe_title">Kies de exe:</string> + <string name="games_add_unreachable">Deze locatie is niet beschikbaar in de sessie</string> + <string name="mode_added_exe_check">Deze exe controleren</string> + <string name="wincomp_title">Windows-componenten</string> + <string name="wincomp_pad_hint">Met LB / RB spring je tussen de secties.</string> + <string name="wincomp_card_none">Geen</string> + <string name="wincomp_applies">Windows-DLL’s die Proton niet meelevert, uit de componentenlijst. Zet aan wat deze game nodig heeft: ze komen in de eigen Windows-map van de game, nooit tussen de gamebestanden, en blijven staan op welke Proton de game ook draait.</string> + <string name="wincomp_offline">Kan de componentenlijst niet bereiken. Al gedownloade componenten kun je nog steeds aan- en uitzetten.</string> + <string name="wincomp_recommended">Aanbevolen voor deze game</string> + <string name="wincomp_checking">Controleren wat deze game nodig heeft…</string> + <string name="wincomp_when_online">Beschikbaar als je online bent</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">%1$d component aanzetten…</item> + <item quantity="other">%1$d componenten aanzetten…</item> + </plurals> + <string name="wincomp_auto_section">Aan voor deze game</string> + <string name="wincomp_auto_steam">Steam installeert %1$s met %2$s (app %3$s)</string> + <string name="wincomp_auto_gog">Uit de lijst van GOG voor deze game</string> + <string name="wincomp_auto_epic">Uit de vereisten van Epic voor deze game</string> + <string name="wincomp_auto_amazon">Uit de installatiestappen van Amazon voor deze game</string> + <string name="wincomp_steam_by_name">Steam installeert %1$s met app %2$s, gevonden op naam</string> + <string name="wincomp_suggestions">Ook vermeld</string> + <string name="wincomp_suggestion_reason">Vermeld door %1$s; hier geen component</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">De game levert %1$s mee</string> + <string name="wincomp_found_shipped">De game heeft %1$s (werkt vaak zoals het is)</string> + <string name="wincomp_already_here">Al aanwezig in deze game</string> + <string name="wincomp_no_recommendation">Niets in de gamemap wijst op een component.</string> + <string name="wincomp_active">Geïnstalleerd voor deze game (%1$d)</string> + <string name="wincomp_all">Alle componenten (%1$d)</string> + <string name="wincomp_installers">Vereisen een Windows-installatieprogramma (%1$d)</string> + <string name="wincomp_installers_note">Dit zijn eigen installatieprogramma’s zonder pakket dat uit elkaar gehaald kan worden, en er is nog geen opname van wat ze installeren. De meeste zijn oude runtimes waarvoor de Mono in Proton al invalt; opnames worden toegevoegd zodra ze gemaakt zijn.</string> + <string name="wincomp_show">Tonen</string> + <string name="wincomp_hide">Verbergen</string> + <string name="wincomp_needs_installer">Voert een eigen installatieprogramma uit (nog geen opname)</string> + <string name="wincomp_unsupported">Hier nog niet ondersteund</string> + <string name="wincomp_loading">Componenten laden…</string> + <string name="wincomp_failed">Kan niet installeren: %1$s</string> + <string name="wincomp_downloaded">Gedownload</string> + <string name="wincomp_downloading">%1$s downloaden</string> + <string name="wincomp_installing">Installeren: %1$s</string> + <string name="wincomp_reverting">Uitzetten…</string> + <string name="wincomp_download_size">%1$s te downloaden</string> + <string name="wincomp_next_launch">Wordt actief bij de volgende start van de game.</string> + <string name="mode_stretch169">16:9-games uitrekken</string> + <string name="mode_stretch169_hint">Rekt games op schermen die vierkanter zijn dan 16:9 uit tot het hele scherm, in plaats van balken te tonen. Geldt vanaf de volgende sessie.</string> + <string name="mode_audio">Audio</string> + <string name="mode_directaudio">DirectAudio voor games</string> + <string name="mode_directaudio_hint">Omzeilt PulseAudio voor minder vertraging.</string> + <string name="mode_mic">Microfoon</string> + <string name="mode_mic_hint">Voor spraakchat.</string> + <string name="mode_network">Netwerk</string> + <string name="mode_wifi_discovery">Wi-Fi-detectie</string> + <string name="mode_wifi_discovery_hint">Toon Wi-Fi-namen en netwerken in de buurt in Steam.</string> + <string name="mode_wifi_location_off">Locatieservices staan uit.</string> + <string name="mode_wifi_permission_denied">Locatietoestemming niet verleend.</string> + <string name="mode_wifi_open_settings">Instellingen openen</string> + <string name="mode_wifi_continue">Doorgaan</string> + <string name="mode_wifi_explain_title">Wi-Fi-detectie aanzetten?</string> + <string name="mode_wifi_explain_text">Android vereist locatietoestemming om Wi-Fi-namen te lezen en netwerken in de buurt te vinden. DroidDeck gebruikt deze toegang om die netwerken in Steam te tonen. We volgen je locatie niet. Internettoegang werkt ook zonder deze toestemming.</string> + <string name="mode_storage">Gameopslag</string> + <string name="mode_storage_auto">Automatisch - de SD-kaart als die er is, plus je Games-mappen</string> + <string name="mode_storage_auto_none">Automatisch - je Games-mappen, plus de SD-kaart als die er is (nu geen)</string> + <string name="mode_storage_internal">Alleen intern</string> + <string name="mode_storage_folder">Map: %1$s</string> + <string name="mode_second_library">Tweede bibliotheek</string> + <string name="mode_storage_note">Games die assets streamen vanaf SD of gedeelde opslag kunnen haperen.</string> + <string name="mode_storage_diagnostics">Opslagdiagnose</string> + <string name="mode_storage_diagnostics_hint">Legt toewijzingsresultaten en steekproeven van opslagtijden vast in Logs delen. Kan installaties vertragen zolang dit aan staat. Geldt vanaf de volgende sessie.</string> + <string name="mode_choose_folder">Kies een map…</string> + <string name="mode_custom_res_title">Aangepaste resolutie</string> + <string name="mode_custom_res_text">Vervangt de limiet en de beeldverhouding. Een formaat dat niet bij het scherm past, krijgt balken.</string> + <string name="mode_width">Breedte</string> + <string name="mode_height">Hoogte</string> + <string name="mode_custom_res_range">Tussen 320×240 en 3840×2160.</string> + <plurals name="mode_added_count"> + <item quantity="one">%1$d game</item> + <item quantity="other">%1$d games</item> + </plurals> + <string name="common_ok">OK</string> + <string name="common_delete">Verwijderen</string> + <string name="common_restore">Herstellen</string> + <string name="common_download">Downloaden</string> + <string name="common_delete_named">%1$s verwijderen</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Runtimedriver</string> + <string name="comp_runtime_driver_hint">Gebruikt door Steam, games en de desktop. Geldt vanaf de volgende sessie.</string> + <string name="comp_import_turnip">Turnip-zip importeren…</string> + <string name="comp_display_driver">Weergavedriver</string> + <string name="comp_display_driver_hint">Tekent frames op het scherm in beide modi. Start de app opnieuw om dit toe te passen.</string> + <string name="comp_import_adrenotools">AdrenoTools-zip importeren…</string> + <string name="comp_never">nooit</string> + <string name="comp_nightlies_checked">Nightlies gecontroleerd: %1$s</string> + <string name="comp_title">Componenten</string> + <string name="comp_about">Over Componenten</string> + <string name="comp_check_drivers">Zoeken naar nieuwe drivers</string> + <string name="comp_check_nightlies">Nightlies controleren op nieuwe pakketten</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">De hier gekozen Proton draait elke game die in Steam geen eigen Proton heeft gekozen, en een wijziging in Steam zie je hier ook. Steam vervangt Valves Proton bij updates; originelen worden per build bewaard.</string> + <string name="comp_scope">Configuratie</string> + <string name="comp_profile_configure">Profiel configureren</string> + <string name="comp_profile">Profiel</string> + <string name="comp_scope_general">Standaardprofiel</string> + <string name="comp_scope_general_detail">Automatisch gebruikt door games zonder overschrijvingen.</string> + <string name="comp_profile_game_detail">Opgeslagen instellingen die automatisch worden gebruikt als %1$s start.</string> + <string name="comp_game_hint">Kies alleen de instellingen die deze game nodig heeft. Al het andere blijft het Standaardprofiel gebruiken.</string> + <string name="comp_game_fex">FEX-voorinstelling</string> + <string name="comp_game_fex_inherited">Gebruikt de voorinstelling van het Standaardprofiel.</string> + <string name="comp_game_fex_override">Aangepaste voorinstelling opgeslagen voor deze game.</string> + <string name="comp_game_fex_custom">Aangepaste FEX-variabelen</string> + <string name="comp_game_use_general">Standaard gebruiken</string> + <string name="comp_game_compatibility">Gamecompatibiliteit</string> + <string name="comp_game_environment_hint">Omgevingsopties voor Proton, DXVK, VKD3D en synchronisatie voor deze game.</string> + <string name="comp_game_proton_inherited">Gebruikt de Proton van het Standaardprofiel.</string> + <string name="comp_game_proton_override">Aangepaste Proton opgeslagen voor deze game via de compatibiliteitstoewijzing van Steam.</string> + <string name="comp_no_proton_short">Geen Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Gebruikt het pakket van het Standaardprofiel uit %1$s, tenzij deze game een ander pakket kiest.</string> + <string name="comp_game_component_general">Gebruikt het componentpakket van het Standaardprofiel.</string> + <string name="comp_game_component_override">Gebruikt %1$s alleen voor deze game.</string> + <string name="comp_game_runtime_options">Runtime-opties</string> + <string name="comp_game_environment_other_hint">Overige omgevingsopties per game voor Proton, DXVK, VKD3D-Proton en synchronisatie.</string> + <string name="comp_game_saved_next">Het profiel van %1$s is opgeslagen voor de volgende start van de game.</string> + <string name="game_no_steam_title">Starten zonder Steam</string> + <string name="game_no_steam_detail">Voor een game die meteen sluit: start deze zonder Protons Steam-setup. Steam-functies zoals de overlay staan uit voor deze game.</string> + <string name="game_no_steam_on">Aan</string> + <string name="comp_game_proton_live">De Proton-keuze van %1$s is opgeslagen en nu beschikbaar in Steam.</string> + <string name="comp_game_proton_next">De Proton-keuze van %1$s is opgeslagen. Start Steam opnieuw voordat je de game start.</string> + <string name="comp_game_proton_not_runnable">%1$s kan de gekozen Proton op dit apparaat niet gebruiken.</string> + <string name="comp_game_load_failed">Kan het profiel van %1$s niet laden: %2$s</string> + <string name="comp_game_save_failed">Het gameprofiel kan niet worden opgeslagen: %1$s</string> + <string name="comp_game_reset">Gameprofiel resetten</string> + <string name="comp_game_reset_title">Profiel van %1$s resetten?</string> + <string name="comp_game_reset_body">De overschrijvingen voor Proton, FEX, componentpakket, omgeving, textuurfiltering en Windows-componenten van deze game verwijderen? De game gebruikt dan het Standaardprofiel.</string> + <string name="comp_game_reset_done">%1$s gebruikt nu het Standaardprofiel.</string> + <string name="comp_game_reset_failed">Het gameprofiel kan niet volledig worden gereset: %1$s</string> + <string name="comp_default_live">%1$s draait nu elke game die in Steam geen eigen Proton heeft gekozen.</string> + <string name="comp_default_next">%1$s draait elke game die geen eigen Proton heeft gekozen, vanaf de volgende start van Steam.</string> + <string name="comp_default_not_runnable">%1$s kan hier geen games starten en kan dus niet de standaard zijn.</string> + <string name="comp_default_failed">De standaard-Proton kan niet worden opgeslagen: %1$s</string> + <string name="comp_swapped">%1$s · %2$d gewisseld</string> + <string name="comp_all_original">%1$s · alles origineel</string> + <string name="comp_game_running_suffix">%1$s · game actief</string> + <string name="comp_esync_pack">%1$s · droiddeck-esync-pakket</string> + <string name="comp_esync_wanted">%1$s · nog geen droiddeck-esync-pakket</string> + <string name="comp_esync_builtin">%1$s · esync ingebouwd, geen pakket nodig</string> + <string name="comp_gpu_drivers">GPU-drivers</string> + <string name="comp_reapplied">%1$s · opnieuw toegepast bij start %2$s</string> + <string name="comp_next_queued">Volgende: %1$s · nadat de game sluit</string> + <string name="comp_in_use">%1$s in gebruik: %2$s</string> + <string name="comp_waits">Wacht tot de game sluit</string> + <string name="comp_applies_next_game">Geldt bij de volgende start van een game</string> + <string name="comp_import_zip">.zip importeren</string> + <string name="comp_reading">Protons lezen…</string> + <string name="comp_no_proton">Nog geen Proton geïnstalleerd. Start Steam één keer om de ARM64-Proton te downloaden, of voeg GE-Proton, CachyOS of DroidDeck-Proton toe via Installatie.</string> + <string name="comp_original">Origineel · %1$s</string> + <string name="comp_this_build">%1$s · deze build</string> + <string name="comp_earlier_build">%1$s · eerdere build</string> + <string name="comp_installed_count">Geïnstalleerd · %1$d</string> + <string name="comp_nothing_stored">Nog niets bewaard voor %1$s. De eigen bestanden van deze Proton worden als origineel opgeslagen.</string> + <string name="comp_waits_long">Er draait een game op deze Proton, dus dit wacht tot die sluit.</string> + <string name="comp_applies_long">Geldt bij de volgende start van een game.</string> + <string name="comp_swap_title">%1$s wisselen?</string> + <string name="comp_swap_body">%1$s in %2$s zetten? De originele bestanden blijven bewaard. %3$s</string> + <string name="comp_restore_title">%1$s herstellen?</string> + <string name="comp_restore_body">Het origineel van %1$s terugzetten in %2$s? %3$s</string> + <string name="comp_delete_package_body">Verwijdert het pakket uit de app. Je kunt het opnieuw downloaden.</string> + <string name="comp_delete_original_title">Dit origineel verwijderen?</string> + <string name="comp_delete_original_body">Dit origineel van %1$s komt uit een eerdere build van %2$s. Het origineel van de huidige build wordt altijd bewaard.</string> + <string name="comp_nightlies_count">In de Nightlies · %1$d</string> + <string name="comp_refresh_to_list">Vernieuw om de pakketten in de Nightlies te tonen.</string> + <string name="comp_nothing_new">Niets nieuws sinds de laatste controle.</string> + <string name="comp_import_wcp">.wcp importeren</string> + <string name="comp_restore_original">Origineel van deze Proton herstellen</string> + <string name="comp_restore_original_body">Het originele %2$s van %1$s terugzetten?</string> + <string name="comp_swap">Wisselen</string> + <string name="comp_about_text">FEX, DXVK en VKD3D-Proton voor elke Proton waarmee Steam games draait.\n\nKies een geïnstalleerde rij om die in te wisselen. Dit geldt bij de volgende start van een game, of nadat de draaiende game sluit.\n\nJe keuzes worden voor elke start opnieuw gecontroleerd en teruggezet als iets ze heeft gewijzigd. De originele bestanden van elke Proton worden bewaard, zodat Steam-updates ze nooit kwijtraken.</string> + <string name="comp_tag_original">Origineel</string> + <string name="comp_tag_in_use">In gebruik</string> + <string name="comp_tag_new">Nieuw</string> + <string name="comp_tag_stored">Bewaard</string> + <string name="perf_title">Prestaties</string> + <string name="perf_lede">CPU-cores, sessiefixes en de hostnaam. Geldt vanaf de volgende sessie.</string> + <string name="perf_client_cores_group">Cores voor de Steam-client</string> + <string name="perf_override">Corekeuze van Steam overschrijven</string> + <string name="perf_override_hint">Bindt Steam, de UI-helper en gamescope aan de cores hieronder.</string> + <string name="perf_client_cores">Clientcores</string> + <string name="perf_client_cores_on">Cores voor Steam.</string> + <string name="perf_client_cores_off">Zet de overschrijving aan om te kiezen.</string> + <string name="perf_game_cores_group">Gamecores</string> + <string name="perf_game_cores">Gamecores</string> + <string name="perf_game_cores_hint">Cores voor games. Met alles geselecteerd verdeelt Android vrij.</string> + <string name="perf_client_ui">Clientinterface</string> + <string name="perf_glthread">GL-threading</string> + <string name="perf_glthread_hint">Kan Steam-menu’s sneller laten reageren.</string> + <string name="perf_zink">Zink: luie descriptors</string> + <string name="perf_zink_hint">Aanbevolen voor drivers zonder descriptorbuffers.</string> + <string name="perf_noglerror">GL-foutcontroles overslaan</string> + <string name="perf_noglerror_hint">Schakelt GL-validatie per aanroep uit.</string> + <string name="perf_gsrealtime">gamescope: realtime GPU-wachtrij</string> + <string name="perf_gsrealtime_hint">Geeft het GPU-werk van de compositor voorrang. Kan de framepacing gladder maken, ten koste van wat gameprestaties.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">GPU op hoogste kloksnelheid houden</string> + <string name="perf_gpuclock_hint">Alleen Adreno. Minder haperingen, meer batterijverbruik en warmte.</string> + <string name="perf_fixes">Sessiefixes</string> + <string name="perf_sysmem">Turnip: sysmem-rendering</string> + <string name="perf_sysmem_hint">Sneller voor Steam en de meeste games. Vereist op Adreno 710/720/722.</string> + <string name="perf_xalia">Sla de xalia-helper van Steam over</string> + <string name="perf_fastsync">droiddeck-ntsync (experimenteel)</string> + <string name="perf_fastsync_hint">Een userspace-vervanger voor de ntsync-driver van Linux, voor tests. Terwijl dit aan staat, gebruikt Proton dit in plaats van droiddeck-esync en droiddeck-fsync. Werkt met Proton Experimental ARM64, GE-Proton en proton-cachyos. Geldt vanaf de volgende sessie.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync eerst</string> + <string name="perf_fsync_hint">Draait de eigen fsync van Proton op de standaard Proton-bestanden, met futex_waitv in userspace geleverd, zodat er geen pakket nodig is en dit blijft werken zodra Steam Proton bijwerkt. Terwijl dit aan staat, gebruiken games dit vóór droiddeck-esync, behalve de paar die Proton niet op fsync laat draaien. Geldt vanaf de volgende sessie.</string> + <string name="perf_esync_hint">De Windows-threads van games wachten via eventfd en gedeeld geheugen op elkaar, in plaats van heen-en-weer via wineserver. Gebruikt een droiddeck-esync-pakket dat voor die exacte Proton-build is gemaakt; pakketten worden automatisch gehaald. Totdat er een pakket is voor een nieuwe Proton-build, springt droiddeck-fsync in. Geldt vanaf de volgende sessie.</string> + <string name="perf_xalia_hint">Schakelt de gamepadnavigatiehulp van Proton uit, die in elke game CPU kost.</string> + <string name="perf_seccomp">Draai proot zonder seccomp</string> + <string name="perf_seccomp_hint">Kan foutmeldingen over ontbrekende systeemaanroepen op sommige kernels verhelpen. Kan de prestaties verlagen.</string> + <string name="perf_fastpath">proot-snelpad</string> + <string name="perf_fastpath_needs_seccomp">Vereist het seccomp-filter van proot.</string> + <string name="perf_fastpath_hint">Snellere bestandstoegang en minder stotteren. Zet dit uit als een programma zijn bestanden niet kan vinden.</string> + <string name="perf_identity">Sessie-identiteit</string> + <string name="perf_hostname">Hostnaam</string> + <string name="perf_hostname_hint">De naam die Steam voor dit apparaat ziet. Leeg herstelt %1$s.</string> + <string name="perf_hostname_invalid">Alleen letters, cijfers en koppeltekens binnen de naam, tot 63 tekens.</string> + <string name="perf_phantom_warning">Android is ingesteld om deze sessie te sluiten</string> + <string name="driver_installed">Geïnstalleerd</string> + <string name="driver_available">Beschikbaar om te downloaden</string> + <string name="driver_nothing_new">Niets nieuws sinds de laatste controle.</string> + <string name="driver_restore_builtin">Ingebouwde drivers herstellen</string> + <string name="driver_delete_builtin">Ingebouwd in de app: dit verwijdert de uitgepakte bestanden en verbergt de driver. Met Ingebouwde drivers herstellen komt hij terug.</string> + <string name="driver_delete_bundle">Een helft van een Android + Linux-bundel: beide helften worden verwijderd. Een driver die in gebruik is, valt terug op de standaard.</string> + <string name="driver_delete_plain">De bestanden worden uit de app verwijderd.</string> + <string name="driver_delete_in_use">De bestanden worden verwijderd. Hij is in gebruik, dus de standaard neemt het over.</string> + <string name="drawer_page_display">Scherm</string> + <string name="drawer_page_effects">Effecten</string> + <string name="drawer_page_controls">Bediening</string> + <string name="drawer_page_components">Componenten</string> + <string name="drawer_page_controller">Controller</string> + <string name="drawer_page_settings">Instellingen</string> + <string name="drawer_page_desc">%1$s-pagina</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Desktop</string> + <string name="drawer_steam_menu">Steam-menu</string> + <string name="drawer_quick_access">Snelmenu</string> + <string name="drawer_open_qam">Snelmenu openen</string> + <string name="drawer_prev_page">Vorige pagina</string> + <string name="drawer_next_page">Volgende pagina</string> + <string name="drawer_hud">Prestatie-HUD</string> + <string name="drawer_scaling">Schaalmodus</string> + <string name="drawer_scaling_sharpness">Schaalscherpte</string> + <string name="drawer_effects">Schermeffecten</string> + <string name="drawer_effects_hint">Live. Kost wat GPU-tijd zolang er een effect aan staat.</string> + <string name="drawer_look">Voorinstellingen</string> + <string name="drawer_look_custom">Aangepast</string> + <string name="drawer_effects_advanced">Geavanceerd</string> + <string name="widgets_expanded">Uitgeklapt</string> + <string name="widgets_collapsed">Ingeklapt</string> + <string name="drawer_cas">Verscherping (CAS)</string> + <string name="drawer_cas_level">Verscherpingsniveau</string> + <string name="drawer_fake_hdr">Nep-HDR</string> + <string name="drawer_deband">Debanding</string> + <string name="drawer_deband_strength">Debanding-sterkte</string> + <string name="drawer_brightness">Helderheid</string> + <string name="drawer_contrast">Contrast</string> + <string name="drawer_gamma">Gamma</string> + <string name="drawer_saturation">Verzadiging</string> + <string name="drawer_fxaa">FXAA (vloeiende randen)</string> + <string name="drawer_toon">Toon-contouren</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Textuurfiltering</string> + <string name="drawer_texture_hint">DirectX 9–11-games, vanaf hun volgende start.</string> + <string name="drawer_anisotropy">Anisotrope filtering</string> + <string name="drawer_texture_sharpness">Textuurscherpte</string> + <string name="drawer_texture_sharpness_note">Een negatieve mip-bias: scherpere texturen, met wat shimmer. Automatisch volgt hoe ver de sessie wordt uitvergroot naar het scherm.</string> + <string name="drawer_touch_auto">Automatisch (%1$s)</string> + <string name="drawer_keyboard">Toetsenbord</string> + <string name="drawer_pc_keyboard">Pc-toetsenbord</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">Tweede scherm</string> + <string name="drawer_second_screen_mode">Modus</string> + <string name="drawer_second_screen_shows">Toont</string> + <string name="drawer_android_apps">Android-apps</string> + <string name="drawer_launch_app">Een app starten</string> + <string name="common_hide">Verbergen</string> + <string name="common_show">Tonen</string> + <string name="drawer_no_apps">Geen apps om te starten</string> + <string name="drawer_session_behavior">Sessiegedrag</string> + <string name="drawer_suspend_hint">Geldt nu en voor toekomstige sessies in deze modus.</string> + <string name="drawer_next_session">Volgende sessie</string> + <string name="drawer_support">Ondersteuning</string> + <string name="drawer_logs">Sessielogs</string> + <string name="drawer_logs_hint">Voeg bij een bugmelding</string> + <string name="drawer_share_logs">Logs delen</string> + <string name="drawer_background">Achtergrond</string> + <string name="drawer_stop">Stoppen</string> + <string name="drawer_stop_title">Sessie stoppen?</string> + <string name="drawer_running_now">Nu actief</string> + <string name="drawer_no_game">Er draait geen game op een Proton</string> + <string name="drawer_selected_profile">Gameprofiel</string> + <string name="drawer_selected_profile_none">Profielgegevens zijn nog niet beschikbaar.</string> + <string name="drawer_profile_refresh_failed_initial">Kan gameprofielen niet laden. Wordt automatisch opnieuw geprobeerd.</string> + <string name="drawer_profile_refresh_failed_stale">Kan gameprofielen niet vernieuwen. Toont de laatst beschikbare gegevens; wordt automatisch opnieuw geprobeerd.</string> + <string name="drawer_profile_inspect_hint">Bekijk en bewerk de opgeslagen instellingen voor de volgende start.</string> + <string name="drawer_profile_default">Gebruikt het standaardprofiel</string> + <string name="drawer_profile_custom">Aangepast profiel</string> + <string name="drawer_component_inherited">Gebruikt %1$s van het standaardprofiel.</string> + <string name="drawer_game_components_note">Wijzigingen worden opgeslagen in het profiel van %1$s en gelden vanaf de volgende keer dat de game start.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d game-override</item> + <item quantity="other">%1$d game-overrides</item> + </plurals> + <string name="drawer_no_proton">Er is nog geen Proton geïnstalleerd.</string> + <string name="drawer_game_running">game actief</string> + <string name="drawer_reapplied">opnieuw toegepast bij start %1$s</string> + <string name="drawer_original">Origineel</string> + <string name="drawer_components_note">Importeren en verwijderen op de pagina Componenten.</string> + <string name="setup_check_done">Klaar</string> + <string name="setup_check_warn">Aandacht nodig</string> + <string name="setup_check_busy">Bezig</string> + <string name="setup_limit_adb_failed">Controleer het IP-adres en de poort van Draadloos debuggen, of koppel opnieuw als Android dit apparaat heeft verwijderd.</string> + <string name="setup_install">Installeren</string> + <string name="setup_manage">Beheren</string> + <string name="setup_tab_overview">Overzicht</string> + <string name="setup_tab_controller">Controller</string> + <string name="setup_tab_session">Sessie</string> + <string name="setup_tab_launcher">Launcher</string> + <string name="setup_title">Installatie</string> + <string name="setup_all_set">● Alles klaar</string> + <string name="setup_n_ready">%1$d van %2$d gereed</string> + <string name="setup_system_check">Systeemcontrole</string> + <string name="setup_gpu_supported">Apparaat ondersteund</string> + <string name="setup_gpu_untested">Ongeteste GPU</string> + <string name="setup_gpu_unsupported">GPU niet ondersteund</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">Steam heeft een Adreno-driver nodig; %1$s kan een zwart scherm tonen</string> + <string name="setup_runtime">Linux-runtime</string> + <string name="setup_runtime_progress">%1$s · %2$d%%</string> + <string name="setup_runtime_missing">Niet geïnstalleerd · ongeveer 3 GB, geïnstalleerd bij de eerste keer Spelen</string> + <string name="setup_installed">Geïnstalleerd</string> + <string name="setup_runtime_update">%1$s · update beschikbaar</string> + <string name="setup_runtime_current">%1$s · up-to-date</string> + <string name="setup_limit">Limiet voor subprocessen</string> + <string name="setup_limit_on">Aan · Android kan Steam sluiten. Uitzetten duurt een minuutje.</string> + <string name="setup_limit_unset">Niet ingesteld · Android kan Steam sluiten. Uitzetten duurt een minuutje.</string> + <string name="setup_limit_unknown">Kan niet worden gecontroleerd · uitzetten duurt een minuutje.</string> + <string name="setup_fix_it">Oplossen</string> + <string name="setup_details">Details</string> + <string name="setup_dev_options">Opties voor ontwikkelaars</string> + <string name="setup_use_wireless">Draadloos debuggen gebruiken</string> + <string name="setup_check_again">Opnieuw controleren</string> + <string name="setup_limit_turn_on">Limiet inschakelen</string> + <string name="setup_account">Steam-account</string> + <string name="setup_signed_in_offline">Aangemeld als %1$s · offlinemodus</string> + <string name="setup_signed_in">Aangemeld als %1$s</string> + <string name="setup_sign_in">Druk op Spelen en meld je aan bij Steam</string> + <string name="setup_tools">Hulpmiddelen</string> + <string name="setup_controller">Controller</string> + <string name="setup_controller_unavailable">Controllerinstellingen zijn niet beschikbaar.</string> + <string name="setup_session">Sessie</string> + <string name="setup_logs">Sessielogs</string> + <string name="setup_logs_hint">Opgeslagen na elke sessie</string> + <string name="setup_agent_commands">Hulpmiddelen voor debuggen</string> + <string name="setup_latest_logs">Nieuwste sessielogs</string> + <string name="setup_saved_logs">Opgeslagen sessielogs</string> + <string name="setup_saved_logs_hint">Bewaart de nieuwste %1$d</string> + <string name="setup_clear_logs">Logs wissen</string> + <string name="setup_offline">Offlinemodus</string> + <string name="setup_sign_in_first">Meld je eerst aan bij Steam</string> + <string name="setup_launcher">Launcher</string> + <string name="setup_language">Taal</string> + <string name="setup_language_hint">Steam gebruikt deze ook, vanaf de volgende keer dat het start.</string> + <string name="setup_language_system">Systeemstandaard (%1$s)</string> + <string name="setup_language_system_short">Systeemstandaard</string> + <string name="setup_app_scale">Appschaal</string> + <string name="setup_app_scale_hint">Tekst en bediening van DroidDeck</string> + <string name="setup_app_scale_default">%1$d%% (Standaard)</string> + <string name="setup_theme">Thema</string> + <string name="setup_theme_hint">Uiterlijk van de launcher</string> + <string name="setup_home">Als startscherm gebruiken</string> + <string name="setup_home_on">DroidDeck kan de startschermapp zijn</string> + <string name="setup_home_off">DroidDeck wordt nooit aangeboden als startschermapp</string> + <string name="setup_orientation">Oriëntatie</string> + <string name="setup_orientation_auto">Automatisch liggend</string> + <string name="setup_orientation_landscape">Liggend</string> + <string name="setup_orientation_reverse">Liggend omgekeerd</string> + <string name="setup_fullscreen">Volledig scherm</string> + <string name="setup_fullscreen_on">Verbergt de statusbalk en navigatiebalk van Android</string> + <string name="setup_fullscreen_off">Toont de statusbalk en navigatiebalk van Android</string> + <string name="setup_default_home">Standaard startschermapp</string> + <string name="setup_choose_home">Kies een startschermapp</string> + <string name="setup_choose">Kiezen</string> + <string name="setup_linux_apps">Linux-apps (bèta)</string> + <string name="setup_tool_files">Bestanden</string> + <string name="setup_tool_files_hint">Bestanden doorbladeren en beheren</string> + <string name="setup_tool_protons">Proton-versies</string> + <string name="setup_tool_protons_hint">ARM64 Proton-builds installeren</string> + <string name="setup_tool_performance">Prestaties</string> + <string name="setup_tool_performance_hint">Toewijzing van CPU-kernen</string> + <string name="setup_tool_roms">ROM-map</string> + <string name="setup_tool_roms_hint">Waar emulatorgames worden opgeslagen</string> + <string name="setup_card_components">Componenten</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Bediening</string> + <string name="setup_card_controls_hint">Knoppenindeling</string> + <string name="store_tab_discover">Ontdekken</string> + <string name="store_tab_search">Zoeken</string> + <string name="store_tab_installed">Geïnstalleerd</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Store</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak gereed</string> + <string name="store_needs_runtime">Installeer eerst de Linux-runtime via Installatie.</string> + <string name="store_setup_title">Flatpak instellen</string> + <string name="store_setup_text">Voegt Flatpak toe en verbindt het met Flathub (ongeveer 20 MB). Bij de eerste app wordt ook de bijbehorende runtime gedownload.</string> + <string name="store_setting_up">Instellen…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Updates</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d%%</string> + <string name="store_starting">Starten…</string> + <string name="store_categories">Categorieën</string> + <string name="store_unreachable">Kan Flathub niet bereiken. Controleer je verbinding en probeer het opnieuw.</string> + <string name="store_try_again">Opnieuw proberen</string> + <string name="store_search_label">Flathub doorzoeken</string> + <string name="store_search_placeholder">Firefox, games, emulators…</string> + <string name="store_search">Zoeken</string> + <string name="store_all">Alle</string> + <string name="store_searching">Zoeken…</string> + <string name="store_search_hint">Zoek op naam of kies een categorie. Alleen ARM64-apps worden weergegeven.</string> + <string name="store_no_results">Geen ARM64-apps op Flathub komen daarmee overeen.</string> + <string name="store_results">Resultaten</string> + <string name="store_check_updates">Controleren op updates</string> + <string name="store_update_all">Alles updaten (%1$d)</string> + <string name="store_all_current">● Alles is up-to-date</string> + <string name="store_no_apps">Nog geen apps. Zoek ze onder Ontdekken of Zoeken.</string> + <string name="store_setup_first_note">Stel Flatpak in om apps te installeren.</string> + <string name="store_update_available_prefix">Update beschikbaar · %1$s</string> + <string name="store_open">Openen</string> + <string name="store_details">Details</string> + <string name="store_remove">Verwijderen</string> + <string name="store_press_again">Druk nogmaals om te verwijderen</string> + <string name="store_installing">Installeren…</string> + <string name="store_install">Installeren</string> + <string name="store_setup_first">Stel eerst Flatpak in</string> + <string name="store_download_size">%1$s download</string> + <string name="store_installed_chip">● Geïnstalleerd</string> + <string name="store_details_failed">Kan de Flathub-pagina van deze app niet laden.</string> + <string name="store_loading">Laden vanuit Flathub…</string> + <string name="store_no_arm">Geen ARM64-build op Flathub, dus kan dit hier niet worden geïnstalleerd.</string> + <string name="store_screenshots">Screenshots</string> + <string name="store_about">Over</string> + <string name="store_app_id">App-ID</string> + <string name="store_license">Licentie</string> + <string name="store_runtime">Runtime</string> + <string name="store_installed_size">Geïnstalleerde grootte</string> + <string name="store_installed_size_value">%1$s (plus de gedeelde runtime)</string> + <string name="store_website">Website</string> + <string name="store_verified">Geverifieerd</string> + <string name="upd_title">Updates</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Gecontroleerd %1$s</string> + <string name="upd_not_checked">Nog niet gecontroleerd</string> + <string name="upd_perm_title">Laat DroidDeck zichzelf updaten</string> + <string name="upd_perm_text">Op het volgende scherm zet je Van deze bron toestaan aan, en kom dan terug. De update gaat daarna automatisch verder.</string> + <string name="upd_open_settings">Instellingen openen</string> + <string name="upd_not_now">Niet nu</string> + <string name="upd_signed_status">Anders ondertekend</string> + <string name="upd_signed_headline">Kan niet rechtstreeks worden bijgewerkt</string> + <string name="upd_signed_detail">Deze kopie is niet ondertekend met de releasesleutel van DroidDeck, dus updates kunnen er niet overheen worden geïnstalleerd. Verwijder de app om te wisselen.</string> + <string name="upd_available">Update beschikbaar</string> + <string name="upd_blocked_status">Installeren niet mogelijk</string> + <string name="upd_switch">Klaar om te wisselen</string> + <string name="upd_ahead_status">Voor op Stabiel</string> + <string name="upd_ahead_headline">Je loopt voor op Stabiel</string> + <string name="upd_ahead_detail">Deze build is nieuwer dan de nieuwste Stabiele build. Je gaat naar Stabiel zodra de volgende release daarvan verschijnt.</string> + <string name="upd_gone_status">Test afgelopen</string> + <string name="upd_gone_headline">Deze test is afgelopen</string> + <string name="upd_gone_detail">De bijbehorende fix is samengevoegd of geschrapt. Volg Preview voor de nieuwste fixes.</string> + <string name="upd_current_status">Up-to-date</string> + <string name="upd_current_headline">Je hebt de nieuwste %1$s</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Updaten%1$s</string> + <string name="upd_install_button">Installeren%1$s</string> + <string name="upd_install_anyway">Stabiel %1$s toch installeren</string> + <string name="upd_follow_preview">Preview volgen</string> + <string name="upd_stop_session">Stop de actieve sessie om te updaten.</string> + <string name="upd_less">Minder</string> + <string name="upd_more">Meer</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Nieuwe Preview-build</string> + <string name="upd_new_test">PR #%1$d-testbuild</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">UPDATEKANAAL</string> + <string name="upd_stable">Stabiel</string> + <string name="upd_stable_hint">Geteste releases, voor de meeste mensen</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Preview</string> + <string name="upd_preview_hint">Nieuwste fixes, vóór Stabiel</string> + <string name="upd_preview_current">Up-to-date</string> + <string name="upd_tests">Testbuilds</string> + <string name="upd_tests_hint">Probeer een fix voordat die wordt uitgebracht</string> + <string name="upd_tests_none">Momenteel geen</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Niet gebouwd voor deze kopie van DroidDeck</string> + <string name="upd_updated">Geüpdatet %1$s</string> + <string name="upd_test_name">de PR #%1$d-test</string> + <string name="upd_while_ago">een tijdje geleden</string> + <string name="upd_just_now">zojuist</string> + <plurals name="upd_tests_count"> + <item quantity="one">%1$d om te proberen</item> + <item quantity="other">%1$d om te proberen</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="one">Wijzigingen sinds jouw build · %1$d build achter</item> + <item quantity="other">Wijzigingen sinds jouw build · %1$d builds achter</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="one">Recente wijzigingen · %1$d build</item> + <item quantity="other">Recente wijzigingen · %1$d builds</item> + </plurals> + <string name="gate_title">Eén Android-instelling om te wijzigen</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Android stopt apps die veel achtergrondprocessen starten, en Steam start er tientallen. Totdat dit verandert, sluiten games zonder foutmelding.</string> + <string name="gate_fix_for_me">Voor mij oplossen</string> + <string name="gate_toggle_steps">Zet “Beperkingen voor subprocessen uitschakelen” aan, en kom dan terug. Laat Opties voor ontwikkelaars aan staan, anders schakelt de schakelaar weer uit.</string> + <string name="gate_open_dev">Opties voor ontwikkelaars openen</string> + <string name="gate_no_toggle">Deze Android-versie heeft hiervoor geen schakelaar, dus wijzigt DroidDeck dit via Draadloos debuggen. Duurt ongeveer een minuut, geen computer nodig.</string> + <string name="gate_needs_wifi">Draadloos debuggen heeft Wi-Fi nodig. Elk netwerk werkt; internet is niet vereist.</string> + <string name="gate_needs_notifications">De koppelingscode komt in een melding. Sta meldingen toe voor DroidDeck, of gebruik Andere manieren.</string> + <string name="gate_allow_notifications">Meldingen toestaan</string> + <string name="gate_step1">Open Instellingen → Opties voor ontwikkelaars → Draadloos debuggen en zet het aan</string> + <string name="gate_step2">Tik op “Apparaat koppelen met koppelingscode”</string> + <string name="gate_step3">Voer de code in bij de melding van DroidDeck</string> + <string name="gate_waiting">Wachten op de koppelingspop-up…</string> + <string name="gate_code_needed">Pop-up gevonden. Voer de code ervan in bij de melding.</string> + <string name="gate_status">Status</string> + <string name="gate_polling">Controleert elke 2 seconden. Deze pagina sluit zodra het uit staat.</string> + <string name="gate_other_ways">Andere manieren</string> + <string name="gate_other_toggle">Draadloos debuggen, handmatig adres, computer-ADB</string> + <string name="gate_other_no_toggle">Handmatig adres, computer-ADB, Opties voor ontwikkelaars</string> + <string name="gate_use_wireless_instead">Gebruik Draadloos debuggen in plaats daarvan</string> + <string name="gate_some_roms">Sommige ROMs voegen de schakelaar toch toe. Zoek naar “subproces” in Opties voor ontwikkelaars.</string> + <string name="gate_manual_hint">Als de melding niet werkt, voer je het adres en de code van de pop-up zelf in.</string> + <string name="gate_enter_manually">Adres handmatig invoeren</string> + <string name="gate_from_computer">Vanaf een computer met ADB:</string> + <string name="gate_copy_command">Opdracht kopiëren</string> + <string name="gate_choose_display">Kies op welk scherm Android-instellingen wordt geopend.</string> + <string name="gate_main_screen">Hoofdscherm</string> + <string name="wadb_finding_port">Gekoppeld. Poort van Draadloos debuggen zoeken…</string> + <string name="wadb_enter_port">Gekoppeld. Voer de poort uit Instellingen in.</string> + <string name="wadb_applying">Apparaat gevonden. Instelling toepassen…</string> + <string name="wadb_apply_failed">Kan niet automatisch worden toegepast. Controleer het adres hieronder en probeer het opnieuw. %1$s</string> + <string name="wadb_title_wireless">Draadloos debuggen</string> + <string name="wadb_title_done">Limiet voor subprocessen bijgewerkt</string> + <string name="wadb_title">De limiet voor subprocessen wijzigen</string> + <string name="wadb_lede_compact0">Als de optie ontbreekt, koppel dan via Draadloos debuggen.</string> + <string name="wadb_lede_compact1">Gekoppeld. Controleer het IP-adres en de poort in Draadloos debuggen.</string> + <string name="wadb_lede_done">De limiet voor subprocessen is bijgewerkt.</string> + <string name="wadb_lede0">Gebruik stap 2 als Opties voor ontwikkelaars geen instelling voor subprocessen heeft. Open Opties voor ontwikkelaars → Draadloos debuggen → Apparaat koppelen met koppelingscode.</string> + <string name="wadb_lede1">Gekoppeld. Gebruik het adres hieronder, of werk het bij via Draadloos debuggen → IP-adres & Poort.</string> + <string name="wadb_lede_done_long">De limiet voor subprocessen is bijgewerkt via Draadloos debuggen.</string> + <string name="wadb_confirmed_on">Android heeft bevestigd dat de limiet aan staat.</string> + <string name="wadb_confirmed_off">Android heeft bevestigd dat de limiet uit staat.</string> + <string name="wadb_pair_group">2 · Draadloos debuggen koppelen</string> + <string name="wadb_pair_address">IP-adres & poort van de koppelingspop-up</string> + <string name="wadb_pin">Koppelingscode (pincode)</string> + <string name="wadb_pin_placeholder">6 cijfers</string> + <string name="wadb_pair">Koppelen</string> + <string name="wadb_apply_group">2 · Toepassen via Draadloos debuggen</string> + <string name="wadb_connect_address">IP-adres & poort van de verbinding</string> + <string name="wadb_pair_again">Opnieuw koppelen</string> + <string name="wadb_turn_off">Limiet uitschakelen</string> + <string name="wadb_other_ways">Andere manieren om dit te wijzigen</string> + <string name="wadb_dev_try_first">1 · Opties voor ontwikkelaars (probeer dit eerst)</string> + <string name="wadb_restrict_on">Zet “Subprocessen beperken” aan.</string> + <string name="wadb_restrict_off">Zet “Subprocessen beperken” uit.</string> + <string name="wadb_computer_last">3 · Computer-ADB (laatste redmiddel)</string> + <string name="wadb_only_if">Alleen als Draadloos debuggen niet beschikbaar is.</string> + <string name="wadb_computer_group">3 · Computer-ADB (laatste redmiddel)</string> + <string name="ctrl_osc_hint">Wanneer de aanraakbediening in een sessie verschijnt</string> + <string name="ctrl_color">Kleur</string> + <string name="ctrl_color_hint">Tint van de knoppen op het scherm</string> + <string name="ctrl_custom">Aangepast</string> + <string name="ctrl_opacity">Transparantie</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">Knopgrootte</string> + <string name="ctrl_size_hint" formatted="false">100% is de standaardgrootte</string> + <string name="ctrl_stick_click">Stickklik</string> + <string name="ctrl_stick_click_hint">Dubbeltik en houd een stick ingedrukt voor L3 of R3</string> + <string name="ctrl_adaptive">Adaptieve sticks</string> + <string name="ctrl_rumble">Trillen in games</string> + <string name="ctrl_steam_button">Steam-knop</string> + <string name="ctrl_qam_button">QAM-knop</string> + <string name="ctrl_keyboard_button">Toetsenbordknop</string> + <string name="ctrl_select_steam">Druk op Select om het Steam-menu te openen</string> + <string name="ctrl_select_steam_hint">In plaats van View/Select.</string> + <string name="setup_animations">Animaties</string> + <string name="ctrl_adaptive_hint">Sticks verschijnen waar je aanraakt en verdwijnen als je loslaat</string> + <string name="ctrl_layout">Indeling</string> + <string name="ctrl_layout_custom">Aangepaste posities opgeslagen</string> + <string name="ctrl_layout_default">Aangepast aan dit scherm</string> + <string name="ctrl_edit">Bewerken</string> + <string name="ctrl_reset">Resetten</string> + <string name="ctrl_mapping">Knoppenindeling</string> + <string name="ctrl_mapping_default">Elke knop stuurt zijn eigen invoer</string> + <string name="ctrl_mapping_count">%1$d van %2$d knoppen opnieuw toegewezen</string> + <string name="ctrl_configure">Configureren</string> + <string name="ctrl_reset_all">Controller resetten</string> + <string name="ctrl_reset_all_hint">Herstelt alle controllerinstellingen en de indeling</string> + <string name="ctrl_mapping_lede">Wat elke schermknop naar de game stuurt. Verborgen verwijdert de knop.</string> + <string name="ctrl_onscreen_buttons">Schermknoppen</string> + <string name="ctrl_defaults">Standaardinstellingen</string> + <string name="ctrl_reset_mapping">Indeling resetten</string> + <string name="content_android_apps">Android-apps</string> + <string name="content_apps_count">%1$d apps</string> + <string name="content_no_android_apps">Geen startbare Android-apps gevonden.</string> + <string name="content_desktop_installed">● Desktop geïnstalleerd</string> + <string name="content_desktop_first_open">Wordt geïnstalleerd bij eerste keer openen</string> + <string name="content_available">Beschikbaar om te installeren</string> + <string name="content_not_installed">Niet geïnstalleerd.</string> + <string name="content_not_installed_suffix">%1$s · niet geïnstalleerd</string> + <string name="content_open_named">%1$s openen</string> + <string name="content_games">Games</string> + <string name="content_choose_roms">Kies een ROM-map.</string> + <string name="content_retroarch_roms">Blader naar /root/ROMs in RetroArch.</string> + <string name="content_add_roms">Voeg %1$s-games toe aan ROMs/%2$s.</string> + <string name="content_install_named">%1$s installeren</string> + <string name="content_stop_to_install">Stop de sessie om te installeren</string> + <string name="content_runtime_required">Runtime vereist</string> + <string name="content_loading_details">Installatiegegevens laden…</string> + <string name="content_details_unavailable">Installatiegegevens zijn niet beschikbaar. Probeer het later opnieuw.</string> + <string name="content_needs_runtime">Installeer eerst de Linux-runtime via Installatie.</string> + <string name="content_stop_first">Stop de actieve sessie om desktopapps te installeren.</string> + <string name="content_install_into">Installeert %1$s in de Linux-desktop.</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">Verbergen haalt het van Desktop. De bestanden blijven in de Linux-runtime.</string> + <string name="content_rom_gone">Die game staat niet meer in de ROM-map.</string> + <string name="content_desktop_system">Desktop · %1$s</string> + <string name="content_launch_in">Starten in %1$s</string> + <string name="content_also_in">Ook in %1$s</string> + <string name="content_page_gone">Die pagina bestaat niet meer. Kies een onderdeel aan de linkerkant.</string> + <string name="content_open_desktop">Desktop openen</string> + <string name="content_install_open_desktop">Installeren & desktop openen</string> + <string name="desktop_remove_title">Desktop verwijderen</string> + <string name="desktop_remove_text">Verwijdert de desktop en de meegeleverde apps. Games, toegevoegde apps, instellingen en gedeelde bibliotheken blijven behouden.</string> + <string name="desktop_removing">Desktop wordt verwijderd</string> + <string name="desktop_removed">Desktop verwijderd</string> + <string name="desktop_cleaning_legacy">Oude LXQt-bestanden verwijderen</string> + <string name="desktop_removal_unknown">Werk DroidDeck bij voordat je deze desktopversie verwijdert.</string> + <string name="content_linux_desktop">Linux-desktop</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox en je emulators</string> + <string name="games_empty">Games die je in Steam installeert, verschijnen hier na de sessie.</string> + <string name="games_play_steam">Steam spelen</string> + <string name="games_launch_settings">Opstartinstellingen</string> + <string name="games_launch">Starten</string> + <string name="games_files">Gamebestanden</string> + <string name="games_prefix">Proton-prefix</string> + <string name="games_saves_looking">Opslagbestanden zoeken…</string> + <string name="games_saves_none">Geen opslagmap gevonden op naam. Exporteren neemt de hele gebruikersmap.</string> + <string name="games_saves_folders">%1$d opslagmappen</string> + <string name="games_saves_summary">%1$s · %2$d bestanden · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">Opslagbestanden beheren</string> + <string name="games_saves">Gameopslag</string> + <string name="games_import_saves">Opslagbestanden importeren…</string> + <string name="games_export_gamehub">Exporteren als GameHub-zip…</string> + <string name="games_export_winlator">Exporteren als Winlator-zip…</string> + <string name="games_open_save_folder">Opslagmap openen</string> + <string name="games_never_played">Nooit gespeeld</string> + <string name="games_runtime_busy_percent">Runtime bezig · %1$d%%</string> + <string name="games_runtime_busy">Runtime bezig</string> + <string name="games_runtime_first_play">Runtime wordt geïnstalleerd bij de eerste keer Spelen</string> + <string name="games_runtime_update">Runtime-update beschikbaar</string> + <string name="games_runtime_ready">● Runtime gereed</string> + <string name="games_last_played">Laatst gespeeld %1$s</string> + <string name="games_internal">Interne opslag</string> + <string name="games_added">Toegevoegde game</string> + <string name="games_custom">Aangepaste game</string> + <string name="games_store_storage">Opslag voor games</string> + <string name="gpu_auto_hint">Het beste paar voor deze GPU, blijft up-to-date</string> + <string name="gpu_manual_hint">Kies zelf een paar</string> + <string name="gpu_checking">Controleren op drivers…</string> + <string name="gpu_none_listed">Nog geen drivers vermeld. Vernieuw om ernaar te zoeken.</string> + <string name="gpu_hide_other">Drivers voor andere GPU’s verbergen</string> + <string name="gpu_show_other">Drivers voor andere GPU’s tonen</string> + <string name="gpu_advanced">Geavanceerd</string> + <string name="gpu_set_with_display">Wordt samen met de beeldschermdriver ingesteld: %1$s bevat beide.</string> + <string name="gpu_set_with_runtime">Wordt samen met de runtimedriver ingesteld: %1$s bevat beide.</string> + <string name="gpu_runtime_hint">Gebruikt door games en de desktop.</string> + <string name="gpu_runtime_default">Runtimestandaard</string> + <string name="gpu_display_hint">Tekent frames naar het scherm. Geldt nadat DroidDeck opnieuw is gestart.</string> + <string name="gpu_auto_by_gpu">Automatisch - gekozen op basis van de GPU</string> + <string name="gpu_manage">Downloaden, importeren of verwijderen…</string> + <string name="gpu_in_use_caps">IN GEBRUIK</string> + <string name="gpu_builtin">De drivers die ingebouwd zijn in DroidDeck</string> + <string name="gpu_matched_pair">Runtime- en beeldschermdrivers samen ingesteld als bijpassend paar.</string> + <string name="gpu_recommended_for">Aanbevolen voor deze GPU: %1$s.</string> + <string name="gpu_refresh_hint">Vernieuw om te zoeken naar drivers voor deze GPU.</string> + <string name="gpu_recommended_caps">AANBEVOLEN</string> + <string name="gpu_unavailable">Niet beschikbaar</string> + <string name="load_runtime">Linux-runtime</string> + <string name="load_runtime_detail">Linux-runtime wordt voorbereid</string> + <string name="load_session">Sessie</string> + <string name="load_session_detail">Beeld en audio starten</string> + <string name="load_steam">Steam-client</string> + <string name="load_steam_detail">Steam en de compatibiliteitstools controleren</string> + <string name="load_opening_steam">Steam wordt geopend</string> + <string name="load_opening_steam_detail">Wachten op het eerste frame van Steam</string> + <string name="load_desktop">Desktop</string> + <string name="load_desktop_detail">Desktop wordt voorbereid</string> + <string name="load_opening">Openen</string> + <string name="load_opening_detail">Wachten op het eerste frame</string> + <string name="load_starting_steam">Steam wordt gestart</string> + <string name="load_ended">De sessie is beëindigd</string> + <string name="load_step">Stap %1$d van %2$d</string> + <string name="load_steam_paused">Steam is gepauzeerd</string> + <string name="deck_tab_grips">Grepen</string> + <string name="deck_tab_left">Linkerpad</string> + <string name="deck_tab_right">Rechterpad</string> + <string name="deck_tab_both">Beide</string> + <string name="deck_tab_gamepad">Gamepad</string> + <string name="deck_surface_stick">Stick</string> + <string name="deck_surface_trackpad">Trackpad</string> + <string name="deck_surface_switch">Wisselen tussen stick en trackpad</string> + <string name="deck_more_controls">Meer Deck-bediening</string> + <string name="deck_panels">PANELEN</string> + <string name="deck_close">Bediening op tweede scherm sluiten</string> + <string name="deck_open_steam_menu">Steam-menu openen</string> + <string name="deck_left_upper">Links · boven</string> + <string name="deck_left_lower">Links · onder</string> + <string name="deck_right_upper">Rechts · boven</string> + <string name="deck_right_lower">Rechts · onder</string> + <string name="deck_click">Klik</string> + <string name="deck_right_trackpad">Rechtertrackpad</string> + <string name="deck_left_trackpad">Linkertrackpad</string> + <string name="second_trackpad">Trackpad</string> + <string name="second_show_keyboard">Toetsenbord tonen</string> + <string name="second_hide_keyboard">Toetsenbord verbergen</string> + <string name="second_terminal">Linux-terminal</string> + <string name="second_terminal_not_ready">De Linux-sessie is nog niet klaar voor een terminal.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Games</string> + <string name="rail_desktop">Desktop</string> + <string name="rail_store">Winkel</string> + <string name="rail_components">Componenten</string> + <string name="rail_apps">Apps</string> + <string name="rail_setup">Installatie</string> + <string name="rail_updates">Updates</string> + <string name="rail_percent">%1$d%%</string> + <string name="rail_resume_named">%1$s hervatten</string> + <string name="roms_text">Verschijnt in elke sessie als /root/ROMs. Interne opslag staat op /root/Storage.</string> + <string name="roms_none">Geen map gekozen</string> + <string name="roms_applies">Geldt vanaf de volgende sessie. SD-kaarten werken ook.</string> + <string name="roms_choose">Map kiezen</string> + <string name="roms_change">Map wijzigen</string> + <string name="proton_lede">Installeer een ARM64-build van Proton en kies die per game in Steam → Eigenschappen → Compatibiliteit.</string> + <string name="proton_stop_first">Stop de actieve sessie om een compatibiliteitstool te installeren of te verwijderen.</string> + <string name="proton_available">Beschikbare builds</string> + <string name="proton_installed">%1$s geïnstalleerd</string> + <string name="proton_queued">In afwachting van een eerder verzoek</string> + <string name="proton_not_installed">Niet geïnstalleerd</string> + <string name="proton_update_available">Update beschikbaar → %1$s</string> + <string name="proton_remove_older">Oude versies van %1$s verwijderen</string> + <string name="proton_remove_older_body">Verwijdert %1$s. Games die op deze versies zijn ingesteld, moeten naar een andere Proton worden overgezet.</string> + <string name="proton_remove_body">Games die op deze versie zijn ingesteld, moeten naar een andere Proton worden overgezet.</string> + <string name="proton_install_now">Nu installeren</string> + <string name="proton_cancel_queued">Wachtrij annuleren</string> + <string name="pckb_android">Android-toetsenbord</string> + <string name="pckb_hide">Verbergen ✕</string> + <string name="main_launch_named">%1$s starten</string> + <string name="main_choose_screen">Kies een scherm. Tweede scherm: %1$s.</string> + <string name="main_secondary_gone">Het tweede scherm is niet meer beschikbaar.</string> + <string name="main_primary">Hoofdscherm</string> + <string name="main_secondary">Tweede scherm</string> + <string name="widgets_prev_tab">Vorig tabblad</string> + <string name="widgets_next_tab">Volgend tabblad</string> + <string name="widgets_on">Aan</string> + <string name="widgets_all_n">Alle %1$d</string> + <string name="widgets_none">Geen</string> + <string name="widgets_n_of_m">%1$d van %2$d</string> + <string name="widgets_off">Uit</string> + <string name="screen_bottom">Onderste scherm</string> + <string name="game_env_applies">Geldt bij de volgende gamestart. Het profiel van een game gaat voor het gedeelde profiel.</string> + <string name="game_env_other">Andere game via app-ID…</string> + <string name="game_env_done">Klaar</string> + <string name="game_env_default">Standaard</string> + <string name="game_env_off">Uit</string> + <string name="game_env_change">Wijzigen…</string> + <string name="game_env_change_title">Variabele wijzigen</string> + <string name="game_env_restore_default">Terug naar standaard</string> + <string name="game_env_eyebrow">Proton-games</string> + <string name="pip_title">Scherm-in-scherm</string> + <string name="pip_auto">Automatisch scherm-in-scherm starten</string> + <string name="unfolded_controls">Bediening op het uitgevouwen scherm</string> + <string name="unfolded_controls_hint">Uitgevouwen staat de game boven de Steam Deck-trackpads en achtergrepen. Dichtgevouwen is de game schermvullend.</string> + <string name="pip_suspended">Onderbroken</string> + <string name="pip_not_ready">Scherm-in-scherm is beschikbaar zodra de sessie klaar is.</string> + <string name="pip_permission">Sta scherm-in-scherm toe voor DroidDeck in de Android-instellingen.</string> + <string name="pip_unavailable">Android kon scherm-in-scherm niet openen.</string> + <string name="game_shortcut">Snelkoppeling</string> + <string name="game_shortcut_add">Toevoegen aan startscherm</string> + <string name="game_shortcut_choose">Game kiezen</string> + <string name="game_shortcut_scanning">Geïnstalleerde games zoeken…</string> + <string name="game_link_copy">Startlink kopiëren</string> + <string name="game_link_copied">Startlink gekopieerd</string> + <string name="game_shortcut_unsupported">Deze launcher kan geen snelkoppelingen toevoegen. Gebruik in plaats daarvan “Startlink kopiëren”.</string> + <string name="game_link_invalid">Ongeldige startlink voor game.</string> + <string name="game_link_missing">Deze game staat niet meer in je DroidDeck-bibliotheek.</string> + <string name="game_link_session_busy">Stop de huidige sessie voordat je een Steam-game start.</string> + <string name="game_link_failed">Kan de game niet naar Steam sturen. Probeer hem opnieuw te starten.</string> + <string name="setup_retry_removal">Verwijderen opnieuw proberen</string> + <string name="runtime_removing">Verwijderen</string> + <string name="runtime_removal_incomplete">Verwijderen onvolledig</string> + <string name="game_file_export">Gamebestand exporteren</string> + <string name="game_file_sync">Alle gamebestanden naar map synchroniseren</string> + <string name="game_file_stop_sync">Synchroniseren van gamebestanden stoppen</string> + <string name="game_file_folder">Kies een map voor frontend-gamebestanden</string> + <string name="game_file_exported">Gamebestanden opgeslagen in %1$s</string> + <string name="game_frontend_files">Frontend-bestanden</string> + + <string name="main_wait_runtime">Wacht tot de runtime-bewerking klaar is</string> + <string name="main_wait_proton_install">Wacht tot de installatie van de compatibiliteitstool klaar is</string> + <string name="main_wait_desktop_install">Wacht tot de installatie van de desktop-app klaar is</string> + <string name="main_wait_install_game">Wacht tot de installatie klaar is voordat je een game start</string> + <string name="main_saves_close_session">Sluit eerst de Steam-sessie, zodat de game niet over de import heen kan opslaan</string> + <string name="main_saves_importing">Importeren in %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="one">%1$d bestand uit de %2$s geïmporteerd in %3$s</item> + <item quantity="other">%1$d bestanden uit de %2$s geïmporteerd in %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">%1$d bestand uit de %2$s geïmporteerd in %3$s. Oude savegames als back-up bewaard in Download/DroidDeck/Saves/backups</item> + <item quantity="other">%1$d bestanden uit de %2$s geïmporteerd in %3$s. Oude savegames als back-up bewaard in Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Kies een savegame-zip voor %1$s</string> + <string name="main_saves_exporting">%1$s exporteren</string> + <plurals name="main_saves_exported"> + <item quantity="one">%1$d bestand geëxporteerd als %2$s: %3$s</item> + <item quantity="other">%1$d bestanden geëxporteerd als %2$s: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Kies waar je %1$s wilt opslaan (%2$s)</string> + <string name="main_action_failed">%1$s mislukt: %2$s</string> + <string name="main_no_logs_yet">Nog geen sessielogs: start eerst een sessie.</string> + <string name="main_no_logs_to_clear">Geen sessielogs om te wissen.</string> + <plurals name="main_logs_cleared"> + <item quantity="one">%1$d map met sessielogs gewist.</item> + <item quantity="other">%1$d mappen met sessielogs gewist.</item> + </plurals> + <string name="main_wadb_pair_failed">Koppelen voor draadloze foutopsporing mislukt</string> + <string name="main_wadb_command_failed">Opdracht voor draadloze foutopsporing mislukt</string> + <string name="main_wadb_unavailable">Draadloze foutopsporing is niet beschikbaar</string> + <string name="main_adb_copied">ADB-opdracht gekopieerd</string> + <string name="main_pick_roms">Kies de ROM-map</string> + <string name="main_non_adreno_title">Geen Adreno-GPU</string> + <string name="main_non_adreno_text">Turnip ondersteunt Adreno-GPU\'s. Op %1$s kan Steam een zwart scherm tonen. Download: %2$.0f MB.</string> + <string name="main_install_anyway">Toch installeren</string> + <string name="main_remove_runtime_title">Linux-runtime verwijderen</string> + <string name="main_remove_runtime_text">Hiermee verwijder je de runtime, de Steam-client erin en alle games die daar zijn geïnstalleerd.</string> + <string name="main_applied">Toegepast: %1$s</string> + <string name="android_app_open_failed_primary">Kan %1$s niet openen op het hoofdscherm</string> + <string name="android_app_open_failed_on">Kan %1$s niet openen op %2$s</string> + <string name="android_app_open_failed_display">Kan %1$s niet openen op scherm %2$d</string> + <string name="comp_busy_importing">Importeren</string> + <string name="comp_busy_swapping">Wisselen</string> + <string name="comp_busy_restoring">Herstellen</string> + <string name="comp_busy_cancelling">Annuleren</string> + <string name="comp_busy_deleting">Verwijderen</string> + <string name="comp_cannot_read">kan het bestand niet lezen</string> + <string name="comp_imported">%1$s geïmporteerd</string> + <string name="comp_swap_cancelled">De wachtende wissel is geannuleerd.</string> + <string name="comp_pick_wcp">Kies een componentpakket (-linux .wcp)</string> + <string name="comp_nightlies_unreachable">De Nightlies zijn niet bereikbaar</string> + <string name="comp_stored">%1$s opgeslagen</string> + <string name="comp_download_failed">Downloaden mislukt: %1$s</string> + <string name="drivers_pick_linux">Kies een Linux-runtimedriver (-Linux-zip of Android + Linux-bundel)</string> + <string name="drivers_pick_android">Kies een weergavedriver (AdrenoTools-zip of Android + Linux-bundel)</string> + <string name="drivers_pick_any">Kies een driver-zip of een Android + Linux-bundel</string> + <string name="main_pkg_problem">%1$s: %2$s</string> + <string name="main_removing_named">%1$s verwijderen…</string> + <string name="main_forgetting_named">%1$s vergeten…</string> + <string name="main_pick_game_storage">Kies de map voor gameopslag</string> + <string name="main_pick_decky_zip">Kies een Decky-plug-in (ZIP)</string> + <string name="main_storage_not_usable">Niet bruikbaar: %1$s</string> + <string name="main_running_program">Programma</string> + <string name="main_removing_runtime">Linux-runtime verwijderen</string> + <string name="main_settings_main_screen">Kan Instellingen niet openen op het onderste scherm; geopend op het hoofdscherm.</string> + <string name="main_open_dev_options">Open Ontwikkelaarsopties in Instellingen</string> + + <string name="upd_unknown_apps_failed">Kan de Android-instelling “Onbekende apps installeren” niet openen</string> + <string name="upd_couldnt_check">Kan niet controleren: %1$s</string> + <string name="upd_no_download">Deze build heeft geen download voor deze versie van DroidDeck</string> + <string name="upd_stop_first">Stop eerst de actieve sessie en update daarna</string> + <string name="upd_stage_downloading">Downloaden</string> + <string name="upd_stage_installing">Installeren</string> + <string name="upd_stage_confirm">Wachten op je bevestiging</string> + <string name="upd_failed">De update is mislukt</string> + <string name="upd_not_offered">Android heeft niet aangeboden om de update te installeren</string> + + <string name="decky_install_failed">Installatie van Decky mislukt: %1$s</string> + <string name="decky_plugin_starting">Plug-in importeren…</string> + <string name="decky_plugin_failed">Importeren van plug-in mislukt: %1$s</string> + <string name="decky_plugin_installed">Plug-in geïnstalleerd. Start Steam opnieuw om hem te laden.</string> + + <string name="drivers_not_checked">Nog niet gecontroleerd - tik op vernieuwen om nieuwe drivers te zoeken</string> + <string name="drivers_bundle_sets_display">Android + Linux: stelt ook de weergavedriver in</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: stelt ook de runtimedriver in</string> + <string name="drivers_runtime_builtin">de Turnip die in de runtime is ingebouwd</string> + <string name="drivers_system_vulkan">systeem-Vulkan: geen meegeleverde build voor deze GPU</string> + <string name="drivers_bundled">%1$s (meegeleverd)</string> + <string name="drivers_manual">Handmatig</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">Er is momenteel maar één helft gepubliceerd</string> + <string name="drivers_pair_size">%1$.0f MB voor beide</string> + <string name="drivers_none_to_set">Geen drivers om in te stellen: %1$s</string> + <string name="drivers_checking_latest">Nieuwste drivers zoeken…</string> + <string name="drivers_up_to_date_check_failed">Up-to-date (kan niet controleren: %1$s)</string> + <string name="drivers_recommended_unpublished">De aanbevolen drivers zijn momenteel niet gepubliceerd</string> + <string name="drivers_downloading_pair">%1$s %2$s downloaden…</string> + <string name="drivers_updated_to">Geüpdatet naar %1$s %2$s.</string> + <string name="drivers_updated_to_restart">Geüpdatet naar %1$s %2$s. De weergavedriver werkt na een herstart van DroidDeck.</string> + <string name="drivers_switched_to">Overgeschakeld naar %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Overgeschakeld naar %1$s %2$s. De weergavedriver werkt na een herstart van DroidDeck.</string> + <string name="drivers_using">%1$s %2$s wordt gebruikt.</string> + <string name="drivers_using_restart">%1$s %2$s wordt gebruikt. De weergavedriver werkt na een herstart van DroidDeck.</string> + <string name="drivers_not_a_driver">Geen driver-zip: geen AdrenoTools-driver, geen -Linux-Turnip en geen Android + Linux-bundel</string> + <string name="drivers_import_failed">Importeren mislukt: %1$s</string> + <string name="drivers_imported_unnamed">Geïmporteerde driver</string> + <string name="drivers_using_bundle">%1$s wordt gebruikt als runtime- én weergavedriver.</string> + <string name="drivers_using_bundle_restart">%1$s wordt gebruikt als runtime- én weergavedriver. De weergavedriver werkt na een herstart van DroidDeck.</string> + <string name="drivers_deleted_bundle">%1$s verwijderd (beide drivers)</string> + <string name="drivers_deleted">%1$s verwijderd</string> + <string name="drivers_release_status">Nieuwste: %1$s · gecontroleerd %2$s</string> + <string name="drivers_release_status_failed">Nieuwste: %1$s · %2$s onbereikbaar · gecontroleerd %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">%1$d min geleden</item> + <item quantity="other">%1$d min geleden</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">%1$d u geleden</item> + <item quantity="other">%1$d u geleden</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">%1$d dag geleden</item> + <item quantity="other">%1$d dagen geleden</item> + </plurals> + <string name="drivers_checking_sources">Banners-Turnip, WinNative en DroidDeck controleren…</string> + <string name="drivers_check_failed">controle mislukt</string> + <string name="drivers_installed_pick_either">%1$s geïnstalleerd - kies hem in een van beide lijsten om beide drivers in te stellen</string> + <string name="drivers_installed_pick">%1$s geïnstalleerd - kies hem in het menu</string> + + <string name="session_wait_removal">Wacht tot de runtime is verwijderd</string> + <string name="session_title_game">Game</string> + <string name="session_touch_auto_touchpad">touchpad</string> + <string name="session_touch_auto_direct">direct</string> + <string name="session_swap_failed">Wisselen mislukt: %1$s</string> + <string name="session_profile_save_failed">Profiel opslaan mislukt: %1$s</string> + <string name="session_desktop_paused">De desktop is gepauzeerd</string> + <string name="session_named_paused">%1$s is gepauzeerd</string> + <string name="session_program_paused">Het programma is gepauzeerd</string> + <string name="session_starting_desktop">Desktop wordt gestart</string> + <string name="session_starting_named">%1$s wordt gestart</string> + <string name="session_starting_program">Programma wordt gestart</string> + <string name="session_no_logs">Geen logs voor deze sessie.</string> + <string name="session_logs_archive_failed">Kan het archief met sessielogs niet maken.</string> + <string name="session_logs_share_failed">Kan de sessielogs niet delen.</string> + <string name="session_starting_session">Sessie wordt gestart…</string> + <string name="session_runtime_downloading">Linux-runtime downloaden</string> + <string name="session_runtime_downloading_mb">Linux-runtime downloaden · %1$d van %2$d MB</string> + <string name="session_runtime_checking">Linux-runtime controleren</string> + <string name="session_runtime_unpacking">Linux-runtime uitpakken</string> + <string name="session_desktop_downloading">desktop downloaden</string> + <string name="session_desktop_downloading_mb">desktop downloaden · %1$d van %2$d MB</string> + <string name="session_desktop_checking">desktop controleren</string> + <string name="session_desktop_unpacking">desktop uitpakken</string> + <string name="session_proton_downloading">Proton Experimental (ARM64) downloaden</string> + <string name="session_proton_downloading_mb">Proton Experimental (ARM64) downloaden · %1$d van %2$d MB</string> + <string name="session_proton_checking">Proton Experimental (ARM64) controleren</string> + <string name="session_proton_unpacking">Proton Experimental (ARM64) uitpakken</string> + <string name="session_runtime_catalog_unreachable">Kan de runtimecatalogus niet bereiken. Controleer de verbinding en druk opnieuw op Spelen.</string> + <string name="session_runtime_install_failed">De Linux-runtime is niet geïnstalleerd. Controleer de runtime bij Installatie en druk opnieuw op Spelen.</string> + <string name="session_desktop_install_failed">De desktop is niet geïnstalleerd (%1$s). Controleer de verbinding en druk opnieuw op Desktop.</string> + <string name="session_ended_named">%1$s is onverwacht gestopt. Deel de logs in een bugrapport of probeer het opnieuw.</string> + <string name="session_ended_program">Het programma is onverwacht gestopt. Deel de logs in een bugrapport of probeer het opnieuw.</string> + <string name="session_ended_desktop">De desktop is onverwacht gestopt. Deel de logs in een bugrapport of probeer het opnieuw.</string> + <string name="session_ended_detail">Afsluitstatus %1$d · %2$s</string> + <string name="session_ended_no_log">geen log</string> + <plurals name="session_hint_enosys"> + <item quantity="one">Het log toont %1$d fout “Function not implemented”: de seccomp-versnelling van proot laat op dit apparaat een hulpprogramma falen. Probeer %2$s → “%3$s” (of “%4$s”) en start opnieuw.</item> + <item quantity="other">Het log toont %1$d fouten “Function not implemented”: de seccomp-versnelling van proot laat op dit apparaat een hulpprogramma falen. Probeer %2$s → “%3$s” (of “%4$s”) en start opnieuw.</item> + </plurals> + + <string name="mode_decky_plugins">Plug-ins</string> + <string name="mode_decky_plugins_hint">Installeer een plug-in-ZIP voor de volgende Steam-sessie.</string> + <string name="mode_decky_plugin_downloading">Downloaden…</string> + <string name="mode_decky_plugin_install_zip">Installeren uit ZIP</string> + <string name="store_cat_games">Games</string> + <string name="store_cat_audio_video">Audio & video</string> + <string name="store_cat_graphics">Grafisch</string> + <string name="store_cat_internet">Internet</string> + <string name="store_cat_office">Kantoor</string> + <string name="store_cat_developer">Ontwikkeling</string> + <string name="store_cat_education">Onderwijs</string> + <string name="store_cat_science">Wetenschap</string> + <string name="store_cat_system">Systeem</string> + <string name="store_cat_utilities">Hulpprogramma\'s</string> + <string name="store_section_popular">Populair</string> + <string name="store_section_trending">In opkomst</string> + <string name="store_section_new">Nieuw</string> + <string name="store_section_updated">Bijgewerkt</string> + <string name="store_section_loading">laden…</string> + <string name="store_flatpak_setup">Flatpak instellen</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Kan Flathub niet controleren op updates</string> + <string name="content_rom_installed">geïnstalleerd</string> + <string name="content_rom_folder">map</string> + <string name="theme_graphite">Grafiet</string> + <string name="theme_graphite_detail">Grafietkleurige vlakken, met het eigen bolblauw van het logo voor alles wat geselecteerd, gefocust of ingedrukt is.</string> + <string name="theme_paper">Papier op zwart</string> + <string name="theme_paper_detail">Wit is de hoofdkleur, het blauw van het pictogram is het signaal voor lijnen, ringen en stippen.</string> + <string name="theme_icon_blue">Pictogramblauw</string> + <string name="theme_icon_blue_detail">Zwarte achtergrond, witte tekst, het blauw voor alles wat geselecteerd, gefocust of ingedrukt is.</string> + <string name="theme_electric">Elektrisch marineblauw</string> + <string name="theme_electric_detail">Het blauw feller, op vlakken die naar marineblauw neigen in plaats van grijs.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Spatie</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <string name="game_sync_err_runtime">Installeer eerst de runtime</string> + <string name="game_sync_err_steam_library">Steam-bibliotheek is niet beschikbaar</string> + <string name="game_sync_err_previous_sd">Vorige SD-bibliotheek is niet beschikbaar</string> + <string name="game_sync_err_game_storage">Gameopslag is niet beschikbaar</string> + <string name="game_sync_err_added_games">Map met toegevoegde games is niet beschikbaar</string> + <string name="game_sync_err_manifests">Steam-manifesten zijn niet beschikbaar</string> + <string name="content_system_many">Veel systemen</string> + <string name="fm_drive_internal">Intern</string> + <string name="fm_drive_linux">Linux-systeem</string> + <string name="fm_drive_sd">SD-kaart</string> + <string name="fm_drive_storage">Opslag</string> + <string name="fm_select">Selecteren</string> + <string name="fm_properties">Eigenschappen</string> + <string name="fm_favorite_add">Toevoegen aan favorieten</string> + <string name="fm_favorite_remove">Verwijderen uit favorieten</string> + <string name="fm_rename">Naam wijzigen</string> + <string name="fm_copy">Kopiëren</string> + <string name="fm_cut">Knippen</string> + <string name="fm_actions">Acties</string> + <string name="fm_favorites">Favorieten</string> + <string name="fm_pin_current">Huidige map vastzetten</string> + <string name="fm_no_favorites">Nog geen favorieten - zet een map vast via het ⋮-menu om hier snel naar terug te gaan.</string> + <string name="fm_unpin_favorite">Verwijderen uit favorieten</string> + <string name="fm_size_zero">0 bytes</string> + <string name="fm_unit_bytes">bytes</string> + <string name="fm_prop_location">Locatie</string> + <string name="fm_prop_type">Type</string> + <string name="fm_prop_size">Grootte</string> + <string name="fm_prop_modified">Gewijzigd</string> + <string name="fm_type_folder">Map</string> + <string name="fm_type_file">Bestand</string> + <string name="fm_type_ext_file">%1$s-bestand</string> + <string name="fm_read_only">Alleen-lezen</string> + <string name="fm_read_only_hint">Voorkomt dat games (en Wine) dit bestand overschrijven of verwijderen.</string> + <string name="fm_read_only_failed">Kan Alleen-lezen niet wijzigen</string> + <string name="fm_hidden">Verborgen</string> + <string name="fm_hidden_unsupported">Deze opslag kan de verborgen-markering niet opslaan.</string> + <string name="fm_hidden_hint">Markeert het bestand als verborgen in Windows (het verborgen-kenmerk van Wine).</string> + <string name="fm_hidden_failed">Kan Verborgen niet wijzigen op deze opslag</string> + <string name="fm_done">Klaar</string> + <string name="fm_op_deleting">Verwijderen...</string> + <string name="fm_delete_failed">Verwijderen mislukt</string> + <string name="fm_op_moving">Verplaatsen...</string> + <string name="fm_op_copying">Kopiëren...</string> + <string name="fm_op_moving_item">Verplaatsen - %1$s</string> + <string name="fm_op_copying_item">Kopiëren - %1$s</string> + <string name="fm_op_moving_batch">Verplaatsen %1$d/%2$d - %3$s</string> + <string name="fm_op_copying_batch">Kopiëren %1$d/%2$d - %3$s</string> + <string name="fm_paste_failed">%1$d klaar, %2$d mislukt</string> + <string name="fm_paste_skipped">%1$d klaar, %2$d overgeslagen</string> + <plurals name="fm_paste_moved"> + <item quantity="one">%1$d item verplaatst</item> + <item quantity="other">%1$d items verplaatst</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">%1$d item gekopieerd</item> + <item quantity="other">%1$d items gekopieerd</item> + </plurals> + <string name="fm_already_exists">“%1$s” bestaat al</string> + <string name="fm_op_renaming">Naam wijzigen...</string> + <string name="fm_rename_failed">Naam wijzigen mislukt</string> + <string name="fm_op_creating_folder">Map maken...</string> + <string name="fm_create_folder_failed">Kan map niet maken</string> + <string name="fm_new_folder">Nieuwe map</string> + <string name="fm_folder_name">Mapnaam</string> + <string name="fm_create">Maken</string> + <string name="fm_new_name">Nieuwe naam</string> + <string name="fm_delete_title">Verwijderen?</string> + <string name="fm_delete_text">“%1$s” definitief verwijderen?</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">%1$d item verwijderen?</item> + <item quantity="other">%1$d items verwijderen?</item> + </plurals> + <string name="fm_cannot_undo">Dit kan niet ongedaan worden gemaakt.</string> + <plurals name="fm_and_more"> + <item quantity="one">…en nog %1$d</item> + <item quantity="other">…en nog %1$d</item> + </plurals> + <string name="fm_op_deleting_batch">Verwijderen %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">%1$d kon niet worden verwijderd</item> + <item quantity="other">%1$d konden niet worden verwijderd</item> + </plurals> + <string name="fm_conflict_merge_hint">Samenvoegen voegt bestanden toe aan de bestaande map en vervangt ze daar.</string> + <string name="fm_conflict_overwrite_hint">Overschrijven vervangt het bestaande bestand.</string> + <string name="fm_conflict_apply_all">Toepassen op alle conflicten</string> + <string name="fm_conflict_merge">Samenvoegen</string> + <string name="fm_conflict_overwrite">Overschrijven</string> + <string name="fm_conflict_keep_both">Beide behouden</string> + <string name="fm_conflict_skip">Overslaan</string> + <string name="fm_select_this_folder">Deze map selecteren</string> + <string name="fm_back">Terug</string> + <string name="fm_drive_not_readable">%1$s is gekoppeld, maar nu niet leesbaar</string> + <string name="fm_list_view">Lijstweergave</string> + <string name="fm_grid_view">Rasterweergave</string> + <string name="fm_search">Zoeken</string> + <string name="fm_sort">Sorteren</string> + <string name="fm_sort_name">Naam</string> + <string name="fm_sort_date">Datum gewijzigd</string> + <string name="fm_rows_comfortable">Ruime rijen</string> + <string name="fm_rows_compact">Compacte rijen</string> + <string name="fm_hide_hidden">Verborgen bestanden verbergen</string> + <string name="fm_show_hidden">Verborgen bestanden tonen</string> + <string name="fm_hide_favorites">Favorieten verbergen</string> + <string name="fm_show_favorites">Favorieten tonen</string> + <string name="fm_filter_folder">Deze map filteren</string> + <string name="fm_free_space">%1$s vrij</string> + <string name="fm_selected_count">%1$d geselecteerd</string> + <string name="fm_select_none">Geen</string> + <string name="fm_select_all">Alle</string> + <string name="fm_paste">Plakken</string> + <plurals name="fm_items"> + <item quantity="one">%1$d item</item> + <item quantity="other">%1$d items</item> + </plurals> + <string name="fm_paste_here">%1$s hier plakken</string> + <string name="fm_paste_move_here">%1$s hier plakken (verplaatsen)</string> + <string name="fm_rail_title">Bestanden</string> + <string name="fm_rail_storage">OPSLAG</string> + <string name="fm_rail_quick">SNEL</string> + <string name="fm_rail_favorites">FAVORIETEN</string> + <string name="fm_rail_downloads">Downloads</string> + <string name="fm_rail_downloads_short">Downlds</string> + <string name="fm_rail_roms">ROMs</string> + <string name="fm_rail_session_logs">Sessielogs</string> + <string name="fm_rail_pictures">Afbeeldingen</string> + <string name="fm_favorite_added">“%1$s” toegevoegd aan favorieten</string> + <string name="fm_favorite_removed">“%1$s” verwijderd uit favorieten</string> + <string name="fm_empty_directory">Lege map</string> + <string name="fm_up_one_level">Eén niveau omhoog</string> + <string name="fm_choose_folder">Kies een map</string> + <string name="fm_choose_file">Kies een bestand</string> + <string name="ctrl_tint_steam_blue">Steam-blauw</string> + <string name="ctrl_tint_sky">Hemelsblauw</string> + <string name="ctrl_tint_white">Wit</string> + <string name="ctrl_tint_violet">Violet</string> + <string name="ctrl_tint_green">Groen</string> + <string name="ctrl_tint_amber">Amber</string> + <string name="ctrl_tint_red">Rood</string> + <string name="ctrl_tint_pink">Roze</string> + <string name="ctrl_button_a">A-knop</string> + <string name="ctrl_button_b">B-knop</string> + <string name="ctrl_button_x">X-knop</string> + <string name="ctrl_button_y">Y-knop</string> + <string name="ctrl_button_lb">Linkerbumper</string> + <string name="ctrl_button_rb">Rechterbumper</string> + <string name="ctrl_button_lt">Linkertrigger</string> + <string name="ctrl_button_rt">Rechtertrigger</string> + <string name="ctrl_button_view">Weergaveknop</string> + <string name="ctrl_button_menu">Menuknop</string> + <string name="ctrl_target_view">Weergave</string> + <string name="ctrl_target_menu">Menu</string> + <string name="ctrl_target_up">D-pad omhoog</string> + <string name="ctrl_target_down">D-pad omlaag</string> + <string name="ctrl_target_left">D-pad links</string> + <string name="ctrl_target_right">D-pad rechts</string> + <string name="ctrl_target_hidden">Verborgen</string> + <string name="ctrl_editor_drag">Sleep om te verplaatsen</string> + <string name="ctrl_editor_snap">Uitlijnen</string> + <string name="ctrl_editor_save">Opslaan</string> + <string name="second_mode_none">Geen</string> + <string name="second_mode_keyboard_trackpad">Toetsenbord + trackpad</string> + <string name="second_mode_terminal">Terminal</string> + <string name="second_mode_deck_controls">Deck-grepen + trackpads</string> + + <string name="rtinst_removing">Linux-runtime verwijderen</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">Linux-runtime verwijderen · %1$d item</item> + <item quantity="other">Linux-runtime verwijderen · %1$d items</item> + </plurals> + <string name="rtinst_removed">Linux-runtime verwijderd</string> + <string name="rtinst_removal_failed">Kan het verwijderen van de Linux-runtime niet voltooien. Probeer het opnieuw bij Installatie.</string> + <string name="rtinst_removing_leftovers">Restanten van de runtime verwijderen</string> + <string name="rtinst_downloading">Downloaden</string> + <string name="rtinst_verifying">Verifiëren</string> + <string name="rtinst_extracting">Uitpakken</string> + <string name="rtinst_channel_description">Verschijnt tijdens het installeren of verwijderen van de Linux-runtime</string> + <string name="rtinst_title_installing">Linux-runtime installeren</string> + <string name="rtinst_title_removing">Linux-runtime verwijderen</string> + + <string name="deskpkg_runtime_missing">De Linux-runtime is niet geïnstalleerd</string> + <string name="deskpkg_no_checksum">De catalogus heeft geen checksum voor %1$s</string> + <string name="deskpkg_checksum_mismatch">Checksum komt niet overeen - er is niets gewijzigd</string> + <string name="deskpkg_installing">%1$s installeren</string> + <string name="deskpkg_place_failed">Kan de AppImage niet plaatsen</string> + <string name="deskpkg_extract_failed">Uitpakken mislukt</string> + <string name="deskpkg_install_failed">Installatie mislukt</string> + + <string name="flatpakmgr_setup_failed">Flatpak instellen mislukt (exitcode %1$d)</string> + <string name="flatpakmgr_setup_unfinished">Flatpak instellen is niet voltooid</string> + <string name="flatpakmgr_busy">Er loopt al een andere winkeltaak</string> + <string name="flatpakmgr_not_ready">Stel eerst Flatpak in</string> + <string name="flatpakmgr_starting">Starten</string> + <string name="flatpakmgr_exit_status">Flatpak is gestopt met status %1$d</string> + <string name="flatpakmgr_installing">%1$s installeren</string> + <string name="flatpakmgr_updating">%1$s updaten</string> + <string name="flatpakmgr_removing">%1$s verwijderen</string> + <string name="flatpakmgr_installing_step">%1$s installeren (%2$d/%3$d)</string> + <string name="flatpakmgr_updating_step">%1$s updaten (%2$d/%3$d)</string> + <string name="flatpakmgr_removing_step">%1$s verwijderen (%2$d/%3$d)</string> + <string name="flatpakmgr_ref_translations">vertalingen</string> + <string name="flatpakmgr_ref_graphics_drivers">grafische drivers</string> + <string name="flatpakmgr_ref_media_codecs">mediacodecs</string> + <string name="flatpakmgr_ref_runtime">%1$s-runtime</string> + <string name="flatpakmgr_ref_runtime_branch">%1$s-runtime %2$s</string> + + <string name="appimg_too_small">Het bestand is te klein voor een AppImage</string> + <string name="appimg_unreadable">Het bestand kan niet worden gelezen: %1$s</string> + <string name="appimg_not_linux">Dit is geen AppImage (het is geen Linux-programma)</string> + <string name="appimg_wrong_arch">Deze AppImage is niet gebouwd voor ARM64 (aarch64) of x86-pc\'s</string> + <string name="appimg_not_appimage">Dit is een Linux-programma, maar geen AppImage</string> + <string name="appimg_wrong_type">Alleen AppImages van type 2 kunnen worden geïmporteerd (deze is type %1$d)</string> + <string name="appimg_no_space_import">Niet genoeg vrije ruimte: importeren vraagt ongeveer %1$s</string> + <string name="appimg_no_space_update">Niet genoeg vrije ruimte: updaten vraagt ongeveer %1$s</string> + <string name="appimg_import_failed">Importeren mislukt</string> + <string name="appimg_gone">Die AppImage bestaat niet meer</string> + <string name="appimg_update_failed">Update mislukt</string> + <string name="appimg_extracting">%1$s uitpakken</string> + <string name="appimg_extract_failed">De AppImage kan niet worden uitgepakt</string> + <string name="appimg_extract_failed_detail">De AppImage kan niet worden uitgepakt: %1$s</string> + + <string name="deckymgr_stop_session_loader">Stop de actieve sessie voordat je Decky Loader installeert</string> + <string name="deckymgr_download_failed">Downloaden van Decky Loader mislukt</string> + <string name="deckymgr_size_mismatch">De grootte van Decky Loader kwam niet overeen met de metadata van de release</string> + <string name="deckymgr_checksum_download_failed">Kan de checksum van de release niet downloaden</string> + <string name="deckymgr_checksum_missing">Checksum van de release ontbreekt</string> + <string name="deckymgr_checksum_invalid">Checksum van de release is ongeldig</string> + <string name="deckymgr_checksum_mismatch">Checksum van Decky Loader komt niet overeen</string> + <string name="deckymgr_not_elf">Release-bestand is geen compatibel 64-bit Linux-programma</string> + <string name="deckymgr_session_started_loader">Er is een sessie gestart tijdens het downloaden; stop die voordat je Decky Loader installeert</string> + <string name="deckymgr_loader_not_executable">Kan PluginLoader niet als uitvoerbaar markeren</string> + <string name="deckymgr_cef_failed">Kan foutopsporing op afstand voor Steam CEF niet inschakelen</string> + <string name="deckymgr_stage_failed">Kan PluginLoader niet klaarzetten</string> + <string name="deckymgr_loader_preserve_failed">Kan de bestaande PluginLoader niet behouden</string> + <string name="deckymgr_loader_activate_failed">Kan de nieuwe PluginLoader niet activeren</string> + <string name="deckymgr_stop_session_plugin">Stop de actieve sessie voordat je een plug-in installeert</string> + <string name="deckymgr_loader_required">Installeer Decky Loader voordat je een plug-in importeert</string> + <string name="deckymgr_zip_unreadable">Kan het geselecteerde zip-bestand niet lezen</string> + <string name="deckymgr_plugins_dir_failed">Kan de map voor Decky-plug-ins niet maken</string> + <string name="deckymgr_extracting_zip">Zip-bestand van de plug-in uitpakken</string> + <string name="deckymgr_zip_one_folder">Het zip-bestand van de plug-in moet precies één hoofdmap met de plug-in bevatten</string> + <string name="deckymgr_zip_missing_files">In het zip-bestand van de plug-in ontbreekt plugin.json, package.json of dist/index.js</string> + <string name="deckymgr_plugin_name_missing">Naam van de plug-in ontbreekt</string> + <string name="deckymgr_session_started_plugin">Er is een sessie gestart tijdens de installatie; stop die voordat je een plug-in installeert</string> + <string name="deckymgr_plugin_installed">Plug-in geïnstalleerd · start Steam opnieuw om hem te laden</string> + <string name="deckymgr_plugin_install_failed">Installeren van het zip-bestand van de plug-in is mislukt</string> + <string name="deckymgr_zip_too_many">Het zip-bestand van de plug-in bevat te veel items</string> + <string name="deckymgr_zip_invalid_path">Het zip-bestand van de plug-in bevat een ongeldig pad</string> + <string name="deckymgr_zip_duplicate_paths">Het zip-bestand van de plug-in bevat dubbele paden</string> + <string name="deckymgr_zip_unsafe_path">Het zip-bestand van de plug-in bevat een onveilig pad</string> + <string name="deckymgr_folder_failed">Kan de map van de plug-in niet maken</string> + <string name="deckymgr_zip_too_large">Het zip-bestand van de plug-in is na uitpakken groter dan de limiet van 1 GiB</string> + <string name="deckymgr_bundled_not_executable">Kan het meegeleverde binaire bestand van de plug-in niet als uitvoerbaar markeren</string> + <string name="deckymgr_remote_entry_invalid">Ongeldig remote_binary-item</string> + <string name="deckymgr_remote_name_invalid">Ongeldige naam voor extern binair bestand</string> + <string name="deckymgr_remote_url_https">De URL van het externe binaire bestand moet HTTPS gebruiken</string> + <string name="deckymgr_remote_hash_invalid">Extern binair bestand %1$s heeft een ongeldige SHA-256-hash</string> + <string name="deckymgr_bin_dir_failed">Kan de bin-map van de plug-in niet maken</string> + <string name="deckymgr_downloading_binary">Binair bestand van de plug-in downloaden · %1$s</string> + <string name="deckymgr_binary_download_failed">Kan het binaire bestand van de plug-in %1$s niet downloaden</string> + <string name="deckymgr_binary_checksum_mismatch">Checksum van het binaire bestand van de plug-in komt niet overeen · %1$s</string> + <string name="deckymgr_binary_not_executable">Kan het binaire bestand van de plug-in niet als uitvoerbaar markeren · %1$s</string> + <string name="deckymgr_binary_replace_failed">Kan het binaire bestand van de plug-in niet vervangen · %1$s</string> + <string name="deckymgr_binary_install_failed">Kan het binaire bestand van de plug-in niet installeren · %1$s</string> + <string name="deckymgr_folder_taken">Deze map hoort al bij een andere plug-in</string> + <string name="deckymgr_preserve_failed">Kan de bestaande plug-in niet behouden</string> + <string name="deckymgr_activate_failed">Kan de geïmporteerde plug-in niet activeren</string> + <string name="deckymgr_settings_failed">Kan de instellingen van de Decky-plug-in niet bijwerken</string> + <string name="deckymgr_restore_failed">Kan de bestaande plug-in niet herstellen</string> + + <string name="adbfix_save_failed">Kan de identiteit voor draadloze foutopsporing niet opslaan</string> + <string name="adbfix_command_failed">ADB-opdracht mislukt (%1$d)</string> + <string name="adbfix_not_confirmed">Android heeft niet bevestigd dat de limiet voor onderliggende processen is gewijzigd: %1$s</string> + <string name="adbfix_no_pairing">Er is geen koppeling voor draadloze foutopsporing opgeslagen</string> + <string name="adbfix_enter_address">Voer het huidige IP-adres en de poort voor draadloze foutopsporing in</string> + + <string name="adbpair_timed_out">Wachten op de pop-up met de koppelcode duurde te lang. Probeer het opnieuw vanuit DroidDeck.</string> + <string name="adbpair_discovery_failed_code">Kan niet naar de pop-up met de koppelcode zoeken (%1$d). Controleer of wifi aan staat.</string> + <string name="adbpair_discovery_failed">Kan niet naar de pop-up met de koppelcode zoeken: %1$s</string> + <string name="adbpair_six_digits">Voer alle 6 cijfers van de code in</string> + <string name="adbpair_pairing">Koppelen…</string> + <string name="adbpair_code_rejected">Die code werkte niet. Open de pop-up met de koppelcode opnieuw en voer de nieuwe code in.</string> + <string name="adbpair_connecting">Gekoppeld. Verbinden…</string> + <string name="adbpair_port_not_found">Gekoppeld, maar de poort voor draadloze foutopsporing is niet gevonden. Laat draadloze foutopsporing aan staan en probeer het opnieuw.</string> + <string name="adbpair_applying">Instelling toepassen…</string> + <string name="adbpair_command_failed">Opdracht voor draadloze foutopsporing mislukt</string> + <string name="adbpair_channel_name">Draadloze foutopsporing instellen</string> + <string name="adbpair_channel_description">Vraagt om de koppelcode voor draadloze foutopsporing terwijl Instellingen open is</string> + <string name="adbpair_title_pair">Draadloze foutopsporing koppelen</string> + <string name="adbpair_waiting_text">Schakel draadloze foutopsporing in en tik vervolgens op “Apparaat koppelen met koppelcode”.</string> + <string name="adbpair_waiting_long">Schakel draadloze foutopsporing in en tik vervolgens op “Apparaat koppelen met koppelcode”. Blijf in Instellingen: de code komt in deze melding.</string> + <string name="adbpair_code_label">Koppelcode van 6 cijfers</string> + <string name="adbpair_code_hint">Tik op Code invoeren en typ de 6 cijfers die in Instellingen worden getoond.</string> + <string name="adbpair_title_code">Voer de Wi-Fi-koppelcode in</string> + <string name="adbpair_title_retry">Probeer de code opnieuw</string> + <string name="adbpair_enter_code">Code invoeren</string> + <string name="adbpair_title_working">Steam instellen</string> + <string name="adbpair_title_done">Steam is klaar</string> + <string name="adbpair_done_text">Tik om terug te gaan naar DroidDeck. Je kunt draadloze foutopsporing nu uitschakelen.</string> + <string name="adbpair_title_failed">Instellen van draadloze foutopsporing gestopt</string> + + <string name="selfinst_http">De download antwoordde met HTTP %1$d</string> + <string name="selfinst_size_mismatch">De downloadgrootte kwam niet overeen met de gepubliceerde APK en is verwijderd - probeer het opnieuw</string> + <string name="selfinst_checksum_mismatch">De download kwam niet overeen met de checksum en is verwijderd - probeer het opnieuw</string> + <string name="selfinst_wrong_package">De update is voor Android-package %1$s, niet voor %2$s</string> + <string name="selfinst_metadata_signer">De metadata van de update vermeldt niet de ondertekeningssleutel van DroidDecks release</string> + <string name="selfinst_unreadable">Android kon de gedownloade APK niet lezen</string> + <string name="selfinst_apk_wrong_package">De gedownloade APK is voor %1$s, niet voor %2$s</string> + <string name="selfinst_version_mismatch">De gedownloade APK geeft versionCode %1$d aan, niet de gepubliceerde %2$d</string> + <string name="selfinst_downgrade">Android installeert versionCode %1$d niet over de geïnstalleerde %2$d heen</string> + <string name="selfinst_no_signing">De gedownloade APK heeft geen ondertekeningsinformatie</string> + <string name="selfinst_wrong_signer">De gedownloade APK is niet ondertekend met de release-sleutel van DroidDeck</string> + <string name="appupd_http">De updatecatalogus antwoordde met HTTP %1$d</string> + + <string name="loadstate_starting">Sessie wordt gestart…</string> + <string name="loadstate_elapsed">%1$d:%2$02d verstreken · nog bezig</string> + <string name="loadstate_steam_update">Update van de Steam-client downloaden · %1$d%%</string> + <string name="loadstate_steam_client">Steam-client downloaden · %1$s</string> + + <string name="pextra_busy">Er wordt al een compatibiliteitstool geïnstalleerd</string> + <string name="pextra_stop_session">Stop de actieve sessie voordat je compatibiliteitstools installeert</string> + <string name="pextra_no_release">Kan geen ARM64-release vinden voor %1$s</string> + <string name="pextra_no_space">Om %1$s te installeren is minstens 4 GB vrije ruimte nodig, plus de downloadgrootte</string> + <string name="pextra_downloading">%1$s %2$s downloaden</string> + <string name="pextra_retrying">Download opnieuw proberen · poging %1$d van %2$d</string> + <string name="pextra_interrupted">Download onderbroken</string> + <string name="pextra_download_failed">Download mislukt; het deelbestand is bewaard zodat een nieuwe poging kan worden hervat</string> + <string name="pextra_too_big">Download was groter dan de release en is verwijderd</string> + <string name="pextra_incomplete">Download was niet volledig; het deelbestand is bewaard zodat de poging kan worden hervat</string> + <string name="pextra_verifying">Download verifiëren</string> + <string name="pextra_checksum_unreadable">Kan de checksum van de release niet lezen; probeer het opnieuw</string> + <string name="pextra_no_checksum">%1$s %2$s publiceert geen checksum; er is niets geïnstalleerd</string> + <string name="pextra_checksum_mismatch">Checksum komt niet overeen; de download is verwijderd</string> + <string name="pextra_session_started">Er is een sessie gestart tijdens het downloaden; stop die voordat je compatibiliteitstools installeert</string> + <string name="pextra_installing">%1$s installeren</string> + <string name="pextra_install_failed">Installeren van %1$s is mislukt (afsluitcode %2$d)</string> + <string name="pextra_could_not_install">Kan %1$s niet installeren</string> + <string name="esync_fetching">Pakket droiddeck-esync ophalen</string> + + <string name="cmgr_fex_linux_helpers">met Linux-hulpprogramma’s</string> + <string name="cmgr_fex_dlls_only">Alleen DLL’s</string> + <string name="cmgr_original_proton_updated">Origineel (Proton bijgewerkt sinds de laatste wissel)</string> + <string name="cmgr_changed_outside">Gewijzigd buiten Componenten</string> + <string name="cmgr_not_present">niet aanwezig</string> + <string name="cmgr_original_version">Origineel %1$s</string> + <string name="cmgr_original_from">Origineel van %1$s</string> + <string name="cmgr_swap_queued">%1$s %2$s wordt in %3$s geplaatst zodra de game die erop draait, wordt afgesloten.</string> + <string name="cmgr_swap_done">%1$s %2$s staat nu in %3$s. Dit wordt toegepast bij de volgende gamestart.</string> + <string name="cmgr_original_missing">Die originele bundel is niet opgeslagen</string> + <string name="cmgr_restore_queued">%1$s van %2$s gaat terug naar het origineel zodra de game die erop draait, wordt afgesloten.</string> + <string name="cmgr_restore_done">%1$s van %2$s is hersteld vanuit het %3$s-origineel.</string> + <string name="cmgr_package_in_use">%1$s is in gebruik in %2$s. Wissel die Proton eerst naar iets anders.</string> + <string name="cmgr_package_not_stored">%1$s is niet opgeslagen</string> + <string name="cmgr_deleted">%1$s verwijderd.</string> + <string name="cmgr_original_kept">Het origineel van de geïnstalleerde build wordt bewaard: dat is de weg terug.</string> + <string name="cmgr_not_stored">Niet opgeslagen</string> + <string name="cmgr_deleted_original">Het %1$s-origineel van %2$s is verwijderd.</string> + <string name="cmgr_nightlies_only">Downloads komen alleen van de Nightlies-releases</string> + <string name="cmgr_list_predates_checksums">Deze pakketlijst is ouder dan de checksums - vernieuw de lijst en probeer het opnieuw</string> + <string name="cmgr_checksum_mismatch">Checksum komt niet overeen</string> + + <string name="sprefs_upscaler_linear">Lineair</string> + <string name="sprefs_upscaler_nearest">Dichtstbijzijnde</string> + <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (kwaliteit)</string> + <string name="sprefs_upscaler_sharpen">Alleen verscherpen</string> + <string name="sprefs_lod_bias_auto">Automatisch (volgt de schaling)</string> + + <string name="look_off_desc">Niets toegepast.</string> + <string name="look_game_clarity">Game-helderheid</string> + <string name="look_game_clarity_desc">Verscherpen met een vleugje contrast - de dagelijkse optie.</string> + <string name="look_vivid">Levendig</string> + <string name="look_vivid_desc">Kleur flink opgevoerd. Deband staat aan zodat luchten niet strepen.</string> + <string name="look_cinematic">Cinematisch</string> + <string name="look_cinematic_desc">Dieper contrast, koelere kleur, randen afgevlakt.</string> + <string name="look_competitive">Competitief</string> + <string name="look_competitive_desc">Schaduwen opgelicht en zware verscherping - zie anderen als eerste.</string> + <string name="look_adaptive_sharpen">Adaptief verscherpen</string> + <string name="look_adaptive_sharpen_desc">Alleen verscherpen, kleur ongewijzigd.</string> + <string name="look_filmic">Filmisch</string> + <string name="look_filmic_desc">Filmachtige curve, licht gedempt.</string> + <string name="look_arcade">Arcade</string> + <string name="look_arcade_desc">Luid en krachtig.</string> + <string name="look_retro_crt">Retro CRT</string> + <string name="look_retro_crt_desc">De CRT-buis en het analoge signaal samen toegepast.</string> + <string name="look_upscale_sharp">Scherp opschalen</string> + <string name="look_upscale_sharp_desc">Voor gebruik onder de schermresolutie.</string> + <string name="look_pixel_clean">Scherpe pixels</string> + <string name="look_pixel_clean_desc">Scherpe pixels voor 2D- en oudere games.</string> + <string name="look_anime_edge">Anime-lijnen</string> + <string name="look_anime_edge_desc">Contourlijnen plus verscherping.</string> + + <string name="gpuinfo_not_adreno">Geen Adreno-GPU</string> + <string name="gpuinfo_supported">Ondersteund</string> + <string name="gpuinfo_experimental">Experimenteel: de drivers zijn testbuilds</string> + <string name="gpuinfo_below_tested">Onder de geteste hardware (Adreno 725 en nieuwer): werkt mogelijk niet</string> + <string name="gpuinfo_unsupported">Niet ondersteund: DroidDeck heeft een Adreno-GPU (Snapdragon) nodig</string> + + <string name="turnip_rate_limited">De ratelimiet van GitHub is bereikt - probeer het later opnieuw</string> + <string name="turnip_github_http">GitHub antwoordde met HTTP %1$d</string> + <string name="turnip_download_http">download antwoordde met HTTP %1$d</string> + <string name="turnip_list_predates_checksums">Deze driverlijst is ouder dan de checksums - vernieuw de lijst en probeer het opnieuw</string> + <string name="turnip_checksum_mismatch">Checksum komt niet overeen - de download is verwijderd</string> + <string name="turnip_not_adrenotools">Geen AdrenoTools-driver: %1$s</string> + <string name="turnip_reject_no_meta">geen meta.json</string> + <string name="turnip_reject_meta_unreadable">meta.json is onleesbaar (%1$s)</string> + <string name="turnip_reject_linux_driver">dit is een Linux-runtimedriver (importeer die onder “Linux-runtimedriver”)</string> + <string name="turnip_reject_wayland">dit is een Wayland-gamedriver, waar deze app niets aan heeft</string> + <string name="turnip_reject_no_library_name">meta.json noemt geen libraryName, dus er kon niets aan AdrenoTools worden doorgegeven</string> + <string name="turnip_reject_library_missing">meta.json noemt %1$s, dat niet in de zip staat</string> + <string name="turnip_reject_not_aarch64">%1$s is geen 64-bit AArch64 ELF shared library</string> + <string name="turnip_reject_glibc">%1$s koppelt aan glibc - een “-Linux”-Turnip, die onder “Linux-runtimedriver” hoort</string> + <string name="lvd_no_library">Geen libvulkan_freedreno*.so in deze zip. Een Android-zip (AdrenoTools) of -Wayland-Turnip-zip is geen Linux-runtimedriver.</string> + <string name="lvd_not_aarch64">%1$s is geen 64-bit AArch64 ELF shared library.</string> + <string name="lvd_not_glibc">%1$s is geen glibc-driver - hij koppelt aan de libc van Android. De Linux-runtime heeft een “-Linux”-zip nodig; een gewone of “-Wayland”-Turnip kan door de Steam-client helemaal niet worden geladen.</string> + <string name="bundle_no_manifest">Geen driverbundel: geen manifest.json dat een android- en een linux-driver noemt</string> + <string name="bundle_entry_outside">Geen driverbundel: %1$s wijst buiten de bundel</string> + <string name="bundle_too_large">Geen driverbundel: na uitpakken is dit groter dan %1$d MB</string> + + <string name="gstore_sd_card">SD-kaart</string> + <string name="gstore_folder">Map</string> + + <string name="logshare_chooser">Sessielogs delen</string> + + <string name="hdr_needs_android_10">vereist Android 10 of nieuwer (schermlagen met een kleurruimte)</string> + <string name="hdr_none">dit scherm geeft geen ondersteuning voor HDR aan</string> + <string name="hdr_not_hdr10">dit scherm ondersteunt %1$s, niet HDR10</string> + + <string name="gpu_this_gpu">deze GPU</string> + <string name="phantom_title_on">Limiet voor onderliggende processen staat aan</string> + <string name="phantom_title_unset">Limiet voor onderliggende processen is niet ingesteld</string> + <string name="phantom_title_unreadable">Limiet voor onderliggende processen kon niet worden gecontroleerd</string> + <string name="phantom_title_off">Limiet voor onderliggende processen staat uit</string> + <string name="phantom_title_not_required">Limiet voor onderliggende processen niet nodig</string> + <string name="phantom_instructions_fix">%1$s Als dat niet mogelijk is, sluit dit apparaat aan op een computer met ADB en voer de onderstaande opdracht uit. Kom hierna terug en controleer opnieuw.</string> + <string name="phantom_instructions_off">Steam-sessies kunnen starten.</string> + <string name="phantom_instructions_not_applicable">Deze Android-versie gebruikt deze limiet niet.</string> + <string name="phantom_fix_toggle">Schakel “Beperkingen voor onderliggende processen uitschakelen” in bij Ontwikkelaarsopties en laat Ontwikkelaarsopties daarna aan staan.</string> + <string name="phantom_fix_no_toggle">Deze Android-versie heeft hier geen schakelaar voor. Gebruik draadloze foutopsporing om dit vanaf dit apparaat te wijzigen.</string> + <string name="upd_block_no_apk">Deze build heeft geen download voor dit exemplaar van DroidDeck.</string> + <string name="upd_block_version_code">Android installeert dit niet over deze build heen, omdat de versionCode %1$d lager is dan de geïnstalleerde %2$d.</string> + <string name="upd_fail_downgrade">Android installeert geen oudere versie over een nieuwere heen. Blijf op deze build totdat het kanaal is bijgewerkt.</string> + <string name="upd_fail_signature">Dit exemplaar van DroidDeck is anders ondertekend (een lokale build?), waardoor Android het niet ter plekke kan bijwerken.</string> + <string name="upd_fail_storage">Er is niet genoeg vrije ruimte voor de update.</string> + <string name="upd_fail_aborted">De update is geannuleerd.</string> + <string name="upd_fail_with_message">De update is niet geïnstalleerd: %1$s</string> + <string name="upd_fail_generic">De update is niet geïnstalleerd.</string> + <string name="saves_layout_zip">%1$s-zip</string> + <string name="saves_zip">zip</string> + <string name="saves_not_launched">%1$s is nog niet gestart</string> + <string name="saves_none_found">Geen savegames gevonden voor %1$s</string> + <string name="saves_not_save_zip">%1$s heeft geen drive_c-map, dus het is geen savegame-zip van GameHub of Winlator</string> + <string name="gstore_cannot_create">kan geen mappen maken in %1$s</string> + <string name="gstore_cannot_write">kan niet schrijven in %1$s</string> + <string name="gstore_cannot_write_detail">kan niet schrijven in %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">Map is niet beschikbaar</string> + <string name="gfiles_name_taken">Een ander bestand gebruikt al %1$s</string> + <string name="gfiles_art_unavailable">Artwork-map is niet beschikbaar</string> + <string name="gfiles_unrelated_inventory">Map heeft een synchronisatie-inventaris die hier niet bij hoort</string> + <string name="gfiles_remove_failed">Kan %1$s niet verwijderen</string> + <string name="gfiles_outside_folder">Bestand verlaat de exportmap</string> + <string name="cmgr_fex_unnamed">FEX (build zonder versienaam)</string> + <string name="cmgr_no_profile">geen profile.json in %1$s</string> + <string name="cmgr_not_component_package">%1$s is geen Linux-componentpakket (type %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s vrij</string> + <string name="mode_storage_auto_short">Automatisch</string> + <string name="appupd_catalog_rejected">De updatecatalogus voldeed niet aan de controles (%1$s)</string> + <string name="main_adb_clip_label">DroidDeck-instelling voor onderliggende processen</string> + <string name="setup_signed_in_offline_unnamed">Aangemeld · offline modus</string> + <string name="setup_signed_in_unnamed">Aangemeld</string> + <string name="driver_tag_bundled">MEEGELEVERD</string> + <string name="driver_tag_downloaded">GEDOWNLOAD</string> + <string name="driver_tag_imported">GEÏMPORTEERD</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Sessiescherm</string> + <string name="display_resolution">Resolutie</string> + <string name="display_game_fit">16:9-beeldvulling</string> + <string name="display_game_fit_hint">Hoe 16:9-games dit scherm vullen. Volgende sessie.</string> + <string name="display_game_fit_unavailable">Kies Schermformaat of een aangepast formaat dat smaller is dan 16:9.</string> + <string name="display_preserve">Beeldverhouding behouden</string> + <string name="display_stretch">Uitrekken naar scherm</string> + <string name="display_image_scaling">Beeldschaling</string> + <string name="display_filter">Schaalfilter</string> + <string name="display_filter_hint">Gedeeld door Steam en Desktop.</string> + <string name="display_filter_live">Wordt direct toegepast. Gedeeld door Steam en Desktop.</string> + <string name="display_filter_note">FSR, GSR en NIS worden toegepast bij het vergroten van het sessiebeeld. Alleen verscherpen werkt ook zonder vergroting.</string> + <string name="display_sharpness">Filterscherpte</string> + <string name="display_frame_rate">Framerate</string> + <string name="display_look_filter">%1$s Zet schaling op %2$s.</string> + <string name="display_match_screen">Schermformaat (%1$d×%2$d)</string> + <string name="display_follow_screen">Scherm volgen (past formaat aan bij vouwen of uitvouwen)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Apparaatgegevens tonen</string> + <string name="device_info_hide">Apparaatgegevens verbergen</string> + <string name="device_info_loading">Apparaat lezen…</string> + <string name="device_info_copy">Kopiëren</string> + <string name="device_info_copy_hint">Kopieer deze gegevens in een bugrapport.</string> + <string name="device_info_copied">Gekopieerd.</string> + <string name="force_ssbs_title">SSBS forceren</string> + <string name="force_ssbs_hint">Houdt een CPU-snelheidsinstelling aan die Wine steeds uitschakelt. Kan Windows-games sneller maken op Snapdragon 8 Elite; op oudere chips is weinig verschil te zien. Geldt bij de volgende gamestart.</string> + + <string name="rail_stores">Winkels</string> + <string name="source_steam">Steam</string> + <string name="source_added">Aangepast</string> + <string name="setup_stores">Winkels</string> + <string name="setup_stores_open_on">Open een winkel op</string> + <string name="setup_stores_speed">Niveau voor downloadsnelheid</string> + <string name="setup_stores_engine">Winkels-engine</string> + <string name="setup_stores_engine_chip_ready">● Engine gereed</string> + <string name="setup_stores_engine_chip_missing">Niet gebouwd</string> + <string name="stores_tier_balanced">Gebalanceerd</string> + <string name="stores_tier_fast">Snel</string> + <string name="stores_tier_max">Max</string> + <string name="stores_title">Winkels</string> + <string name="stores_settings">Instellingen voor winkels</string> + <string name="stores_settings_eyebrow">Instellingen</string> + <string name="stores_chip_signed_out">(niet aangemeld)</string> + <string name="stores_tab_store">Winkel</string> + <string name="stores_tab_installed">Geïnstalleerd</string> + <string name="stores_tab_library">Bibliotheek</string> + <string name="stores_tab_all">Alle</string> + <string name="stores_tab_downloads">Downloads</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Aanmelden</string> + <string name="stores_signout_named">%1$s · afmelden</string> + <string name="stores_signout_title">Afmelden bij %1$s?</string> + <string name="stores_signout_note">Geïnstalleerde games blijven staan. De bibliotheek wordt opnieuw opgehaald wanneer je je weer aanmeldt.</string> + <string name="stores_signout_stay">Aangemeld blijven</string> + <string name="stores_signout_go">Afmelden</string> + <string name="stores_search_catalog">Zoeken in de catalogus van %1$s…</string> + <string name="stores_search_installed">Geïnstalleerde games zoeken…</string> + <string name="stores_search_library">Zoeken in je bibliotheek…</string> + <string name="stores_search_open">Zoeken</string> + <string name="stores_installed_empty">Niets geïnstalleerd</string> + <string name="stores_library_loading">Laden</string> + <string name="stores_nothing_matches">Geen games</string> + <string name="stores_shelves_loading">Winkel laden…</string> + <string name="stores_amazon_no_catalog">Geen catalogus</string> + <string name="stores_shelf_new">Nieuw</string> + <string name="stores_shelf_deals">Aanbiedingen</string> + <string name="stores_shelf_free">Gratis games</string> + <string name="stores_shelf_trending">Populair</string> + <string name="stores_shelf_library">Jouw bibliotheek</string> + <string name="stores_play">Spelen</string> + <string name="stores_install">Installeren</string> + <string name="stores_resume_install">Installatie hervatten</string> + <string name="stores_dl_interrupted">Onderbroken</string> + <string name="stores_install_size">Installeren · %1$s</string> + <string name="stores_get_free">Gratis verkrijgen</string> + <string name="stores_view_on">Bekijken op %1$s</string> + <string name="stores_buy_price">Kopen · %1$s</string> + <string name="stores_installed_chip">Geïnstalleerd</string> + <string name="stores_in_library">In bibliotheek</string> + <string name="stores_free">Gratis</string> + <string name="stores_resume">Hervatten</string> + <string name="stores_refresh">Vernieuwen</string> + <string name="stores_show_mature">Inhoud voor volwassenen tonen</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Offline</string> + <string name="epic_card_none">Uit</string> + <string name="epic_eos">Epic-aanmelding (EOS)</string> + <string name="epic_offline">Offline starten</string> + <string name="epic_resolve">Epic-aanmelding oplossen</string> + <string name="store_cloud_saves">Cloudsaves</string> + <string name="cloud_upload">Uploaden naar cloud</string> + <string name="cloud_download">Downloaden vanaf cloud</string> + <string name="cloud_none">Geen cloudsaves</string> + <string name="cloud_conflict">Cloudconflict</string> + <string name="cloud_conflict_files">Cloudconflict · %1$d bestanden</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Cloud behouden</string> + <string name="cloud_keep_local">Lokaal behouden</string> + <string name="cloud_checking">Cloud controleren…</string> + <string name="cloud_last_sync">Laatste synchronisatie %1$s</string> + <string name="cloud_never">Nog niet gesynchroniseerd</string> + <string name="cloud_done">%1$d bestanden</string> + <string name="stores_eyebrow_installed">geïnstalleerd</string> + <string name="stores_eyebrow_library">in je bibliotheek</string> + <string name="stores_eyebrow_store">winkel</string> + <string name="achievements_title">Prestaties</string> + <string name="achievements_count">%1$d van %2$d ontgrendeld</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Ontgrendeld · %1$d</string> + <string name="achievements_group_locked">Vergrendeld · %1$d</string> + <string name="achievements_group_hidden">Verborgen · %1$d</string> + <string name="achievements_hidden">Verborgen prestatie</string> + <string name="achievements_hidden_detail">Details worden getoond zodra deze is ontgrendeld.</string> + <string name="achievements_all">Alle prestaties ontgrendeld</string> + <string name="achievements_reading">Laden…</string> + <string name="achievements_signed_out">Log in bij %1$s om je prestaties te bekijken.</string> + <string name="achievements_unreachable">Kan %1$s niet bereiken. Controleer opnieuw zodra je online bent.</string> + <string name="achievements_none">%1$s vermeldt geen prestaties voor deze game.</string> + <string name="achievements_show_all">Alle %1$d weergeven</string> + <string name="achievements_dialog_title">Prestaties · %1$s</string> + <string name="stores_in_steam">✓ In Steam</string> + <string name="stores_uninstall">Verwijderen</string> + <string name="stores_uninstalling">Verwijderen…</string> + <string name="stores_uninstall_title">%1$s verwijderen?</string> + <string name="stores_uninstall_body_size">Maakt %1$s vrij en verwijdert het uit Steam.</string> + <string name="stores_uninstall_body">Verwijdert de bestanden en de Steam-snelkoppeling.</string> + <string name="stores_launch_not_registered">Steam herkent de game zodra het de volgende keer start; start de game vanuit Steam, of start Steam nu.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Steams keuze per game</string> + <string name="stores_card_audio">Audio</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Componenten</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Engine</string> + <string name="stores_card_engine_value">%1$s · native</string> + <string name="stores_card_engine_java">%1$s · ingebouwd</string> + <string name="stores_how_it_runs">Hoe het draait</string> + <string name="stores_kv_fetched">Opgehaald van</string> + <string name="stores_kv_fetched_value">%1$s, met jouw account</string> + <string name="stores_kv_installed_to">Geïnstalleerd op</string> + <string name="stores_kv_installed_to_value">Gameopslag (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Gestart door</string> + <string name="stores_kv_launched_by_value">Steam en Proton, hetzelfde pad als Steam-games</string> + <string name="stores_kv_launched_by_epic">Steam en Proton · Epic-aanmelding per start, EOS-overlay uit</string> + <string name="stores_kv_launched_by_amazon">Steam en Proton · Amazon Games Services-variabelen ingesteld</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">toegevoegd aan Steam bij installatie (Deck-UI + Steam Input)</string> + <string name="stores_downloads_empty">Geen downloads</string> + <string name="stores_dl_paused">Gepauzeerd</string> + <string name="stores_dl_queued">In wachtrij</string> + <string name="stores_dl_queued_at">In wachtrij · #%1$d</string> + <string name="stores_dl_cancelled">Geannuleerd</string> + <string name="stores_dl_failed">Mislukt</string> + <string name="stores_dl_failed_reason">Mislukt · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Pauzeren</string> + <string name="stores_dl_resume">Hervatten</string> + <string name="stores_dl_cancel">Annuleren</string> + <string name="stores_dl_retry">Opnieuw proberen</string> + <string name="stores_dl_clear">Wissen</string> + <string name="stores_dl_clear_all">Voltooide wissen</string> + <string name="stores_stage_manifest">Manifest</string> + <string name="stores_stage_download">Download</string> + <string name="stores_stage_verify">Verifiëren</string> + <string name="stores_stage_install">Installeren</string> + <string name="stores_stage_done">Klaar</string> + <string name="stores_stage_checking">Controleren</string> + <string name="stores_stage_downloading">Downloaden</string> + <string name="stores_dl_cancel_confirm">Download verwijderen?</string> + <string name="stores_dl_failed_kept">Mislukt · %1$s bewaard</string> + <string name="stores_stage_verifying">Verifiëren</string> + <string name="stores_stage_installing">Installeren</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d bestanden</string> + <string name="stores_download_manager">Downloadbeheer</string> + <string name="stores_parallel">Downloads tegelijk</string> + <string name="stores_install_where">Installeren op</string> + <string name="stores_install_title">%1$s installeren:</string> + <string name="stores_target_internal">Intern</string> + <string name="stores_target_free">%1$s vrij</string> + <string name="stores_notification_channel">Winkeldownloads</string> + <string name="stores_notification_channel_hint">Toont de voortgang van gamedownloads en houdt downloads op de achtergrond actief</string> + <string name="stores_notification_one">Downloaden</string> + <string name="stores_notification_many">%1$d downloads</string> + <string name="stores_notification_waiting">Voorbereiden…</string> + <string name="stores_notification_done_channel">Voltooide winkeldownloads</string> + <string name="stores_notification_installed">%1$s geïnstalleerd</string> + <string name="stores_notification_failed">Download van %1$s mislukt</string> + <string name="stores_notification_delete">Verwijderen</string> + <string name="stores_notification_keep">Bewaren</string> + <string name="stores_login_title">Aanmelden bij %1$s</string> + <string name="stores_login_provider_title">Aanmelden bij je provider</string> + <string name="stores_login_reload">Opnieuw laden</string> + <string name="stores_login_close">Sluiten</string> + <string name="stores_login_retry">Probeer het opnieuw</string> + <string name="stores_login_finishing">Aanmelden afronden…</string> + <string name="stores_login_error_network">De aanmeldpagina kon niet worden geladen: %1$s</string> + <string name="stores_login_error_idp_blocked">Deze provider staat geen aanmelding via een ingesloten pagina toe. Meld je in plaats daarvan aan met je winkelwachtwoord.</string> + <string name="stores_login_error_http">De aanmeldpagina gaf een foutmelding (HTTP %1$d).</string> + <string name="stores_login_error_ssl">De verbinding met de aanmeldpagina is niet beveiligd.</string> + <string name="stores_login_error_verification">De aanmelding kon niet worden geverifieerd. Probeer het opnieuw.</string> + <string name="stores_login_error_generic">Aanmelden mislukt. Probeer het opnieuw.</string> + <string name="common_close">Sluiten</string> + <string name="common_back">Terug</string> + <string name="common_edit">Bewerken</string> + <string name="games_add_all">Alle games in deze map toevoegen</string> + <string name="games_added_none">Niets nieuws in deze map</string> + <string name="edit_name">Naam</string> + <string name="edit_target">Doel</string> + <string name="edit_start_in">Beginnen in</string> + <string name="edit_launch_options">Opstartopties</string> + <string name="edit_artwork">Artwork</string> + <string name="edit_other_exe">Andere exe…</string> + <string name="edit_remove">Verwijderen</string> + <string name="art_cover">Cover</string> + <string name="art_background">Achtergrond</string> + <string name="art_logo">Logo</string> + <string name="art_icon">Pictogram</string> + <string name="art_src_folder">Gamemap</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">Jouw bestand</string> + <string name="art_src_exe">Vanuit de exe</string> + <string name="art_src_auto">Automatisch</string> + <string name="art_pick_image">Afbeelding kiezen…</string> + <string name="art_pick_title">Kies een afbeelding: %1$s</string> + <string name="art_reset">Terugzetten naar automatisch</string> + <plurals name="games_added_n"> + <item quantity="one">%1$d game toegevoegd</item> + <item quantity="other">%1$d games toegevoegd</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="one">%1$d al in Games</item> + <item quantity="other">%1$d al in Games</item> + </plurals> + <string name="hop_back">B · Terug naar %1$s</string> + <string name="hop_from_games">de game</string> + <string name="hop_from_play">Spelinstellingen</string> + <string name="sheet_graphics_drivers">Grafische drivers</string> + <string name="comp_this_proton">Deze Proton</string> + <string name="comp_every_proton">Elke Proton</string> + <string name="comp_for_every_proton">Voor elke Proton</string> + <string name="comp_available_count">Beschikbaar · %1$d</string> + <string name="comp_checked">Gecontroleerd %1$s</string> + <string name="comp_check_all">Controleren op nieuwe componenten en drivers</string> + <string name="comp_get">Ophalen</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton en FEX hieronder zijn van deze Proton</string> + <string name="comp_device_tested">%1$s · getest</string> + <string name="comp_device_untested">%1$s · ongetest</string> + <string name="comp_device_unsupported">%1$s · niet ondersteund</string> + <string name="gpu_auto_keeps">Houdt het aanbevolen paar actueel</string> + <string name="gpu_bar_auto">Automatisch · %1$s</string> + <string name="gpu_separately">Runtime- en weergavedriver afzonderlijk</string> + <string name="gpu_use_title">%1$s gebruiken?</string> + <string name="proton_remove_title">%1$s verwijderen?</string> + <string name="setup_tab_diagnostics">Diagnostiek</string> + <string name="setup_ready">Klaar om te spelen</string> + <string name="setup_not_ready">Nog niet klaar om te spelen</string> + <string name="setup_ready_runtime">runtime %1$s</string> + <string name="setup_ready_limit_off">proceslimiet uit</string> + <string name="setup_ready_signed_in">aangemeld als %1$s</string> + <string name="setup_needs_you">Als iets je aandacht nodig heeft</string> + <string name="setup_group_look">Weergave</string> + <string name="setup_group_device">Dit apparaat</string> + <string name="setup_group_art">Bibliotheekartwork</string> + <string name="flathub_eyebrow">LINUX-APPS</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Bureaubladapps voor het Linux-bureaublad, geopend vanuit Desktop.</string> + <string name="flathub_setup">Flathub instellen</string> + <string name="flathub_remove_title">%1$s verwijderen?</string> + <string name="flathub_remove_note">De instellingen en bestanden worden bewaard.</string> + <string name="flathub_keep">Bewaren</string> + <string name="desktop_size">Grootte op schijf</string> + <string name="desktop_uninstall_title">De desktop verwijderen?</string> + <string name="desktop_all_systems">Alle</string> + <string name="gog_achievements">GOG-prestaties</string> + <string name="gog_achievements_hint">Synchroniseert prestaties voor ondersteunde GOG-games. Geldt vanaf de volgende start.</string> + <string name="gog_achievements_off">Prestaties uit</string> + <string name="gog_achievements_on">Prestaties aan</string> + <string name="gog_achievements_signin">Log opnieuw in bij GOG</string> + <string name="gog_achievements_uploaded">Prestaties geüpload</string> + <string name="gog_achievements_pending">Uploaden van prestatie in behandeling</string> + <string name="gog_achievements_enable_store">Zet GOG-prestaties aan in de Stores-instellingen</string> + <string name="gog_achievements_unavailable">Prestaties niet beschikbaar; controleer sessielogs</string> + <string name="achievement_unlocked">Prestatie ontgrendeld</string> + <string name="touch_paste">Plakken</string> + <string name="touch_dpad">Richtingstoetsen</string> + <string name="touch_left_stick">Linkerstick</string> + <string name="touch_right_stick">Rechterstick</string> + <string name="touch_menu">Menu</string> + <string name="touch_keyboard">Toetsenbord</string> + <string name="touch_dpad_up">Richtingstoetsen omhoog</string> + <string name="touch_dpad_down">Richtingstoetsen omlaag</string> + <string name="touch_dpad_left">Richtingstoetsen links</string> + <string name="touch_dpad_right">Richtingstoetsen rechts</string> + <string name="touch_title">Aanraakbediening</string> + <string name="touch_controller">Aanraakcontroller</string> + <string name="touch_mouse">Muis</string> + <string name="touch_on">Aan</string> + <string name="touch_off">Uit</string> + <string name="touch_speed_slow">Langzaam</string> + <string name="touch_speed_normal">Normaal</string> + <string name="touch_speed_fast">Snel</string> + <string name="touch_edit_layout">Indeling bewerken</string> + <string name="touch_droiddeck_menu">DroidDeck-menu</string> + <string name="touch_drag_hint">Sleep bedieningselementen naar het scherm</string> + <string name="touch_arrange">Schikken</string> + <string name="touch_discard">Negeren</string> + <string name="touch_saving">Opslaan…</string> + <string name="touch_icon">Pictogram</string> + <string name="touch_narrower">B−</string> + <string name="touch_wider">B+</string> + <string name="touch_shorter">H−</string> + <string name="touch_taller">H+</string> + <string name="touch_hide">Verbergen</string> + <string name="touch_color_opacity">Kleur en transparantie…</string> + <string name="touch_copy_layout">Deze indeling kopiëren</string> + <string name="touch_layout_copied">Indeling gekopieerd; plak deze in de aanraakindeling van elke game</string> + <string name="touch_paste_layout">Gekopieerde indeling plakken</string> + <string name="touch_reset_layout">Terugzetten naar Steams standaard</string> + <string name="touch_no_binding">Deze bediening heeft geen toewijzing in Steam om er een pictogram aan te geven</string> + <string name="touch_icon_for">Pictogram voor %1$s</string> + <string name="touch_layout_color">Kleur van aanraakindeling</string> + <string name="touch_done">Klaar</string> + <string name="touch_save_failed">De aanraakindeling kon niet worden opgeslagen</string> + <string name="touch_saved">Aanraakindeling opgeslagen in Steam</string> + <string name="touch_app_id">App %1$d</string> + <string name="touch_label_hint">Label (optioneel)</string> + <string name="touch_icons_missing">Steams pictogrammen zijn niet gevonden</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d pictogram van Steam</item> + <item quantity="other">%1$d pictogrammen van Steam</item> + </plurals> + <string name="touch_icon_color">Pictogramkleur</string> + <string name="touch_button_color">Knopkleur</string> + <string name="touch_no_icon">Geen pictogram</string> + + <string name="stores_sync_sign_in">Aanmelding controleren…</string> + <string name="stores_sync_library">Je bibliotheek ophalen…</string> + <string name="stores_sync_game_list">Gamelijst ophalen…</string> + <string name="stores_sync_details">Gamedetails laden… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">%1$d game synchroniseren…</item> + <item quantity="other">%1$d games synchroniseren…</item> + </plurals> + <string name="stores_sync_progress">Synchroniseren… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Coverafbeeldingen ophalen…</string> + <string name="stores_sync_failed">Synchronisatie mislukt</string> + <string name="stores_sync_no_games">Geen games gevonden in je %1$s-bibliotheek</string> + <string name="stores_sync_amazon_no_answer">Amazon Games reageerde niet voor je bibliotheek: vernieuw, of meld je opnieuw aan</string> + <string name="stores_sync_gog_unreachable">Kan GOG niet bereiken</string> + <string name="stores_sync_gog_unreadable">Je GOG-bibliotheek kon niet worden gelezen</string> + <string name="stores_registering">Registreren bij Steam…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Cardboard-modus</string> + <string name="cardboard_mode_hint">Beelden voor linker- en rechteroog voor een VR-bril voor je telefoon.</string> + <string name="cardboard_source">Beeld</string> + <string name="cardboard_source_screen">Virtueel scherm</string> + <string name="cardboard_source_sbs">Side-by-side 3D</string> + <string name="cardboard_viewer">Bril</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Geen lens</string> + <string name="cardboard_ipd">Oogafstand</string> + <string name="cardboard_screen_width">Schermbreedte</string> + <string name="cardboard_screen_distance">Schermafstand</string> + <string name="cardboard_head_tracking">Hoofdtracking</string> + <string name="cardboard_no_sensor">Dit apparaat heeft geen rotatiesensor.</string> + <string name="cardboard_recenter">Weergave centreren</string> + <string name="cardboard_recenter_button">Centreren</string> + <string name="cardboard_steamvr">SteamVR (experimenteel)</string> + <string name="cardboard_steamvr_install">Installeren</string> + <string name="cardboard_steamvr_install_hint">Downloadt Valves SteamVR voor ARM64 (571 MB).</string> + <string name="cardboard_steamvr_installing">SteamVR downloaden en uitpakken…</string> + <string name="cardboard_steamvr_start">Starten</string> + <string name="cardboard_steamvr_ready_hint">Draait SteamVR met je hoofd als headset; het beeld schakelt naar side-by-side 3D.</string> + <string name="cardboard_steamvr_stop">Stoppen</string> + <string name="cardboard_steamvr_running_hint">SteamVR draait.</string> + <string name="cardboard_steamvr_unavailable">Niet beschikbaar in deze sessie.</string> + <string name="cardboard_steamvr_failed">SteamVR reageerde niet; controleer het sessielog.</string> + <string name="drawer_component_downloads">Componenten downloaden</string> + <string name="drawer_component_downloads_hint">Downloads worden gedeeld met de pagina Componenten. Kies hierboven een gedownloade versie om die bij de volgende start van een game te gebruiken.</string> + <string name="drawer_component_choose_download">Kies een versie om te downloaden</string> + <string name="stores_fact_developer">Ontwikkelaar</string> + <string name="stores_fact_publisher">Uitgever</string> + <string name="stores_fact_released">Uitgebracht</string> + <string name="stores_hltb_main">Hoofdverhaal</string> + <string name="stores_hltb_extras">+ Extra\'s</string> + <string name="stores_hltb_completionist">Completionist</string> + <string name="stores_hltb_hours">%1$s u</string> + <string name="stores_hltb_minutes">%1$d min</string> +</resources> diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml new file mode 100644 index 000000000..930e5a1f7 --- /dev/null +++ b/app/src/main/res/values-pl/strings.xml @@ -0,0 +1,2285 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>Przy pierwszym uruchomieniu pobierany jest Steam. Może to potrwać kilka minut.</item> + <item>Domyślnie wyjście z aplikacji wstrzymuje Steam; po powrocie wznowi on działanie tam, gdzie przerwał.</item> + <item>Twój kontroler jest widoczny jako pad Xbox 360. Bez niego użyj sterowania ekranowego.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>Przy pierwszym uruchomieniu konfigurowany jest pulpit. Może to potrwać kilka minut.</item> + <item>Domyślnie wyjście z aplikacji wstrzymuje sesję; po powrocie wznowi się ona tam, gdzie została przerwana.</item> + <item>Ekran działa jak touchpad: przeciągnij, aby przesunąć kursor, dotknij, aby kliknąć.</item> + <item>Twój kontroler jest widoczny jako pad Xbox 360. Bez niego użyj sterowania ekranowego.</item> + </string-array> + <string name="session_channel">Sesja</string> + <string name="session_channel_description">Widoczne, gdy otwarta jest sesja Steam lub pulpitu</string> + <string name="session_notification">Steam działa</string> + <string name="session_notification_downloads">Trwa pobieranie w Steam</string> + <string name="session_notification_paused">Sesja wstrzymana – dotknij „Wznów”, aby kontynuować</string> + <string name="resume_session">Wznów</string> + <string name="suspend_now">Wstrzymaj teraz</string> + <string name="stop_session">Zatrzymaj sesję</string> + <string name="frame_gen_title">Generowanie klatek</string> + <string name="frame_gen_hint">Wybierz tryb generowania klatek</string> + <string name="frame_gen_off">Wył.</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG adaptacyjne %1$d</string> + <string name="lsfg_needs_steam">Wymaga Lossless Scaling ze Steam</string> + <string name="lsfg_needs_install">Zainstaluj Lossless Scaling w Steam lub zaimportuj Lossless.dll</string> + <string name="lsfg_import">Importuj Lossless.dll…</string> + <string name="lsfg_using_steam">Używana jest kopia ze Steam</string> + <string name="lsfg_using_import">Używana jest zaimportowana kopia</string> + <string name="lsfg_pick_title">Wybierz Lossless.dll</string> + <string name="lsfg_imported">Zaimportowano Lossless.dll</string> + <string name="lsfg_import_unchanged">Ten plik Lossless.dll jest już używany</string> + <string name="lsfg_import_not_owned">To konto Steam nie ma Lossless Scaling</string> + <string name="lsfg_import_invalid">Ten plik to nie Lossless.dll z Lossless Scaling</string> + <string name="lsfg_import_failed">Nie udało się przygotować LSFG z tego pliku Lossless.dll</string> + <string name="lsfg_problem_missing">LSFG wymaga Lossless Scaling ze Steam lub zaimportowanego pliku Lossless.dll</string> + <string name="lsfg_problem_failed">Nie udało się przygotować shaderów LSFG z pliku Lossless.dll</string> + <string name="hud_fg_unsupported">%1$s: nieobsługiwane</string> + <string name="hud_fg_failed">%1$s: błąd silnika</string> + <string name="hud_fg_needs_lossless">%1$s: wymaga Lossless Scaling</string> + <string name="hud_fg_starting">%1$s %2$s · uruchamianie</string> + <string name="second_library_import_hint">Dodaje bibliotekę Steam i wyszukuje w niej gry na Windows. Posiadane gry korzystają ze swoich wpisów w Steam, a pozostałe stają się skrótami do gier spoza Steam.</string> + <string name="game_env_title">Środowisko gry</string> + <string name="game_env_hint">Zmienne środowiskowe dla gier uruchamianych przez Proton.</string> + <string name="game_env_edit">Edytuj</string> + <string name="game_env_shared">Wszystkie gry Proton</string> + <string name="game_env_scope">Profil</string> + <string name="game_env_app_id">ID aplikacji Steam</string> + <string name="game_env_open">Otwórz</string> + <string name="game_env_profile">Gra %1$s</string> + <string name="game_env_add">Dodaj zmienną</string> + <string name="game_env_name">Nazwa zmiennej</string> + <string name="game_env_value">Wartość</string> + <string name="game_env_save">Zapisz</string> + <string name="game_env_remove">Usuń</string> + <string name="game_env_restore">Przywróć dziedziczenie</string> + <string name="game_env_reset">Wyczyść nadpisania środowiska</string> + <string name="game_env_unset">Nie ustawiaj w tym profilu</string> + <string name="game_env_empty">Brak ustawionych zmiennych.</string> + <string name="game_env_inherited">Dziedziczona</string> + <string name="game_env_custom">Własna zmienna</string> + <string name="game_env_error">Nie udało się wczytać ani zapisać. Sprawdź wolne miejsce i spróbuj ponownie.</string> + <string name="game_env_invalid">Podaj prawidłową nazwę i wartość do 8192 znaków.</string> + <string name="game_env_unused">Standardowe środowisko uruchomieniowe Proton nie odczytuje tej zmiennej.</string> + <string name="game_env_loading">Wczytywanie…</string> + <string name="game_env_feature">Wymuszenie poziomu funkcji D3D12. 12_2 zgłasza możliwości DirectX 12 Ultimate; nieobsługiwane funkcje mogą zawieść. Pozostaw nieustawione, aby wykrywać automatycznie.</string> + <string name="game_env_shader">Wymuszenie modelu shaderów D3D12. 6_9 wymaga odpowiednio nowego VKD3D i pasujących funkcji GPU. Pozostaw nieustawione, aby wykrywać automatycznie.</string> + <string name="game_env_cache">Pozostaw włączoną pamięć podręczną shaderów Mesa, aby ograniczyć ponowną kompilację. false włącza pamięć podręczną.</string> + <string name="game_env_cache_size">Maksymalny rozmiar pamięci podręcznej shaderów Mesa, np. 1G. Większa pamięć podręczna zajmuje więcej miejsca.</string> + <string name="game_env_vkd3d_config">Opcje VKD3D dla D3D12. nodxr wyłącza ray tracing; używaj tylko w grze, która tego wymaga.</string> + <string name="game_env_proton_log">Ustaw 1, aby tworzyć dziennik diagnostyczny Proton. Zapis dziennika może obniżyć wydajność i zajmuje miejsce.</string> + <string name="game_env_wined3d">Ustaw 1, aby do testów zgodności D3D9–11 używać WineD3D opartego na OpenGL zamiast DXVK.</string> + <string name="game_env_no_ntsync">Ustaw 1, aby ta gra nie korzystała z ntsync (droiddeck-ntsync ani sterownika jądra); użyje droiddeck-esync, jeśli istnieje pakiet, a w przeciwnym razie droiddeck-fsync.</string> + <string name="game_env_no_esync">Ustaw 1, aby wyłączyć droiddeck-esync w tej grze; zamiast niego użyje droiddeck-fsync lub droiddeck-ntsync, gdy fsync też jest wyłączone.</string> + <string name="game_env_no_fsync">Ustaw 1, aby ta gra nie korzystała z droiddeck-fsync; użyje droiddeck-esync, jeśli istnieje pakiet, a w przeciwnym razie droiddeck-ntsync.</string> + <string name="game_env_fsync_first">Ustaw 1, aby uruchamiać tę grę na droiddeck-fsync, lub 0, aby preferować droiddeck-esync, gdy pakiet pasuje do jej wersji Proton.</string> + <string name="game_env_xalia">Ustaw 0, aby wyłączyć w tej grze pomocnika Proton do nawigacji kontrolerem.</string> + <string name="game_env_hud">Nakładka DXVK dla D3D9–11. fps pokazuje liczbę klatek; brak wartości ją ukrywa.</string> + <string name="game_env_dxvk_config">Ustawienia DXVK rozdzielone średnikami. Nowsze wersje ograniczają klatki przez dxvk.maxFrameRate; do zwykłej gry lepiej użyć limitu klatek sesji.</string> + <string name="game_env_vkd3d_limit">Limit liczby klatek D3D12. 0 wyłącza limit.</string> + <string name="game_env_glthread">Wielowątkowość OpenGL w Mesa. true może pomóc grom OpenGL ograniczonym przez CPU; testuj każdą grę osobno.</string> + <string name="sync_backend_title">Synchronizacja Wine</string> + <string name="sync_backend_hint">Jak gry synchronizują swoje wątki. Działa od następnej sesji.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">Preset FEX</string> + <string name="fex_next_launch">Działa od następnego uruchomienia gry.</string> + <string name="game_settings_title">Gry</string> + <string name="fex_default_label">Domyślne FEX</string> + <string name="fex_default_detail">Używa domyślnych ustawień FEX.</string> + <string name="fex_stability_label">Stabilność</string> + <string name="fex_stability_detail">Najbezpieczniejsze, najwolniejsze ustawienia.</string> + <string name="fex_compatibility_label">Zgodność</string> + <string name="fex_compatibility_detail">Pełne TSO z multiblock.</string> + <string name="fex_intermediate_label">Pośredni</string> + <string name="fex_intermediate_detail">Zrównoważone TSO z obniżoną precyzją x87.</string> + <string name="fex_performance_label">Wydajność</string> + <string name="fex_performance_detail">Całe TSO wyłączone; multiblock.</string> + <string name="fex_performance_tso_label">Wydajność + TSO</string> + <string name="fex_performance_tso_detail">Ustawienia „Wydajność” z włączonym głównym TSO.</string> + <string name="fex_extreme_label">Ekstremalny</string> + <string name="fex_extreme_detail">Wyłącza kontrole kodu samomodyfikującego się. Gry z JIT i DRM mogą przestać działać.</string> + <string name="fex_extreme_tso_label">Ekstremalny + TSO</string> + <string name="fex_extreme_tso_detail">Ustawienia „Ekstremalny” z włączonym głównym TSO.</string> + <string name="fex_extreme_gn_label">Ekstremalny-gn</string> + <string name="fex_extreme_gn_detail">Wydajność z TSC i wskazówkami metadanych. Wypróbuj przed „Ekstremalnym”.</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">Wydajność z pełnymi kontrolami kodu i ukrytym bitem hiperwizora.</string> + <string name="game_env_custom_value">Własna wartość</string> + <string name="game_env_select_value">Wybierz wartość</string> + <string name="user_apps_section">Emulatory i aplikacje</string> + <string name="user_apps_add">Dodaj</string> + <string name="user_apps_add_detail">Twoja własna aplikacja lub gra</string> + <string name="user_apps_kind_script">Skrypt</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d%%</string> + <string name="user_apps_starting">Uruchamianie…</string> + <string name="user_apps_added">Dodano: %1$s</string> + <string name="user_apps_removed">Usunięto: %1$s</string> + <string name="user_apps_back">Pulpit</string> + <string name="user_apps_gone">Tej aplikacji już nie ma. Wybierz ją ponownie z Pulpitu.</string> + <string name="user_apps_open">Otwórz %1$s</string> + <string name="user_apps_remove">Usuń</string> + <string name="user_apps_remove_confirm">Naciśnij ponownie, aby usunąć</string> + <string name="user_apps_stop_session">Zatrzymaj sesję, aby usunąć</string> + <string name="user_apps_script_linked">Uruchamia %1$s z jego folderu, podłączonego do sesji tam, gdzie się znajduje. Usunięcie aplikacji nie zmienia tego folderu.</string> + <string name="user_apps_script_copied">Uruchamia %1$s z kopii jego folderu w DroidDeck. Usunięcie aplikacji kasuje kopię, a nie oryginał.</string> + <string name="user_apps_runtime_required">Najpierw zainstaluj środowisko Linux</string> + <string name="user_apps_file_gone">Pliku już nie ma</string> + <string name="user_apps_no_space">Za mało wolnego miejsca: skopiowanie folderu wymaga %1$s</string> + <string name="user_apps_copying">Kopiowanie %1$s</string> + <string name="user_apps_failed">Nie udało się dodać aplikacji</string> + <string name="user_apps_github_checking">Szukanie wydania %1$s</string> + <string name="user_apps_github_unreachable">Nie udało się odczytać wydań %1$s</string> + <string name="user_apps_github_no_appimage">Żadne wydanie %1$s nie zawiera AppImage ARM64 ani x86_64</string> + <string name="user_apps_downloading">Pobieranie %1$s</string> + <string name="user_apps_download_failed">Pobieranie nie powiodło się</string> + <string name="user_apps_update_failed">Nie udało się zaktualizować aplikacji</string> + <string name="user_apps_icon_failed">Nie udało się odczytać tej ikony</string> + <string name="user_apps_saved">Zapisano: %1$s</string> + <string name="user_apps_edit">Edytuj</string> + <string name="user_apps_check_updates">Sprawdź aktualizacje</string> + <string name="user_apps_checking_updates">Sprawdzanie aktualizacji…</string> + <string name="user_apps_update_to">Aktualizuj do %1$s</string> + <string name="user_apps_up_to_date">Aktualna · %1$s</string> + <string name="user_apps_updated">Zaktualizowano: %1$s</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">Dodaj aplikację</string> + <string name="add_app_tab_script">Skrypt</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">Plik .sh. Uruchamia się z własnego folderu, na pełnym ekranie w gamescope.</string> + <string name="add_app_appimage_hint">AppImage ARM64 (aarch64) lub x86_64 uruchamiany przez FEX. Jest jednorazowo rozpakowywany do środowiska Linux.</string> + <string name="add_app_github_hint">Instaluje AppImage ARM64 z najnowszego wydania repozytorium lub wersję x86_64 przez FEX.</string> + <string name="app_fex_title">Używaj FEX</string> + <string name="app_fex_auto">Auto</string> + <string name="app_fex_on">Wł.</string> + <string name="app_fex_off">Wył.</string> + <string name="app_fex_hint">FEX uruchamia programy Linux x86 na tym urządzeniu ARM64. Auto używa go, gdy program jest zbudowany dla x86.</string> + <string name="app_fex_x86">%1$s przez FEX</string> + <string name="app_fex_not_ready">Ten program jest zbudowany dla komputerów %1$s i działa przez FEX, który nie jest jeszcze skonfigurowany. Otwórz raz Steam po zalogowaniu: DroidDeck pobierze wtedy FEX i potrzebne Steam Linux Runtime, a potem program się otworzy.</string> + <string name="session_end_program">Ten program</string> + <string name="session_end_fex_missing">%1$s jest zbudowany dla komputerów x86 i działa przez FEX, który nie jest jeszcze zainstalowany. Uruchom raz Steam po zalogowaniu i pozwól mu pobrać FEX oraz Steam Linux Runtime, a potem uruchom program ponownie.</string> + <string name="add_app_flatpak_hint">ID aplikacji z Flathub lub jej strona na Flathub. Jeśli Flatpak nie jest jeszcze skonfigurowany, zostanie skonfigurowany najpierw.</string> + <string name="add_app_file">Plik</string> + <string name="add_app_no_file">Nie wybrano pliku</string> + <string name="add_app_choose_file">Wybierz plik</string> + <string name="add_app_change_file">Zmień</string> + <string name="add_app_pick_script">Wybierz skrypt</string> + <string name="add_app_pick_appimage">Wybierz AppImage</string> + <string name="add_app_pick_icon">Wybierz ikonę</string> + <string name="add_app_repository">Repozytorium</string> + <string name="add_app_repository_placeholder">właściciel/repo lub link github.com</string> + <string name="add_app_repository_invalid">To nie jest repozytorium GitHub</string> + <string name="add_app_flatpak_id">ID aplikacji</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">To nie jest ID aplikacji z Flathub</string> + <string name="add_app_flatpak_bundle_hint">Lub plik .flatpak dla aplikacji spoza Flathub.</string> + <string name="add_app_pick_flatpak">Wybierz plik .flatpak</string> + <string name="add_app_source">Źródło</string> + <string name="add_app_appearance">Na pulpicie</string> + <string name="add_app_preview_name">Nowa aplikacja</string> + <string name="add_app_name">Nazwa</string> + <string name="add_app_name_placeholder">Z aplikacji</string> + <string name="add_app_icon">Ikona</string> + <string name="add_app_icon_default">Domyślna ikona</string> + <string name="add_app_choose_icon">Wybierz ikonę</string> + <string name="add_app_clear_icon">Wyczyść</string> + <string name="add_app_icon_suggestions">Z repozytorium</string> + <string name="add_app_icon_searching">Szukanie ikon…</string> + <string name="add_app_icon_reset">Domyślna</string> + <string name="edit_app_title">Edytuj %1$s</string> + <string name="edit_app_save">Zapisz</string> + <string name="add_app_checking_folder">Sprawdzanie folderu…</string> + <string name="add_app_folder_linked">Jego folder jest podłączony do sesji: %1$s</string> + <string name="add_app_folder_copied">Jego folder zawiera programy Linux, których nie można uruchomić z pamięci współdzielonej, więc zostanie skopiowany do DroidDeck (%1$s).</string> + <string name="add_app_confirm">Dodaj</string> + <string name="add_app_cancel">Anuluj</string> + <string name="add_app_runtime_required">Najpierw zainstaluj środowisko Linux w sekcji Konfiguracja.</string> + <string name="add_app_busy">Poczekaj, aż %1$s się zakończy.</string> + <string name="steam_compat_label">Zgodny</string> + <string name="proton_compatible_note">Wersje oznaczone „%1$s” w menu Zgodność w Steam mają poprawki prefiksu od DroidDeck i uruchamiają tu gry. Domyślnie jest to Proton Experimental ARM64 (%1$s) lub Proton 11.0 ARM64 (%1$s), gdy ta nowsza wersja jest zainstalowana. Własne wpisy Proton od Valve bez tego oznaczenia nie uruchomią gry na tym urządzeniu.</string> + <string name="proton_installed_compatible">Zainstalowano %1$s · %2$s</string> + <string name="common_cancel">Anuluj</string> + <string name="common_auto">Auto</string> + <string name="common_never">Nigdy</string> + <string name="common_always">Zawsze</string> + <string name="common_uninstall">Odinstaluj</string> + <string name="common_check">Sprawdź</string> + <string name="common_update">Aktualizuj</string> + <string name="common_checking">Sprawdzanie…</string> + <string name="common_working">Przetwarzanie…</string> + <string name="common_forget">Zapomnij</string> + <string name="common_add_ellipsis">Dodaj…</string> + <string name="common_use">Użyj</string> + <string name="common_applies_next_session">Działa od następnej sesji.</string> + <string name="mode_steam_title">Sesja Steam</string> + <string name="mode_desktop_title">Sesja pulpitu</string> + <string name="mode_display">Wyświetlacz</string> + <string name="mode_resolution">Rozdzielczość</string> + <string name="mode_res_720">Do 720p</string> + <string name="mode_res_900">Do 900p</string> + <string name="mode_res_1080">Do 1080p</string> + <string name="mode_res_native">Natywna</string> + <string name="mode_res_default">%1$s (domyślna)</string> + <string name="mode_res_custom_value">Własna · %1$d×%2$d</string> + <string name="mode_res_custom">Własna…</string> + <string name="mode_res_note">720p może poprawić responsywność menu.</string> + <string name="mode_ratio">Proporcje ekranu</string> + <string name="mode_ratio_custom">Ustalane przez własną rozdzielczość.</string> + <string name="mode_ratio_auto">Auto używa co najmniej 16:9.</string> + <string name="mode_fps">Limit klatek</string> + <string name="mode_fps_note">Ogranicza całą sesję. Wyświetlacz przełącza się na częstotliwość odświeżania będącą wielokrotnością limitu.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">Wyjście HDR10</string> + <string name="mode_hdr_unavailable">Niedostępne: %1$s.</string> + <string name="mode_hdr_restart">Uruchom ponownie aplikację, aby zastosować.</string> + <string name="mode_drivers">Sterowniki</string> + <string name="mode_gpu_drivers">Sterowniki GPU</string> + <string name="mode_gpu_drivers_hint">Wspólne dla Steam i pulpitu. Ustawiane na stronie Komponenty.</string> + <string name="mode_touch_controls">Dotyk i sterowanie</string> + <string name="mode_touch">Dotyk</string> + <string name="mode_touch_touchpad">Touchpad</string> + <string name="mode_touch_direct">Bezpośredni</string> + <string name="mode_touch_note">Auto: touchpad na pulpicie, bezpośredni w Steam.</string> + <string name="mode_osc">Sterowanie ekranowe</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">Auto pokazuje sterowanie, gdy nie jest podłączony żaden kontroler.</string> + <string name="osc_steam_touch">Dotyk Steam (beta)</string> + <string name="osc_style_next_session">Sterowanie dotykowe Steam zacznie działać od następnej sesji Steam</string> + <string name="osc_steam_layout">Układ dotykowy Steam</string> + <string name="osc_steam_layout_hint">Przesuwaj, zmieniaj rozmiar lub ukrywaj sterowanie dotykowe tej gry; zapisywane w Steam</string> + <string name="mode_controller">Kontroler</string> + <string name="mode_controller_hint">Jak Steam widzi Twój kontroler. Działa od następnej sesji.</string> + <string name="mode_controller_deck">Kontroler Steam Deck</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Kontroler Xbox 360</string> + <string name="mode_controller_note">Deck: przycisk szybkiego dostępu i żyroskop. Steam Controller: to samo oraz wykrywanie chwytu na tylnych przyciskach. DualShock 4 i DualSense: żyroskop, a gładziki działają jako ich touchpad. Xbox 360: szybki dostęp otwiera Guide+A.</string> + <string name="mode_back">Wstecz</string> + <string name="back_menu_then_qam">Menu → QAM</string> + <string name="back_qam_then_menu">QAM → Menu</string> + <string name="mode_session">Sesja</string> + <string name="mode_suspend">Działanie w tle</string> + <string name="mode_suspend_hint">Gdy aplikacja jest ukryta lub ekran się wyłącza.</string> + <string name="mode_suspend_manual">Ręcznie</string> + <string name="mode_suspend_native">Natywnie</string> + <string name="mode_suspend_steam_note">„Auto” wstrzymuje i wznawia po powrocie. „Natywnie” najpierw przygotowuje Steam do uśpienia. „Ręcznie” czeka na „Wznów”. „Nigdy” nie przerywa działania.</string> + <string name="mode_background_downloads">Nie przerywaj pobierania</string> + <string name="mode_background_downloads_hint">Opóźnia wstrzymanie w tle, gdy Steam aktywnie pobiera.</string> + <string name="mode_suspend_note">„Auto” wstrzymuje w tle i wznawia po powrocie. „Ręcznie” czeka na „Wznów”. „Nigdy” nie przerywa działania.</string> + <string name="mode_startup">Uruchamianie</string> + <string name="mode_steam_startup">Uruchamiaj Steam z DroidDeck</string> + <string name="mode_steam_startup_hint">Otwiera sesję Steam przy uruchomieniu DroidDeck.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">Sprawdzanie wydań…</string> + <string name="mode_decky_ready">Gotowe do instalacji: %1$s.</string> + <string name="mode_decky_none">Nie znaleziono zgodnej wersji. Spróbuj ponownie później.</string> + <string name="mode_decky_installed">Zainstalowano · %1$s</string> + <string name="mode_decky_update">Dostępna aktualizacja · %1$s</string> + <string name="mode_decky_current">Aktualna · %1$s</string> + <string name="mode_decky_loader">Program ładujący</string> + <string name="mode_decky_install_latest">Zainstaluj najnowszą</string> + <string name="mode_decky_on">Uruchamia się ze Steam. Gdy jest włączony, inne aplikacje na tym urządzeniu mogą sterować Steam.</string> + <string name="mode_decky_off">Wyłączony. Port zdalnego debugowania Steam pozostaje zamknięty.</string> + <string name="mode_decky_remove_title">Odinstalować Decky Loader?</string> + <string name="mode_decky_remove_text">Usuwa program ładujący. Twoje wtyczki i ustawienia zostaną zachowane.</string> + <string name="mode_client">Klient</string> + <string name="mode_mangoapp">Nakładka wydajności</string> + <string name="mode_mangoapp_hint">Wyłącz, jeśli gry ulegają awarii lub mają czarny ekran w trybie Deck. Działa od następnej sesji.</string> + <string name="mode_steam_repair">Napraw klienta Steam</string> + <string name="mode_steam_repair_hint">Pobiera klienta Steam ponownie przy następnym uruchomieniu Steam. Użyj, jeśli Steam utknie na ekranie wczytywania. Gry, logowanie i ustawienia zostaną zachowane.</string> + <string name="mode_steam_repair_queued">Klient Steam zostanie pobrany ponownie przy następnym uruchomieniu Steam.</string> + <string name="mode_steam_repair_button">Napraw</string> + <string name="mode_added_art">Grafiki</string> + <string name="mode_sgdb_key">Klucz API SteamGridDB</string> + <string name="mode_sgdb_key_yours">Twój klucz</string> + <string name="mode_sgdb_edit">Edytuj…</string> + <string name="mode_added_art_hint">Uzupełnia brakujące grafiki ze Steam, a potem z SteamGridDB. Pierwszeństwo mają Twoje pliki cover.jpg, hero.jpg, logo.png lub icon.png w folderze gry.</string> + <string name="summary_remove_game">Usuń %1$s</string> + <string name="mode_added_remove_confirm">Usunąć?</string> + <string name="games_add">Dodaj grę</string> + <string name="games_add_title">Dodaj grę:</string> + <string name="games_pick_exe_title">Wybierz plik exe:</string> + <string name="games_add_unreachable">Ta lokalizacja nie jest dostępna w sesji</string> + <string name="mode_added_exe_check">Sprawdź ten plik exe</string> + <string name="wincomp_title">Komponenty Windows</string> + <string name="wincomp_pad_hint">LB / RB przełączają między sekcjami.</string> + <string name="wincomp_card_none">Brak</string> + <string name="wincomp_applies">Biblioteki DLL Windows, których Proton nie zawiera, z listy komponentów. Włącz te, których potrzebuje ta gra: trafiają do własnego folderu Windows gry, nigdy do jej plików, i zostają niezależnie od wersji Proton, na której działa.</string> + <string name="wincomp_offline">Nie udało się pobrać listy komponentów. Już pobrane komponenty nadal można przełączać.</string> + <string name="wincomp_recommended">Zalecane dla tej gry</string> + <string name="wincomp_checking">Sprawdzanie, czego potrzebuje ta gra…</string> + <string name="wincomp_when_online">Dostępne po połączeniu z internetem</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">Włączanie %1$d komponentu…</item> + <item quantity="few">Włączanie %1$d komponentów…</item> + <item quantity="many">Włączanie %1$d komponentów…</item> + <item quantity="other">Włączanie %1$d komponentów…</item> + </plurals> + <string name="wincomp_auto_section">Włączone dla tej gry</string> + <string name="wincomp_auto_steam">Steam instaluje %1$s razem z %2$s (aplikacja %3$s)</string> + <string name="wincomp_auto_gog">Z listy GOG dla tej gry</string> + <string name="wincomp_auto_epic">Z wymagań wstępnych Epic dla tej gry</string> + <string name="wincomp_auto_amazon">Z kroków instalacji Amazon dla tej gry</string> + <string name="wincomp_steam_by_name">Steam instaluje %1$s z aplikacją %2$s, dopasowano po nazwie</string> + <string name="wincomp_suggestions">Również na liście</string> + <string name="wincomp_suggestion_reason">Wymienione przez %1$s; tutaj to nie jest komponent</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">Gra dostarcza %1$s</string> + <string name="wincomp_found_shipped">Gra ma %1$s (często działa bez zmian)</string> + <string name="wincomp_already_here">Już jest w tej grze</string> + <string name="wincomp_no_recommendation">Nic w folderze gry nie wskazuje na żaden komponent.</string> + <string name="wincomp_active">Zainstalowane dla tej gry (%1$d)</string> + <string name="wincomp_all">Wszystkie komponenty (%1$d)</string> + <string name="wincomp_installers">Wymagają instalatora Windows (%1$d)</string> + <string name="wincomp_installers_note">To osobne programy instalacyjne bez pakietu w środku, który można rozebrać, i jeszcze bez nagrania tego, co instalują. Większość to stare środowiska uruchomieniowe, które Mono w Proton już zastępuje; nagrania są dodawane, gdy powstają.</string> + <string name="wincomp_show">Pokaż</string> + <string name="wincomp_hide">Ukryj</string> + <string name="wincomp_needs_installer">Uruchamia własny instalator (jeszcze brak nagrania)</string> + <string name="wincomp_unsupported">Tutaj jeszcze nieobsługiwane</string> + <string name="wincomp_loading">Wczytywanie komponentów…</string> + <string name="wincomp_failed">Nie udało się zainstalować: %1$s</string> + <string name="wincomp_downloaded">Pobrano</string> + <string name="wincomp_downloading">Pobieranie %1$s</string> + <string name="wincomp_installing">Instalowanie: %1$s</string> + <string name="wincomp_reverting">Wyłączanie…</string> + <string name="wincomp_download_size">Do pobrania: %1$s</string> + <string name="wincomp_next_launch">Zacznie działać przy następnym uruchomieniu gry.</string> + <string name="mode_stretch169">Rozciągaj gry 16:9</string> + <string name="mode_stretch169_hint">Na ekranach bardziej kwadratowych niż 16:9 rozciąga gry na cały ekran zamiast pokazywać pasy. Działa od następnej sesji.</string> + <string name="mode_audio">Dźwięk</string> + <string name="mode_directaudio">DirectAudio dla gier</string> + <string name="mode_directaudio_hint">Omija PulseAudio, aby zmniejszyć opóźnienie.</string> + <string name="mode_mic">Mikrofon</string> + <string name="mode_mic_hint">Do czatu głosowego.</string> + <string name="mode_network">Sieć</string> + <string name="mode_wifi_discovery">Wykrywanie Wi-Fi</string> + <string name="mode_wifi_discovery_hint">Pokazuj w Steam nazwy Wi-Fi i sieci w pobliżu.</string> + <string name="mode_wifi_location_off">Usługi lokalizacyjne są wyłączone.</string> + <string name="mode_wifi_permission_denied">Nie przyznano uprawnienia do lokalizacji.</string> + <string name="mode_wifi_open_settings">Otwórz ustawienia</string> + <string name="mode_wifi_continue">Dalej</string> + <string name="mode_wifi_explain_title">Włączyć wykrywanie Wi-Fi?</string> + <string name="mode_wifi_explain_text">Android wymaga uprawnienia do lokalizacji, aby odczytywać nazwy Wi-Fi i wykrywać sieci w pobliżu. DroidDeck używa tego dostępu, aby pokazać te sieci w Steam. Nie śledzimy Twojej lokalizacji. Dostęp do internetu działa bez tego uprawnienia.</string> + <string name="mode_storage">Pamięć na gry</string> + <string name="mode_storage_auto">Automatycznie – karta SD, gdy jest, oraz Twoje foldery Games</string> + <string name="mode_storage_auto_none">Automatycznie – Twoje foldery Games oraz karta SD, gdy jest (teraz brak)</string> + <string name="mode_storage_internal">Tylko pamięć wewnętrzna</string> + <string name="mode_storage_folder">Folder: %1$s</string> + <string name="mode_second_library">Druga biblioteka</string> + <string name="mode_storage_note">Gry, które strumieniują zasoby z karty SD lub pamięci współdzielonej, mogą się zacinać.</string> + <string name="mode_storage_diagnostics">Diagnostyka pamięci</string> + <string name="mode_storage_diagnostics_hint">Zapisuje wyniki alokacji i próbkowane czasy dostępu do pamięci w „Udostępnij logi”. Po włączeniu może spowalniać instalacje. Działa od następnej sesji.</string> + <string name="mode_choose_folder">Wybierz folder…</string> + <string name="mode_custom_res_title">Własna rozdzielczość</string> + <string name="mode_custom_res_text">Zastępuje limit i proporcje. Przy rozmiarze niepasującym do ekranu pojawią się pasy.</string> + <string name="mode_width">Szerokość</string> + <string name="mode_height">Wysokość</string> + <string name="mode_custom_res_range">Od 320×240 do 3840×2160.</string> + <plurals name="mode_added_count"> + <item quantity="one">%1$d gra</item> + <item quantity="few">%1$d gry</item> + <item quantity="many">%1$d gier</item> + <item quantity="other">%1$d gry</item> + </plurals> + <string name="common_ok">OK</string> + <string name="common_delete">Usuń</string> + <string name="common_restore">Przywróć</string> + <string name="common_download">Pobierz</string> + <string name="common_delete_named">Usuń %1$s</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Sterownik środowiska</string> + <string name="comp_runtime_driver_hint">Używany przez Steam, gry i pulpit. Działa od następnej sesji.</string> + <string name="comp_import_turnip">Importuj plik zip Turnip…</string> + <string name="comp_display_driver">Sterownik wyświetlania</string> + <string name="comp_display_driver_hint">Rysuje klatki na ekranie w obu trybach. Uruchom ponownie aplikację, aby zastosować.</string> + <string name="comp_import_adrenotools">Importuj plik zip AdrenoTools…</string> + <string name="comp_never">nigdy</string> + <string name="comp_nightlies_checked">Wersje nightly sprawdzono: %1$s</string> + <string name="comp_title">Komponenty</string> + <string name="comp_about">Informacje o Komponentach</string> + <string name="comp_check_drivers">Sprawdź nowe sterowniki</string> + <string name="comp_check_nightlies">Szukaj nowych pakietów w wersjach nightly</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">Wybrany tutaj Proton uruchamia każdą grę, która nie ma własnego wyboru w Steam, a zmiana dokonana w Steam też się tu pojawia. Steam zastępuje Proton od Valve przy aktualizacji; oryginały są zachowywane dla każdej wersji.</string> + <string name="comp_scope">Konfiguracja</string> + <string name="comp_profile_configure">Skonfiguruj profil</string> + <string name="comp_profile">Profil</string> + <string name="comp_scope_general">Profil domyślny</string> + <string name="comp_scope_general_detail">Używany automatycznie przez gry bez własnych ustawień.</string> + <string name="comp_profile_game_detail">Zapisane ustawienia używane automatycznie przy uruchomieniu %1$s.</string> + <string name="comp_game_hint">Wybierz tylko ustawienia, których potrzebuje ta gra. Cała reszta nadal korzysta z profilu domyślnego.</string> + <string name="comp_game_fex">Preset FEX</string> + <string name="comp_game_fex_inherited">Używany jest preset z profilu domyślnego.</string> + <string name="comp_game_fex_override">Zapisano własny preset dla tej gry.</string> + <string name="comp_game_fex_custom">Własne zmienne FEX</string> + <string name="comp_game_use_general">Użyj domyślnego</string> + <string name="comp_game_compatibility">Zgodność gry</string> + <string name="comp_game_environment_hint">Opcje środowiska Proton, DXVK, VKD3D i synchronizacji dla tej gry.</string> + <string name="comp_game_proton_inherited">Używany jest Proton z profilu domyślnego.</string> + <string name="comp_game_proton_override">Zapisano własny Proton dla tej gry przez mapowanie zgodności Steam.</string> + <string name="comp_no_proton_short">Brak Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Używa pakietu z profilu domyślnego z %1$s, chyba że ta gra wybierze inny pakiet.</string> + <string name="comp_game_component_general">Używa pakietu komponentów z profilu domyślnego.</string> + <string name="comp_game_component_override">Używa %1$s tylko w tej grze.</string> + <string name="comp_game_runtime_options">Opcje środowiska uruchomieniowego</string> + <string name="comp_game_environment_other_hint">Inne opcje środowiska Proton, DXVK, VKD3D-Proton i synchronizacji dla poszczególnych gier.</string> + <string name="comp_game_saved_next">Profil gry %1$s zapisano na następne uruchomienie gry.</string> + <string name="game_no_steam_title">Uruchom bez Steam</string> + <string name="game_no_steam_detail">Dla gry, która zamyka się od razu: uruchamia ją bez konfiguracji Steam w Protonie. Funkcje Steam, takie jak nakładka, są dla niej wyłączone.</string> + <string name="game_no_steam_on">Wł.</string> + <string name="comp_game_proton_live">Wybór Proton dla %1$s zapisano i jest już dostępny w Steam.</string> + <string name="comp_game_proton_next">Wybór Proton dla %1$s zapisano. Uruchom ponownie Steam przed uruchomieniem gry.</string> + <string name="comp_game_proton_not_runnable">%1$s nie może używać wybranego Proton na tym urządzeniu.</string> + <string name="comp_game_load_failed">Nie udało się wczytać profilu gry %1$s: %2$s</string> + <string name="comp_game_save_failed">Nie udało się zapisać profilu gry: %1$s</string> + <string name="comp_game_reset">Resetuj profil gry</string> + <string name="comp_game_reset_title">Zresetować profil gry %1$s?</string> + <string name="comp_game_reset_body">Usunąć ustawienia Proton, FEX, pakietu komponentów, środowiska, filtrowania tekstur i komponentów Windows dla tej gry? Będzie ona używać profilu domyślnego.</string> + <string name="comp_game_reset_done">%1$s używa teraz profilu domyślnego.</string> + <string name="comp_game_reset_failed">Nie udało się całkowicie zresetować profilu gry: %1$s</string> + <string name="comp_default_live">%1$s uruchamia teraz każdą grę, która nie ma własnego Proton wybranego w Steam.</string> + <string name="comp_default_next">%1$s będzie uruchamiać każdą grę bez własnego Proton od następnego uruchomienia Steam.</string> + <string name="comp_default_not_runnable">%1$s nie może tu uruchamiać gier, więc nie może być domyślny.</string> + <string name="comp_default_failed">Nie udało się zapisać domyślnego Proton: %1$s</string> + <string name="comp_swapped">%1$s · podmienione: %2$d</string> + <string name="comp_all_original">%1$s · wszystko oryginalne</string> + <string name="comp_game_running_suffix">%1$s · gra działa</string> + <string name="comp_esync_pack">%1$s · pakiet droiddeck-esync</string> + <string name="comp_esync_wanted">%1$s · brak pakietu droiddeck-esync</string> + <string name="comp_esync_builtin">%1$s · wbudowany esync, pakiet zbędny</string> + <string name="comp_gpu_drivers">Sterowniki GPU</string> + <string name="comp_reapplied">%1$s · ponownie zastosowano przy uruchomieniu %2$s</string> + <string name="comp_next_queued">Następnie: %1$s · po zamknięciu gry</string> + <string name="comp_in_use">%1$s w użyciu: %2$s</string> + <string name="comp_waits">Czeka na zamknięcie gry</string> + <string name="comp_applies_next_game">Działa od następnego uruchomienia gry</string> + <string name="comp_import_zip">Importuj .zip</string> + <string name="comp_reading">Odczytywanie wersji Proton…</string> + <string name="comp_no_proton">Nie zainstalowano jeszcze żadnego Proton. Uruchom raz Steam, aby pobrać jego Proton ARM64, lub dodaj GE-Proton, CachyOS albo DroidDeck-Proton w sekcji Konfiguracja.</string> + <string name="comp_original">Oryginał · %1$s</string> + <string name="comp_this_build">%1$s · ta wersja</string> + <string name="comp_earlier_build">%1$s · wcześniejsza wersja</string> + <string name="comp_installed_count">Zainstalowane · %1$d</string> + <string name="comp_nothing_stored">Nic jeszcze nie zapisano dla %1$s. Własne pliki tego Proton są zapisywane jako jego oryginał.</string> + <string name="comp_waits_long">Na tym Proton działa gra, więc zmiana poczeka do jej zamknięcia.</string> + <string name="comp_applies_long">Zacznie działać przy następnym uruchomieniu gry.</string> + <string name="comp_swap_title">Podmienić %1$s?</string> + <string name="comp_swap_body">Umieścić %1$s w %2$s? Jego oryginalne pliki pozostaną zapisane. %3$s</string> + <string name="comp_restore_title">Przywrócić %1$s?</string> + <string name="comp_restore_body">Przywrócić oryginał %1$s w %2$s? %3$s</string> + <string name="comp_delete_package_body">Usuwa pakiet z aplikacji. Możesz pobrać go ponownie.</string> + <string name="comp_delete_original_title">Usunąć ten oryginał?</string> + <string name="comp_delete_original_body">Ten oryginał %1$s pochodzi z wcześniejszej wersji %2$s. Oryginał bieżącej wersji jest zawsze zachowywany.</string> + <string name="comp_nightlies_count">W wersjach nightly · %1$d</string> + <string name="comp_refresh_to_list">Odśwież, aby wyświetlić pakiety w wersjach nightly.</string> + <string name="comp_nothing_new">Nic nowego od ostatniego sprawdzenia.</string> + <string name="comp_import_wcp">Importuj .wcp</string> + <string name="comp_restore_original">Przywróć oryginał tego Proton</string> + <string name="comp_restore_original_body">Przywrócić oryginalny %2$s w %1$s?</string> + <string name="comp_swap">Podmień</string> + <string name="comp_about_text">FEX, DXVK i VKD3D-Proton dla każdej wersji Proton, z którą Steam uruchamia gry.\n\nWybierz zainstalowany wiersz, aby go podmienić. Zmiana działa od następnego uruchomienia gry lub po zamknięciu działającej gry.\n\nTwoje wybory są sprawdzane przed każdym uruchomieniem i przywracane, jeśli coś je zmieniło. Oryginalne pliki każdej wersji Proton są zachowywane, więc aktualizacje Steam nigdy ich nie usuwają.</string> + <string name="comp_tag_original">Oryginał</string> + <string name="comp_tag_in_use">W użyciu</string> + <string name="comp_tag_new">Nowy</string> + <string name="comp_tag_stored">Zapisany</string> + <string name="perf_title">Wydajność</string> + <string name="perf_lede">Rdzenie CPU, poprawki sesji i nazwa hosta. Działa od następnej sesji.</string> + <string name="perf_client_cores_group">Rdzenie klienta Steam</string> + <string name="perf_override">Zastąp wybór rdzeni przez Steam</string> + <string name="perf_override_hint">Przypina Steam, jego pomocnika interfejsu i gamescope do rdzeni poniżej.</string> + <string name="perf_client_cores">Rdzenie klienta</string> + <string name="perf_client_cores_on">Rdzenie dla Steam.</string> + <string name="perf_client_cores_off">Włącz zastępowanie, aby wybrać.</string> + <string name="perf_game_cores_group">Rdzenie gier</string> + <string name="perf_game_cores">Rdzenie gier</string> + <string name="perf_game_cores_hint">Rdzenie dla gier. Gdy wszystkie są zaznaczone, Android swobodnie przydziela zadania.</string> + <string name="perf_client_ui">Interfejs klienta</string> + <string name="perf_glthread">Wielowątkowy GL</string> + <string name="perf_glthread_hint">Może poprawić responsywność menu Steam.</string> + <string name="perf_zink">Zink: leniwe deskryptory</string> + <string name="perf_zink_hint">Zalecane dla sterowników bez buforów deskryptorów.</string> + <string name="perf_noglerror">Pomijaj sprawdzanie błędów GL</string> + <string name="perf_noglerror_hint">Wyłącza walidację GL przy każdym wywołaniu.</string> + <string name="perf_gsrealtime">gamescope: kolejka GPU czasu rzeczywistego</string> + <string name="perf_gsrealtime_hint">Nadaje priorytet pracy GPU kompozytora. Może wygładzić tempo klatek kosztem części wydajności gier.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">Utrzymuj GPU na najwyższym taktowaniu</string> + <string name="perf_gpuclock_hint">Tylko Adreno. Mniej przycięć, większe zużycie baterii i więcej ciepła.</string> + <string name="perf_fixes">Poprawki sesji</string> + <string name="perf_sysmem">Turnip: renderowanie sysmem</string> + <string name="perf_sysmem_hint">Szybsze dla Steam i większości gier. Wymagane na Adreno 710/720/722.</string> + <string name="perf_xalia">Pomijaj pomocnika xalia w Steam</string> + <string name="perf_fastsync">droiddeck-ntsync (eksperymentalne)</string> + <string name="perf_fastsync_hint">Zastępnik sterownika Linux ntsync w przestrzeni użytkownika, do testów. Gdy jest włączony, Proton używa go zamiast droiddeck-esync i droiddeck-fsync. Zgodny z Proton Experimental ARM64, GE-Proton i proton-cachyos. Działa od następnej sesji.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">Najpierw droiddeck-fsync</string> + <string name="perf_fsync_hint">Uruchamia własny fsync Proton na standardowych plikach Proton, z futex_waitv dostarczanym w przestrzeni użytkownika, więc nie wymaga pakietu i działa od razu po aktualizacji Proton przez Steam. Gdy jest włączony, gry używają go przed droiddeck-esync, z wyjątkiem nielicznych, którym Proton wyłącza fsync. Działa od następnej sesji.</string> + <string name="perf_esync_hint">Wątki Windows w grach czekają na siebie przez eventfd i pamięć współdzieloną zamiast przez wymiany z wineserver. Używa pakietu droiddeck-esync przygotowanego dla dokładnie tej wersji Proton; pakiety są pobierane automatycznie. Dopóki nie ma pakietu dla nowej wersji Proton, zastępuje go droiddeck-fsync. Działa od następnej sesji.</string> + <string name="perf_xalia_hint">Wyłącza pomocnika Proton do nawigacji kontrolerem, który zużywa CPU w każdej grze.</string> + <string name="perf_seccomp">Uruchamiaj proot bez seccomp</string> + <string name="perf_seccomp_hint">Może naprawić błędy brakujących wywołań systemowych na niektórych jądrach. Może obniżyć wydajność.</string> + <string name="perf_fastpath">Szybka ścieżka proot</string> + <string name="perf_fastpath_needs_seccomp">Wymaga filtra seccomp proot.</string> + <string name="perf_fastpath_hint">Szybsze wyszukiwanie plików i mniej przycięć. Wyłącz, jeśli program nie może znaleźć swoich plików.</string> + <string name="perf_identity">Tożsamość sesji</string> + <string name="perf_hostname">Nazwa hosta</string> + <string name="perf_hostname_hint">Nazwa tego urządzenia widoczna dla Steam. Puste pole przywraca %1$s.</string> + <string name="perf_hostname_invalid">Tylko litery, cyfry i łączniki w środku, do 63 znaków.</string> + <string name="perf_phantom_warning">Android jest ustawiony tak, że zamknie tę sesję</string> + <string name="driver_installed">Zainstalowane</string> + <string name="driver_available">Do pobrania</string> + <string name="driver_nothing_new">Nic nowego od ostatniego sprawdzenia.</string> + <string name="driver_restore_builtin">Przywróć wbudowane sterowniki</string> + <string name="driver_delete_builtin">Wbudowany w aplikację: usuwa to jego rozpakowane pliki i go ukrywa. „Przywróć wbudowane sterowniki” przywraca go z powrotem.</string> + <string name="driver_delete_bundle">Połowa pakietu Android + Linux: obie połowy zostaną usunięte. Zamiast używanego sterownika zostanie użyty domyślny.</string> + <string name="driver_delete_plain">Jego pliki zostaną usunięte z aplikacji.</string> + <string name="driver_delete_in_use">Jego pliki zostaną usunięte. Jest w użyciu, więc zastąpi go domyślny.</string> + <string name="drawer_page_display">Wyświetlacz</string> + <string name="drawer_page_effects">Efekty</string> + <string name="drawer_page_controls">Sterowanie</string> + <string name="drawer_page_components">Komponenty</string> + <string name="drawer_page_controller">Kontroler</string> + <string name="drawer_page_settings">Ustawienia</string> + <string name="drawer_page_desc">Strona: %1$s</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Pulpit</string> + <string name="drawer_steam_menu">Menu Steam</string> + <string name="drawer_quick_access">Szybki dostęp</string> + <string name="drawer_open_qam">Otwórz menu szybkiego dostępu</string> + <string name="drawer_prev_page">Poprzednia strona</string> + <string name="drawer_next_page">Następna strona</string> + <string name="drawer_hud">HUD wydajności</string> + <string name="drawer_scaling">Tryb skalowania</string> + <string name="drawer_scaling_sharpness">Ostrość skalowania</string> + <string name="drawer_effects">Efekty ekranowe</string> + <string name="drawer_effects_hint">Na żywo. Zużywa trochę czasu GPU, gdy włączony jest dowolny efekt.</string> + <string name="drawer_look">Presety</string> + <string name="drawer_look_custom">Własny</string> + <string name="drawer_effects_advanced">Zaawansowane</string> + <string name="widgets_expanded">Rozwinięte</string> + <string name="widgets_collapsed">Zwinięte</string> + <string name="drawer_cas">Wyostrzanie (CAS)</string> + <string name="drawer_cas_level">Poziom wyostrzania</string> + <string name="drawer_fake_hdr">Sztuczne HDR</string> + <string name="drawer_deband">Usuwanie bandingu</string> + <string name="drawer_deband_strength">Siła usuwania bandingu</string> + <string name="drawer_brightness">Jasność</string> + <string name="drawer_contrast">Kontrast</string> + <string name="drawer_gamma">Gamma</string> + <string name="drawer_saturation">Nasycenie</string> + <string name="drawer_fxaa">FXAA (wygładzanie krawędzi)</string> + <string name="drawer_toon">Kontury kreskówkowe</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Filtrowanie tekstur</string> + <string name="drawer_texture_hint">Gry DirectX 9–11, od następnego uruchomienia.</string> + <string name="drawer_anisotropy">Filtrowanie anizotropowe</string> + <string name="drawer_texture_sharpness">Ostrość tekstur</string> + <string name="drawer_texture_sharpness_note">Ujemny bias mipmap: ostrzejsze tekstury, trochę migotania. Auto dopasowuje się do tego, jak bardzo sesja jest powiększona na ekranie.</string> + <string name="drawer_touch_auto">Auto (%1$s)</string> + <string name="drawer_keyboard">Klawiatura</string> + <string name="drawer_pc_keyboard">Klawiatura PC</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">Drugi ekran</string> + <string name="drawer_second_screen_mode">Tryb</string> + <string name="drawer_second_screen_shows">Pokazuje</string> + <string name="drawer_android_apps">Aplikacje Android</string> + <string name="drawer_launch_app">Uruchom aplikację</string> + <string name="common_hide">Ukryj</string> + <string name="common_show">Pokaż</string> + <string name="drawer_no_apps">Brak aplikacji do uruchomienia</string> + <string name="drawer_session_behavior">Działanie sesji</string> + <string name="drawer_suspend_hint">Działa od teraz i w przyszłych sesjach w tym trybie.</string> + <string name="drawer_next_session">Następna sesja</string> + <string name="drawer_support">Pomoc</string> + <string name="drawer_logs">Logi sesji</string> + <string name="drawer_logs_hint">Dołącz do zgłoszenia błędu</string> + <string name="drawer_share_logs">Udostępnij logi</string> + <string name="drawer_background">Przenieś w tło</string> + <string name="drawer_stop">Zatrzymaj</string> + <string name="drawer_stop_title">Zatrzymać sesję?</string> + <string name="drawer_running_now">Teraz działa</string> + <string name="drawer_no_game">Żadna gra nie działa teraz przez Proton</string> + <string name="drawer_selected_profile">Profil gry</string> + <string name="drawer_selected_profile_none">Szczegóły profilu nie są jeszcze dostępne.</string> + <string name="drawer_profile_refresh_failed_initial">Nie udało się wczytać profili gier. Automatyczne ponawianie.</string> + <string name="drawer_profile_refresh_failed_stale">Nie udało się odświeżyć profili gier. Wyświetlane są ostatnie dostępne dane; automatyczne ponawianie.</string> + <string name="drawer_profile_inspect_hint">Wyświetl i edytuj zapisane ustawienia używane przy następnym uruchomieniu.</string> + <string name="drawer_profile_default">Używany jest profil domyślny</string> + <string name="drawer_profile_custom">Własny profil</string> + <string name="drawer_component_inherited">Używany jest %1$s z profilu domyślnego.</string> + <string name="drawer_game_components_note">Zmiany są zapisywane w profilu gry %1$s i działają od następnego uruchomienia gry.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d nadpisanie gry</item> + <item quantity="few">%1$d nadpisania gry</item> + <item quantity="many">%1$d nadpisań gry</item> + <item quantity="other">%1$d nadpisania gry</item> + </plurals> + <string name="drawer_no_proton">Nie zainstalowano jeszcze żadnego Proton.</string> + <string name="drawer_game_running">gra działa</string> + <string name="drawer_reapplied">ponownie zastosowano przy uruchomieniu %1$s</string> + <string name="drawer_original">Oryginał</string> + <string name="drawer_components_note">Importowanie i usuwanie na stronie Komponenty.</string> + <string name="setup_check_done">Gotowe</string> + <string name="setup_check_warn">Wymaga uwagi</string> + <string name="setup_check_busy">W toku</string> + <string name="setup_limit_adb_failed">Sprawdź adres IP i port w sekcji Debugowanie bezprzewodowe lub sparuj ponownie, jeśli Android usunął to urządzenie.</string> + <string name="setup_install">Zainstaluj</string> + <string name="setup_manage">Zarządzaj</string> + <string name="setup_tab_overview">Przegląd</string> + <string name="setup_tab_controller">Kontroler</string> + <string name="setup_tab_session">Sesja</string> + <string name="setup_tab_launcher">Aplikacja</string> + <string name="setup_title">Konfiguracja</string> + <string name="setup_all_set">● Wszystko gotowe</string> + <string name="setup_n_ready">Gotowe: %1$d z %2$d</string> + <string name="setup_system_check">Sprawdzenie systemu</string> + <string name="setup_gpu_supported">Urządzenie obsługiwane</string> + <string name="setup_gpu_untested">Nieprzetestowany GPU</string> + <string name="setup_gpu_unsupported">GPU nieobsługiwany</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">Steam wymaga sterownika Adreno; %1$s może wyświetlać czarny ekran</string> + <string name="setup_runtime">Środowisko Linux</string> + <string name="setup_runtime_progress">%1$s · %2$d%%</string> + <string name="setup_runtime_missing">Nie zainstalowano · ok. 3 GB, instalowane przy pierwszym „Graj”</string> + <string name="setup_installed">Zainstalowano</string> + <string name="setup_runtime_update">%1$s · dostępna aktualizacja</string> + <string name="setup_runtime_current">%1$s · aktualne</string> + <string name="setup_limit">Limit procesów podrzędnych</string> + <string name="setup_limit_on">Wł. · Android może zamykać Steam. Wyłączenie zajmuje minutę.</string> + <string name="setup_limit_unset">Nieustawiony · Android może zamykać Steam. Wyłączenie zajmuje minutę.</string> + <string name="setup_limit_unknown">Nie udało się sprawdzić · wyłączenie zajmuje minutę.</string> + <string name="setup_fix_it">Napraw</string> + <string name="setup_details">Szczegóły</string> + <string name="setup_dev_options">Opcje programisty</string> + <string name="setup_use_wireless">Użyj debugowania bezprzewodowego</string> + <string name="setup_check_again">Sprawdź ponownie</string> + <string name="setup_limit_turn_on">Włącz limit</string> + <string name="setup_account">Konto Steam</string> + <string name="setup_signed_in_offline">Zalogowano jako %1$s · tryb offline</string> + <string name="setup_signed_in">Zalogowano jako %1$s</string> + <string name="setup_sign_in">Naciśnij „Graj” i zaloguj się do Steam</string> + <string name="setup_tools">Narzędzia</string> + <string name="setup_controller">Kontroler</string> + <string name="setup_controller_unavailable">Ustawienia kontrolera są niedostępne.</string> + <string name="setup_session">Sesja</string> + <string name="setup_logs">Logi sesji</string> + <string name="setup_logs_hint">Zapisywane po każdej sesji</string> + <string name="setup_agent_commands">Narzędzia debugowania</string> + <string name="setup_latest_logs">Najnowsze logi sesji</string> + <string name="setup_saved_logs">Zapisane logi sesji</string> + <string name="setup_saved_logs_hint">Zachowuje najnowsze: %1$d</string> + <string name="setup_clear_logs">Wyczyść logi</string> + <string name="setup_offline">Tryb offline</string> + <string name="setup_sign_in_first">Najpierw zaloguj się do Steam</string> + <string name="setup_launcher">Aplikacja</string> + <string name="setup_language">Język</string> + <string name="setup_language_hint">Steam też go używa, od następnego uruchomienia.</string> + <string name="setup_language_system">Domyślny systemu (%1$s)</string> + <string name="setup_language_system_short">Domyślny systemu</string> + <string name="setup_app_scale">Skala aplikacji</string> + <string name="setup_app_scale_hint">Tekst i elementy sterujące DroidDeck</string> + <string name="setup_app_scale_default">%1$d%% (domyślnie)</string> + <string name="setup_theme">Motyw</string> + <string name="setup_theme_hint">Wygląd aplikacji</string> + <string name="setup_home">Użyj jako ekranu głównego</string> + <string name="setup_home_on">DroidDeck może być aplikacją ekranu głównego</string> + <string name="setup_home_off">DroidDeck nigdy nie jest proponowany jako aplikacja ekranu głównego</string> + <string name="setup_orientation">Orientacja</string> + <string name="setup_orientation_auto">Automatyczna pozioma</string> + <string name="setup_orientation_landscape">Pozioma</string> + <string name="setup_orientation_reverse">Odwrócona pozioma</string> + <string name="setup_fullscreen">Pełny ekran</string> + <string name="setup_fullscreen_on">Ukrywa systemowe paski stanu i nawigacji</string> + <string name="setup_fullscreen_off">Pokazuje systemowe paski stanu i nawigacji</string> + <string name="setup_default_home">Domyślna aplikacja ekranu głównego</string> + <string name="setup_choose_home">Wybierz aplikację ekranu głównego</string> + <string name="setup_choose">Wybierz</string> + <string name="setup_linux_apps">Aplikacje Linux (beta)</string> + <string name="setup_tool_files">Pliki</string> + <string name="setup_tool_files_hint">Przeglądaj pliki i zarządzaj nimi</string> + <string name="setup_tool_protons">Wersje Proton</string> + <string name="setup_tool_protons_hint">Instaluj wersje Proton ARM64</string> + <string name="setup_tool_performance">Wydajność</string> + <string name="setup_tool_performance_hint">Przydział rdzeni CPU</string> + <string name="setup_tool_roms">Folder ROM-ów</string> + <string name="setup_tool_roms_hint">Miejsce przechowywania gier do emulatorów</string> + <string name="setup_card_components">Komponenty</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Sterowanie</string> + <string name="setup_card_controls_hint">Mapowanie przycisków</string> + <string name="store_tab_discover">Odkrywaj</string> + <string name="store_tab_search">Szukaj</string> + <string name="store_tab_installed">Zainstalowane</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Sklep</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak gotowy</string> + <string name="store_needs_runtime">Najpierw zainstaluj środowisko Linux w sekcji Konfiguracja.</string> + <string name="store_setup_title">Skonfiguruj Flatpak</string> + <string name="store_setup_text">Dodaje Flatpak i łączy go z Flathub (ok. 20 MB). Pierwsza aplikacja pobiera też swoje środowisko uruchomieniowe.</string> + <string name="store_setting_up">Konfigurowanie…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Aktualizacje</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d%%</string> + <string name="store_starting">Uruchamianie…</string> + <string name="store_categories">Kategorie</string> + <string name="store_unreachable">Nie udało się połączyć z Flathub. Sprawdź połączenie i spróbuj ponownie.</string> + <string name="store_try_again">Spróbuj ponownie</string> + <string name="store_search_label">Szukaj w Flathub</string> + <string name="store_search_placeholder">Firefox, gry, emulatory…</string> + <string name="store_search">Szukaj</string> + <string name="store_all">Wszystkie</string> + <string name="store_searching">Wyszukiwanie…</string> + <string name="store_search_hint">Szukaj po nazwie lub wybierz kategorię. Wyświetlane są tylko aplikacje ARM64.</string> + <string name="store_no_results">Brak pasujących aplikacji ARM64 w Flathub.</string> + <string name="store_results">Wyniki</string> + <string name="store_check_updates">Sprawdź aktualizacje</string> + <string name="store_update_all">Aktualizuj wszystkie (%1$d)</string> + <string name="store_all_current">● Wszystko jest aktualne</string> + <string name="store_no_apps">Brak aplikacji. Znajdź je w sekcji Odkrywaj lub Szukaj.</string> + <string name="store_setup_first_note">Skonfiguruj Flatpak, aby instalować aplikacje.</string> + <string name="store_update_available_prefix">Dostępna aktualizacja · %1$s</string> + <string name="store_open">Otwórz</string> + <string name="store_details">Szczegóły</string> + <string name="store_remove">Usuń</string> + <string name="store_press_again">Naciśnij ponownie, aby usunąć</string> + <string name="store_installing">Instalowanie…</string> + <string name="store_install">Zainstaluj</string> + <string name="store_setup_first">Najpierw skonfiguruj Flatpak</string> + <string name="store_download_size">Do pobrania: %1$s</string> + <string name="store_installed_chip">● Zainstalowano</string> + <string name="store_details_failed">Nie udało się wczytać strony tej aplikacji w Flathub.</string> + <string name="store_loading">Wczytywanie z Flathub…</string> + <string name="store_no_arm">Brak wersji ARM64 w Flathub, więc nie można jej tu zainstalować.</string> + <string name="store_screenshots">Zrzuty ekranu</string> + <string name="store_about">Informacje</string> + <string name="store_app_id">ID aplikacji</string> + <string name="store_license">Licencja</string> + <string name="store_runtime">Środowisko uruchomieniowe</string> + <string name="store_installed_size">Rozmiar po instalacji</string> + <string name="store_installed_size_value">%1$s (plus współdzielone środowisko uruchomieniowe)</string> + <string name="store_website">Strona internetowa</string> + <string name="store_verified">Zweryfikowano</string> + <string name="upd_title">Aktualizacje</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Sprawdzono %1$s</string> + <string name="upd_not_checked">Jeszcze nie sprawdzono</string> + <string name="upd_perm_title">Pozwól DroidDeck się aktualizować</string> + <string name="upd_perm_text">Na następnym ekranie włącz „Zezwól z tego źródła” i wróć tutaj. Aktualizacja będzie kontynuowana sama.</string> + <string name="upd_open_settings">Otwórz ustawienia</string> + <string name="upd_not_now">Nie teraz</string> + <string name="upd_signed_status">Inny podpis</string> + <string name="upd_signed_headline">Nie można zaktualizować w miejscu</string> + <string name="upd_signed_detail">Ta kopia nie jest podpisana kluczem wydań DroidDeck, więc aktualizacje nie mogą jej zastąpić. Odinstaluj ją, aby przełączyć.</string> + <string name="upd_available">Dostępna aktualizacja</string> + <string name="upd_blocked_status">Nie można zainstalować</string> + <string name="upd_switch">Gotowe do przełączenia</string> + <string name="upd_ahead_status">Przed Stable</string> + <string name="upd_ahead_headline">Jesteś przed wersją Stable</string> + <string name="upd_ahead_detail">Ta kompilacja jest nowsza niż najnowsza Stable. Przejdziesz na Stable przy jej następnym wydaniu.</string> + <string name="upd_gone_status">Test zakończony</string> + <string name="upd_gone_headline">Ten test się zakończył</string> + <string name="upd_gone_detail">Jego poprawka została scalona albo odrzucona. Śledź Preview, aby mieć najnowsze poprawki.</string> + <string name="upd_current_status">Aktualne</string> + <string name="upd_current_headline">Masz najnowszą wersję %1$s</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Aktualizuj%1$s</string> + <string name="upd_install_button">Instaluj%1$s</string> + <string name="upd_install_anyway">Zainstaluj Stable %1$s mimo to</string> + <string name="upd_follow_preview">Śledź Preview</string> + <string name="upd_stop_session">Zatrzymaj działającą sesję, aby zaktualizować.</string> + <string name="upd_less">Mniej</string> + <string name="upd_more">Więcej</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Nowa kompilacja Preview</string> + <string name="upd_new_test">Kompilacja testowa PR #%1$d</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">KANAŁ AKTUALIZACJI</string> + <string name="upd_stable">Stable</string> + <string name="upd_stable_hint">Przetestowane wydania, dla większości osób</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Preview</string> + <string name="upd_preview_hint">Najnowsze poprawki, zanim trafią do Stable</string> + <string name="upd_preview_current">Aktualne</string> + <string name="upd_tests">Kompilacje testowe</string> + <string name="upd_tests_hint">Wypróbuj poprawkę przed jej wydaniem</string> + <string name="upd_tests_none">Obecnie żadnych</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Nie zbudowano dla tej kopii DroidDeck</string> + <string name="upd_updated">Zaktualizowano %1$s</string> + <string name="upd_test_name">test PR #%1$d</string> + <string name="upd_while_ago">jakiś czas temu</string> + <string name="upd_just_now">przed chwilą</string> + <plurals name="upd_tests_count"> + <item quantity="one">%1$d do wypróbowania</item> + <item quantity="few">%1$d do wypróbowania</item> + <item quantity="many">%1$d do wypróbowania</item> + <item quantity="other">%1$d do wypróbowania</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="one">Zmiany od Twojej kompilacji · %1$d kompilacja wstecz</item> + <item quantity="few">Zmiany od Twojej kompilacji · %1$d kompilacje wstecz</item> + <item quantity="many">Zmiany od Twojej kompilacji · %1$d kompilacji wstecz</item> + <item quantity="other">Zmiany od Twojej kompilacji · %1$d kompilacji wstecz</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="one">Niedawne zmiany · %1$d kompilacja</item> + <item quantity="few">Niedawne zmiany · %1$d kompilacje</item> + <item quantity="many">Niedawne zmiany · %1$d kompilacji</item> + <item quantity="other">Niedawne zmiany · %1$d kompilacji</item> + </plurals> + <string name="gate_title">Jedno ustawienie Androida do zmiany</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Android zatrzymuje aplikacje, które uruchamiają wiele procesów w tle, a Steam uruchamia ich kilkadziesiąt. Dopóki to się nie zmieni, gry zamykają się bez błędu.</string> + <string name="gate_fix_for_me">Napraw to za mnie</string> + <string name="gate_toggle_steps">Włącz „Wyłącz ograniczenia procesów podrzędnych” i wróć tutaj. Zostaw włączone Opcje programisty, bo inaczej przełącznik znowu się wyłączy.</string> + <string name="gate_open_dev">Otwórz Opcje programisty</string> + <string name="gate_no_toggle">Ta wersja Androida nie ma dla tego przełącznika, więc DroidDeck zmienia go przez debugowanie bezprzewodowe. Zajmuje to około minuty, bez potrzeby komputera.</string> + <string name="gate_needs_wifi">Debugowanie bezprzewodowe wymaga Wi-Fi. Działa każda sieć; nie musi mieć internetu.</string> + <string name="gate_needs_notifications">Kod parowania pojawia się w powiadomieniu. Zezwól DroidDeck na powiadomienia albo użyj sekcji „Inne sposoby”.</string> + <string name="gate_allow_notifications">Zezwól na powiadomienia</string> + <string name="gate_step1">Otwórz Ustawienia → Opcje programisty → Debugowanie bezprzewodowe i włącz je</string> + <string name="gate_step2">Dotknij „Sparuj urządzenie za pomocą kodu parowania”</string> + <string name="gate_step3">Wpisz kod w powiadomieniu DroidDeck</string> + <string name="gate_waiting">Czekanie na okienko parowania…</string> + <string name="gate_code_needed">Znaleziono okienko. Wpisz jego kod w powiadomieniu.</string> + <string name="gate_status">Stan</string> + <string name="gate_polling">Sprawdzanie co 2 sekundy. Ta strona zamknie się, gdy przełącznik będzie wyłączony.</string> + <string name="gate_other_ways">Inne sposoby</string> + <string name="gate_other_toggle">Debugowanie bezprzewodowe, adres ręczny, komputer z ADB</string> + <string name="gate_other_no_toggle">Adres ręczny, komputer z ADB, Opcje programisty</string> + <string name="gate_use_wireless_instead">Użyj za to debugowania bezprzewodowego</string> + <string name="gate_some_roms">Niektóre ROM-y dodają ten przełącznik mimo to. Poszukaj „child process” w Opcjach programisty.</string> + <string name="gate_manual_hint">Jeśli powiadomienie nie działa, wpisz adres i kod z okienka samodzielnie.</string> + <string name="gate_enter_manually">Wpisz adres ręcznie</string> + <string name="gate_from_computer">Z komputera z ADB:</string> + <string name="gate_copy_command">Kopiuj polecenie</string> + <string name="gate_choose_display">Wybierz, na którym ekranie otworzyć Ustawienia Androida.</string> + <string name="gate_main_screen">Ekran główny</string> + <string name="wadb_finding_port">Sparowano. Wyszukiwanie portu debugowania bezprzewodowego…</string> + <string name="wadb_enter_port">Sparowano. Wpisz port z Ustawień.</string> + <string name="wadb_applying">Znaleziono urządzenie. Stosowanie ustawienia…</string> + <string name="wadb_apply_failed">Nie udało się zastosować automatycznie. Sprawdź adres poniżej i spróbuj ponownie. %1$s</string> + <string name="wadb_title_wireless">Debugowanie bezprzewodowe</string> + <string name="wadb_title_done">Limit procesów podrzędnych zmieniony</string> + <string name="wadb_title">Zmień limit procesów podrzędnych</string> + <string name="wadb_lede_compact0">Jeśli opcja jest niedostępna, sparuj przez debugowanie bezprzewodowe.</string> + <string name="wadb_lede_compact1">Sparowano. Sprawdź adres IP i port w Debugowaniu bezprzewodowym.</string> + <string name="wadb_lede_done">Limit procesów podrzędnych został zmieniony.</string> + <string name="wadb_lede0">Użyj kroku 2, jeśli Opcje programisty nie mają ustawienia procesów podrzędnych. Otwórz Opcje programisty → Debugowanie bezprzewodowe → Sparuj urządzenie za pomocą kodu parowania.</string> + <string name="wadb_lede1">Sparowano. Użyj adresu poniżej lub zaktualizuj go z Debugowanie bezprzewodowe → Adres IP i port.</string> + <string name="wadb_lede_done_long">Limit procesów podrzędnych został zmieniony przez debugowanie bezprzewodowe.</string> + <string name="wadb_confirmed_on">Android potwierdził, że limit jest włączony.</string> + <string name="wadb_confirmed_off">Android potwierdził, że limit jest wyłączony.</string> + <string name="wadb_pair_group">2 · Sparuj debugowanie bezprzewodowe</string> + <string name="wadb_pair_address">Adres IP i port okienka parowania</string> + <string name="wadb_pin">Kod parowania (PIN)</string> + <string name="wadb_pin_placeholder">6 cyfr</string> + <string name="wadb_pair">Sparuj</string> + <string name="wadb_apply_group">2 · Zastosuj przez debugowanie bezprzewodowe</string> + <string name="wadb_connect_address">Adres IP i port połączenia</string> + <string name="wadb_pair_again">Sparuj ponownie</string> + <string name="wadb_turn_off">Wyłącz limit</string> + <string name="wadb_other_ways">Inne sposoby na zmianę tego</string> + <string name="wadb_dev_try_first">1 · Opcje programisty (spróbuj najpierw)</string> + <string name="wadb_restrict_on">Ustaw „Ogranicz procesy podrzędne” na włączone.</string> + <string name="wadb_restrict_off">Ustaw „Ogranicz procesy podrzędne” na wyłączone.</string> + <string name="wadb_computer_last">3 · Komputer z ADB (ostatnia opcja)</string> + <string name="wadb_only_if">Tylko jeśli debugowanie bezprzewodowe jest niedostępne.</string> + <string name="wadb_computer_group">3 · Komputer z ADB (ostatnia opcja)</string> + <string name="ctrl_osc_hint">Gdy w sesji pojawiają się sterowanie dotykowe</string> + <string name="ctrl_color">Kolor</string> + <string name="ctrl_color_hint">Odcień przycisków na ekranie</string> + <string name="ctrl_custom">Własny</string> + <string name="ctrl_opacity">Przezroczystość</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">Rozmiar przycisków</string> + <string name="ctrl_size_hint" formatted="false">100% to rozmiar standardowy</string> + <string name="ctrl_stick_click">Kliknięcie gałki</string> + <string name="ctrl_stick_click_hint">Dotknij dwukrotnie i przytrzymaj gałkę, aby uzyskać L3 lub R3</string> + <string name="ctrl_adaptive">Adaptacyjne gałki</string> + <string name="ctrl_rumble">Wibracje gry</string> + <string name="ctrl_steam_button">Przycisk Steam</string> + <string name="ctrl_qam_button">Przycisk QAM</string> + <string name="ctrl_keyboard_button">Przycisk klawiatury</string> + <string name="ctrl_select_steam">Naciśnij Select, aby otworzyć menu Steam</string> + <string name="ctrl_select_steam_hint">Zamiast View/Select.</string> + <string name="setup_animations">Animacje</string> + <string name="ctrl_adaptive_hint">Gałki pojawiają się tam, gdzie dotykasz, i chowają się po puszczeniu</string> + <string name="ctrl_layout">Układ</string> + <string name="ctrl_layout_custom">Zapisano własne pozycje</string> + <string name="ctrl_layout_default">Dopasowany do tego ekranu</string> + <string name="ctrl_edit">Edytuj</string> + <string name="ctrl_reset">Resetuj</string> + <string name="ctrl_mapping">Mapowanie przycisków</string> + <string name="ctrl_mapping_default">Każdy przycisk wysyła własny sygnał</string> + <string name="ctrl_mapping_count">Zmapowano %1$d z %2$d przycisków</string> + <string name="ctrl_configure">Skonfiguruj</string> + <string name="ctrl_reset_all">Resetuj kontroler</string> + <string name="ctrl_reset_all_hint">Przywraca wszystkie ustawienia kontrolera i układ</string> + <string name="ctrl_mapping_lede">Co wysyła do gry każdy przycisk na ekranie. Ukryty usuwa przycisk.</string> + <string name="ctrl_onscreen_buttons">Przyciski na ekranie</string> + <string name="ctrl_defaults">Domyślne</string> + <string name="ctrl_reset_mapping">Resetuj mapowanie</string> + <string name="content_android_apps">Aplikacje Android</string> + <string name="content_apps_count">%1$d aplikacji</string> + <string name="content_no_android_apps">Nie znaleziono żadnych aplikacji Android do uruchomienia.</string> + <string name="content_desktop_installed">● Pulpit zainstalowany</string> + <string name="content_desktop_first_open">Instaluje się przy pierwszym otwarciu</string> + <string name="content_available">Dostępne do instalacji</string> + <string name="content_not_installed">Nie zainstalowano.</string> + <string name="content_not_installed_suffix">%1$s · nie zainstalowano</string> + <string name="content_open_named">Otwórz %1$s</string> + <string name="content_games">Gry</string> + <string name="content_choose_roms">Wybierz folder ROM-ów.</string> + <string name="content_retroarch_roms">Przejdź do /root/ROMs w RetroArch.</string> + <string name="content_add_roms">Dodaj gry %1$s do ROMs/%2$s.</string> + <string name="content_install_named">Zainstaluj %1$s</string> + <string name="content_stop_to_install">Zatrzymaj sesję, aby zainstalować</string> + <string name="content_runtime_required">Wymagane środowisko uruchomieniowe</string> + <string name="content_loading_details">Wczytywanie szczegółów instalacji…</string> + <string name="content_details_unavailable">Szczegóły instalacji są niedostępne. Spróbuj ponownie później.</string> + <string name="content_needs_runtime">Najpierw zainstaluj środowisko Linux w sekcji Konfiguracja.</string> + <string name="content_stop_first">Zatrzymaj działającą sesję, aby zainstalować aplikacje pulpitu.</string> + <string name="content_install_into">Instaluje %1$s w pulpicie Linux.</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">Ukrycie usuwa to z Pulpitu. Jego pliki zostają w środowisku Linux.</string> + <string name="content_rom_gone">Tej gry nie ma już w folderze ROM-ów.</string> + <string name="content_desktop_system">Pulpit · %1$s</string> + <string name="content_launch_in">Uruchom w %1$s</string> + <string name="content_also_in">Także w %1$s</string> + <string name="content_page_gone">Ta strona nie istnieje. Wybierz sekcję po lewej.</string> + <string name="content_open_desktop">Otwórz pulpit</string> + <string name="content_install_open_desktop">Zainstaluj i otwórz pulpit</string> + <string name="desktop_remove_title">Odinstaluj pulpit</string> + <string name="desktop_remove_text">Usuwa pulpit i jego dołączone aplikacje. Zachowuje gry, dodane aplikacje, ustawienia i współdzielone biblioteki.</string> + <string name="desktop_removing">Usuwanie pulpitu</string> + <string name="desktop_removed">Pulpit usunięty</string> + <string name="desktop_cleaning_legacy">Usuwanie starych plików LXQt</string> + <string name="desktop_removal_unknown">Zaktualizuj DroidDeck przed usunięciem tej wersji pulpitu.</string> + <string name="content_linux_desktop">Pulpit Linux</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox i Twoje emulatory</string> + <string name="games_empty">Gry, które instalujesz w Steam, pojawią się tutaj po sesji.</string> + <string name="games_play_steam">Zagraj w Steam</string> + <string name="games_launch_settings">Ustawienia uruchamiania</string> + <string name="games_launch">Uruchom</string> + <string name="games_files">Pliki gry</string> + <string name="games_prefix">Prefiks Proton</string> + <string name="games_saves_looking">Wyszukiwanie zapisów…</string> + <string name="games_saves_none">Nie znaleziono folderu zapisów po nazwie. Eksport obejmuje cały folder użytkownika.</string> + <string name="games_saves_folders">%1$d folderów zapisów</string> + <string name="games_saves_summary">%1$s · %2$d plików · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">Zarządzaj zapisami</string> + <string name="games_saves">Zapisy gry</string> + <string name="games_import_saves">Importuj zapisy…</string> + <string name="games_export_gamehub">Eksportuj jako zip GameHub…</string> + <string name="games_export_winlator">Eksportuj jako zip Winlator…</string> + <string name="games_open_save_folder">Otwórz folder zapisów</string> + <string name="games_never_played">Nigdy nie grano</string> + <string name="games_runtime_busy_percent">Środowisko zajęte · %1$d%%</string> + <string name="games_runtime_busy">Środowisko zajęte</string> + <string name="games_runtime_first_play">Środowisko instaluje się przy pierwszym uruchomieniu</string> + <string name="games_runtime_update">Dostępna aktualizacja środowiska</string> + <string name="games_runtime_ready">● Środowisko gotowe</string> + <string name="games_last_played">Ostatnio grano %1$s</string> + <string name="games_internal">Pamięć wewnętrzna</string> + <string name="games_added">Dodana gra</string> + <string name="games_custom">Własna gra</string> + <string name="games_store_storage">Miejsce na gry</string> + <string name="gpu_auto_hint">Najlepsza para dla tego GPU, aktualizowana automatycznie</string> + <string name="gpu_manual_hint">Wybierz parę samodzielnie</string> + <string name="gpu_checking">Sprawdzanie sterowników…</string> + <string name="gpu_none_listed">Brak sterowników na liście. Odśwież, aby ich poszukać.</string> + <string name="gpu_hide_other">Ukryj sterowniki innych GPU</string> + <string name="gpu_show_other">Pokaż sterowniki innych GPU</string> + <string name="gpu_advanced">Zaawansowane</string> + <string name="gpu_set_with_display">Ustawiany wraz ze sterownikiem wyświetlania: %1$s obejmuje oba.</string> + <string name="gpu_set_with_runtime">Ustawiany wraz ze sterownikiem środowiska: %1$s obejmuje oba.</string> + <string name="gpu_runtime_hint">Używany przez gry i pulpit.</string> + <string name="gpu_runtime_default">Domyślny dla środowiska</string> + <string name="gpu_display_hint">Rysuje obraz na ekranie. Działa po ponownym uruchomieniu DroidDeck.</string> + <string name="gpu_auto_by_gpu">Auto – wybrany według GPU</string> + <string name="gpu_manage">Pobierz, zaimportuj lub usuń…</string> + <string name="gpu_in_use_caps">W UŻYCIU</string> + <string name="gpu_builtin">Sterowniki wbudowane w DroidDeck</string> + <string name="gpu_matched_pair">Sterowniki środowiska i wyświetlania ustawiane razem jako dobrana para.</string> + <string name="gpu_recommended_for">Polecany dla tego GPU: %1$s.</string> + <string name="gpu_refresh_hint">Odśwież, aby poszukać sterowników dla tego GPU.</string> + <string name="gpu_recommended_caps">POLECANY</string> + <string name="gpu_unavailable">Niedostępny</string> + <string name="load_runtime">Środowisko Linux</string> + <string name="load_runtime_detail">Przygotowywanie środowiska Linux</string> + <string name="load_session">Sesja</string> + <string name="load_session_detail">Uruchamianie obrazu i dźwięku</string> + <string name="load_steam">Klient Steam</string> + <string name="load_steam_detail">Sprawdzanie Steam i jego narzędzi zgodności</string> + <string name="load_opening_steam">Otwieranie Steam</string> + <string name="load_opening_steam_detail">Czekanie na pierwszą klatkę Steam</string> + <string name="load_desktop">Pulpit</string> + <string name="load_desktop_detail">Przygotowywanie pulpitu</string> + <string name="load_opening">Otwieranie</string> + <string name="load_opening_detail">Oczekiwanie na pierwszą klatkę</string> + <string name="load_starting_steam">Uruchamianie Steam</string> + <string name="load_ended">Sesja się zakończyła</string> + <string name="load_step">Krok %1$d z %2$d</string> + <string name="load_steam_paused">Steam jest wstrzymany</string> + <string name="deck_tab_grips">Uchwyty</string> + <string name="deck_tab_left">Lewy pad</string> + <string name="deck_tab_right">Prawy pad</string> + <string name="deck_tab_both">Oba</string> + <string name="deck_tab_gamepad">Gamepad</string> + <string name="deck_surface_stick">Gałka</string> + <string name="deck_surface_trackpad">Trackpad</string> + <string name="deck_surface_switch">Przełącz między gałką a trackpadem</string> + <string name="deck_more_controls">Więcej sterowania Deck</string> + <string name="deck_panels">PANELE</string> + <string name="deck_close">Zamknij sterowanie na drugim ekranie</string> + <string name="deck_open_steam_menu">Otwórz menu Steam</string> + <string name="deck_left_upper">Lewy · górny</string> + <string name="deck_left_lower">Lewy · dolny</string> + <string name="deck_right_upper">Prawy · górny</string> + <string name="deck_right_lower">Prawy · dolny</string> + <string name="deck_click">Kliknięcie</string> + <string name="deck_right_trackpad">Prawy trackpad</string> + <string name="deck_left_trackpad">Lewy trackpad</string> + <string name="second_trackpad">Trackpad</string> + <string name="second_show_keyboard">Pokaż klawiaturę</string> + <string name="second_hide_keyboard">Ukryj klawiaturę</string> + <string name="second_terminal">Terminal Linux</string> + <string name="second_terminal_not_ready">Sesja Linux nie jest jeszcze gotowa na terminal.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Gry</string> + <string name="rail_desktop">Pulpit</string> + <string name="rail_store">Sklep</string> + <string name="rail_components">Komponenty</string> + <string name="rail_apps">Aplikacje</string> + <string name="rail_setup">Konfiguracja</string> + <string name="rail_updates">Aktualizacje</string> + <string name="rail_percent">%1$d%%</string> + <string name="rail_resume_named">Wznów %1$s</string> + <string name="roms_text">W każdej sesji widoczny jako /root/ROMs. Pamięć wewnętrzna jest w /root/Storage.</string> + <string name="roms_none">Nie wybrano folderu</string> + <string name="roms_applies">Zadziała od następnej sesji. Karty SD są obsługiwane.</string> + <string name="roms_choose">Wybierz folder</string> + <string name="roms_change">Zmień folder</string> + <string name="proton_lede">Zainstaluj wersję Proton dla ARM64, a potem wybierz ją dla gry w Steam → Właściwości → Zgodność.</string> + <string name="proton_stop_first">Zatrzymaj działającą sesję, aby zainstalować lub usunąć narzędzie zgodności.</string> + <string name="proton_available">Dostępne wersje</string> + <string name="proton_installed">Zainstalowano %1$s</string> + <string name="proton_queued">Oczekuje z wcześniejszego żądania</string> + <string name="proton_not_installed">Nie zainstalowano</string> + <string name="proton_update_available">Dostępna aktualizacja → %1$s</string> + <string name="proton_remove_older">Usuń stare wersje %1$s</string> + <string name="proton_remove_older_body">Usuwa %1$s. Gry ustawione na te wersje trzeba przełączyć na inny Proton.</string> + <string name="proton_remove_body">Gry ustawione na tę wersję trzeba przełączyć na inny Proton.</string> + <string name="proton_install_now">Zainstaluj teraz</string> + <string name="proton_cancel_queued">Anuluj oczekujące</string> + <string name="pckb_android">Klawiatura Android</string> + <string name="pckb_hide">Ukryj ✕</string> + <string name="main_launch_named">Uruchom %1$s</string> + <string name="main_choose_screen">Wybierz ekran. Dodatkowy: %1$s.</string> + <string name="main_secondary_gone">Dodatkowy wyświetlacz nie jest już dostępny.</string> + <string name="main_primary">Ekran podstawowy</string> + <string name="main_secondary">Ekran dodatkowy</string> + <string name="widgets_prev_tab">Poprzednia karta</string> + <string name="widgets_next_tab">Następna karta</string> + <string name="widgets_on">Wł.</string> + <string name="widgets_all_n">Wszystkie %1$d</string> + <string name="widgets_none">Brak</string> + <string name="widgets_n_of_m">%1$d z %2$d</string> + <string name="widgets_off">Wył.</string> + <string name="screen_bottom">Dolny ekran</string> + <string name="game_env_applies">Zadziała przy następnym uruchomieniu gry. Profil gry zastępuje profil wspólny.</string> + <string name="game_env_other">Inna gra według ID aplikacji…</string> + <string name="game_env_done">Gotowe</string> + <string name="game_env_default">Domyślne</string> + <string name="game_env_off">Wył.</string> + <string name="game_env_change">Zmień…</string> + <string name="game_env_change_title">Zmień zmienną</string> + <string name="game_env_restore_default">Przywróć domyślne</string> + <string name="game_env_eyebrow">Gry Proton</string> + <string name="pip_title">Obraz w obrazie</string> + <string name="pip_auto">Automatycznie włączaj obraz w obrazie</string> + <string name="unfolded_controls">Sterowanie na rozłożonym ekranie</string> + <string name="unfolded_controls_hint">Po rozłożeniu gra jest nad trackpadami i tylnymi uchwytami Steam Deck. Po złożeniu gra zajmuje cały ekran.</string> + <string name="pip_suspended">Wstrzymano</string> + <string name="pip_not_ready">Obraz w obrazie będzie dostępny, gdy sesja będzie gotowa.</string> + <string name="pip_permission">Zezwól DroidDeck na obraz w obrazie w ustawieniach systemu Android.</string> + <string name="pip_unavailable">Android nie mógł otworzyć obrazu w obrazie.</string> + <string name="game_shortcut">Skrót</string> + <string name="game_shortcut_add">Dodaj do ekranu głównego</string> + <string name="game_shortcut_choose">Wybierz grę</string> + <string name="game_shortcut_scanning">Skanowanie zainstalowanych gier…</string> + <string name="game_link_copy">Kopiuj link uruchamiania</string> + <string name="game_link_copied">Skopiowano link uruchamiania</string> + <string name="game_shortcut_unsupported">Ten launcher nie może dodawać skrótów. Użyj opcji „Kopiuj link uruchamiania”.</string> + <string name="game_link_invalid">Nieprawidłowy link uruchamiania gry.</string> + <string name="game_link_missing">Tej gry nie ma już w bibliotece DroidDeck.</string> + <string name="game_link_session_busy">Zatrzymaj bieżącą sesję przed uruchomieniem gry Steam.</string> + <string name="game_link_failed">Nie udało się wysłać gry do Steam. Spróbuj uruchomić ją ponownie.</string> + <string name="setup_retry_removal">Ponów usuwanie</string> + <string name="runtime_removing">Usuwanie</string> + <string name="runtime_removal_incomplete">Usuwanie nieukończone</string> + <string name="game_file_export">Eksportuj plik gry</string> + <string name="game_file_sync">Synchronizuj wszystkie pliki gier z folderem</string> + <string name="game_file_stop_sync">Zatrzymaj synchronizację plików gier</string> + <string name="game_file_folder">Wybierz folder na pliki gier dla frontendów</string> + <string name="game_file_exported">Pliki gier zapisano w %1$s</string> + <string name="game_frontend_files">Pliki frontendów</string> + + <string name="main_wait_runtime">Poczekaj na zakończenie operacji na środowisku</string> + <string name="main_wait_proton_install">Poczekaj na zakończenie instalacji narzędzia zgodności</string> + <string name="main_wait_desktop_install">Poczekaj na zakończenie instalacji aplikacji desktopowej</string> + <string name="main_wait_install_game">Poczekaj na zakończenie instalacji, zanim uruchomisz grę</string> + <string name="main_saves_close_session">Najpierw zamknij sesję Steam, aby gra nie nadpisała importowanych zapisów</string> + <string name="main_saves_importing">Importowanie do %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="one">Zaimportowano %1$d plik z %2$s do %3$s</item> + <item quantity="few">Zaimportowano %1$d pliki z %2$s do %3$s</item> + <item quantity="many">Zaimportowano %1$d plików z %2$s do %3$s</item> + <item quantity="other">Zaimportowano %1$d pliku z %2$s do %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">Zaimportowano %1$d plik z %2$s do %3$s. Kopię starych zapisów umieszczono w Download/DroidDeck/Saves/backups</item> + <item quantity="few">Zaimportowano %1$d pliki z %2$s do %3$s. Kopię starych zapisów umieszczono w Download/DroidDeck/Saves/backups</item> + <item quantity="many">Zaimportowano %1$d plików z %2$s do %3$s. Kopię starych zapisów umieszczono w Download/DroidDeck/Saves/backups</item> + <item quantity="other">Zaimportowano %1$d pliku z %2$s do %3$s. Kopię starych zapisów umieszczono w Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Wybierz plik zip z zapisami dla %1$s</string> + <string name="main_saves_exporting">Eksportowanie %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="one">Wyeksportowano %1$d plik jako %2$s: %3$s</item> + <item quantity="few">Wyeksportowano %1$d pliki jako %2$s: %3$s</item> + <item quantity="many">Wyeksportowano %1$d plików jako %2$s: %3$s</item> + <item quantity="other">Wyeksportowano %1$d pliku jako %2$s: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Wybierz, gdzie zapisać %1$s (%2$s)</string> + <string name="main_action_failed">%1$s nie powiodło się: %2$s</string> + <string name="main_no_logs_yet">Brak logów sesji: najpierw uruchom sesję.</string> + <string name="main_no_logs_to_clear">Brak logów sesji do wyczyszczenia.</string> + <plurals name="main_logs_cleared"> + <item quantity="one">Wyczyszczono %1$d folder logów sesji.</item> + <item quantity="few">Wyczyszczono %1$d foldery logów sesji.</item> + <item quantity="many">Wyczyszczono %1$d folderów logów sesji.</item> + <item quantity="other">Wyczyszczono %1$d folderu logów sesji.</item> + </plurals> + <string name="main_wadb_pair_failed">Parowanie przez debugowanie bezprzewodowe nie powiodło się</string> + <string name="main_wadb_command_failed">Polecenie debugowania bezprzewodowego nie powiodło się</string> + <string name="main_wadb_unavailable">Debugowanie bezprzewodowe jest niedostępne</string> + <string name="main_adb_copied">Skopiowano polecenie ADB</string> + <string name="main_pick_roms">Wybierz folder ROM-ów</string> + <string name="main_non_adreno_title">To nie jest GPU Adreno</string> + <string name="main_non_adreno_text">Turnip obsługuje GPU Adreno. Na tym urządzeniu (%1$s) Steam może wyświetlać czarny ekran. Pobieranie: %2$.0f MB.</string> + <string name="main_install_anyway">Zainstaluj mimo to</string> + <string name="main_remove_runtime_title">Usuń środowisko Linux</string> + <string name="main_remove_runtime_text">Spowoduje to usunięcie środowiska, klienta Steam w nim i wszystkich zainstalowanych tam gier.</string> + <string name="main_applied">Zastosowano: %1$s</string> + <string name="android_app_open_failed_primary">Nie udało się otworzyć %1$s na ekranie podstawowym</string> + <string name="android_app_open_failed_on">Nie udało się otworzyć %1$s na wyświetlaczu %2$s</string> + <string name="android_app_open_failed_display">Nie udało się otworzyć %1$s na wyświetlaczu %2$d</string> + <string name="comp_busy_importing">Importowanie</string> + <string name="comp_busy_swapping">Zamienianie</string> + <string name="comp_busy_restoring">Przywracanie</string> + <string name="comp_busy_cancelling">Anulowanie</string> + <string name="comp_busy_deleting">Usuwanie</string> + <string name="comp_cannot_read">nie można odczytać pliku</string> + <string name="comp_imported">Zaimportowano %1$s</string> + <string name="comp_swap_cancelled">Anulowano oczekującą zamianę.</string> + <string name="comp_pick_wcp">Wybierz pakiet komponentu (-linux .wcp)</string> + <string name="comp_nightlies_unreachable">Nie udało się połączyć z Nightlies</string> + <string name="comp_stored">Zapisano %1$s</string> + <string name="comp_download_failed">Pobieranie nie powiodło się: %1$s</string> + <string name="drivers_pick_linux">Wybierz sterownik środowiska Linux (zip -Linux lub pakiet Android + Linux)</string> + <string name="drivers_pick_android">Wybierz sterownik wyświetlania (zip AdrenoTools lub pakiet Android + Linux)</string> + <string name="drivers_pick_any">Wybierz zip ze sterownikiem lub pakiet Android + Linux</string> + <string name="main_pkg_problem">%1$s: %2$s</string> + <string name="main_removing_named">Usuwanie %1$s…</string> + <string name="main_forgetting_named">Zapominanie %1$s…</string> + <string name="main_pick_game_storage">Wybierz folder na gry</string> + <string name="main_pick_decky_zip">Wybierz plik ZIP wtyczki Decky</string> + <string name="main_storage_not_usable">Nie nadaje się: %1$s</string> + <string name="main_running_program">Program</string> + <string name="main_removing_runtime">Usuwanie środowiska Linux</string> + <string name="main_settings_main_screen">Nie udało się otworzyć Ustawień na dolnym ekranie; otwarto je na ekranie podstawowym.</string> + <string name="main_open_dev_options">Otwórz Opcje programisty w Ustawieniach</string> + + <string name="upd_unknown_apps_failed">Nie udało się otworzyć ustawienia systemu Android „Instalowanie nieznanych aplikacji”</string> + <string name="upd_couldnt_check">Nie udało się sprawdzić: %1$s</string> + <string name="upd_no_download">Ta wersja nie ma pliku do pobrania dla tej kopii DroidDeck</string> + <string name="upd_stop_first">Najpierw zatrzymaj działającą sesję, a potem zaktualizuj</string> + <string name="upd_stage_downloading">Pobieranie</string> + <string name="upd_stage_installing">Instalowanie</string> + <string name="upd_stage_confirm">Oczekiwanie na Twoje potwierdzenie</string> + <string name="upd_failed">Aktualizacja nie powiodła się</string> + <string name="upd_not_offered">Android nie zaproponował instalacji aktualizacji</string> + + <string name="decky_install_failed">Instalacja Decky nie powiodła się: %1$s</string> + <string name="decky_plugin_starting">Rozpoczynanie importu wtyczki…</string> + <string name="decky_plugin_failed">Import wtyczki nie powiódł się: %1$s</string> + <string name="decky_plugin_installed">Zainstalowano wtyczkę. Uruchom ponownie Steam, aby ją wczytać.</string> + + <string name="drivers_not_checked">Jeszcze nie sprawdzono – dotknij odświeżania, aby poszukać nowych sterowników</string> + <string name="drivers_bundle_sets_display">Android + Linux: ustawia też sterownik wyświetlania</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: ustawia też sterownik środowiska</string> + <string name="drivers_runtime_builtin">Turnip wbudowany w środowisko</string> + <string name="drivers_system_vulkan">systemowy Vulkan: brak dołączonej wersji dla tego GPU</string> + <string name="drivers_bundled">%1$s (dołączony)</string> + <string name="drivers_manual">Ręcznie</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">Obecnie opublikowano tylko jedną połowę</string> + <string name="drivers_pair_size">%1$.0f MB łącznie</string> + <string name="drivers_none_to_set">Brak sterowników do ustawienia: %1$s</string> + <string name="drivers_checking_latest">Szukanie najnowszych sterowników…</string> + <string name="drivers_up_to_date_check_failed">Aktualne (nie udało się sprawdzić: %1$s)</string> + <string name="drivers_recommended_unpublished">Zalecane sterowniki nie są obecnie opublikowane</string> + <string name="drivers_downloading_pair">Pobieranie %1$s %2$s…</string> + <string name="drivers_updated_to">Zaktualizowano do %1$s %2$s.</string> + <string name="drivers_updated_to_restart">Zaktualizowano do %1$s %2$s. Sterownik wyświetlania zadziała po ponownym uruchomieniu DroidDeck.</string> + <string name="drivers_switched_to">Przełączono na %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Przełączono na %1$s %2$s. Sterownik wyświetlania zadziała po ponownym uruchomieniu DroidDeck.</string> + <string name="drivers_using">Używany: %1$s %2$s.</string> + <string name="drivers_using_restart">Używany: %1$s %2$s. Sterownik wyświetlania zadziała po ponownym uruchomieniu DroidDeck.</string> + <string name="drivers_not_a_driver">To nie jest zip ze sterownikiem: ani sterownik AdrenoTools, ani Turnip -Linux, ani pakiet Android + Linux</string> + <string name="drivers_import_failed">Import nie powiódł się: %1$s</string> + <string name="drivers_imported_unnamed">Zaimportowany sterownik</string> + <string name="drivers_using_bundle">%1$s służy teraz jako sterownik środowiska i wyświetlania.</string> + <string name="drivers_using_bundle_restart">%1$s służy teraz jako sterownik środowiska i wyświetlania. Sterownik wyświetlania zadziała po ponownym uruchomieniu DroidDeck.</string> + <string name="drivers_deleted_bundle">Usunięto %1$s (oba sterowniki)</string> + <string name="drivers_deleted">Usunięto %1$s</string> + <string name="drivers_release_status">Najnowsze: %1$s · sprawdzono %2$s</string> + <string name="drivers_release_status_failed">Najnowsze: %1$s · %2$s niedostępne · sprawdzono %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">%1$d min temu</item> + <item quantity="few">%1$d min temu</item> + <item quantity="many">%1$d min temu</item> + <item quantity="other">%1$d min temu</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">%1$d godz. temu</item> + <item quantity="few">%1$d godz. temu</item> + <item quantity="many">%1$d godz. temu</item> + <item quantity="other">%1$d godz. temu</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">%1$d dzień temu</item> + <item quantity="few">%1$d dni temu</item> + <item quantity="many">%1$d dni temu</item> + <item quantity="other">%1$d dnia temu</item> + </plurals> + <string name="drivers_checking_sources">Sprawdzanie Banners-Turnip, WinNative i DroidDeck…</string> + <string name="drivers_check_failed">błąd sprawdzania</string> + <string name="drivers_installed_pick_either">Zainstalowano %1$s – wybierz go na dowolnej liście, aby ustawić oba sterowniki</string> + <string name="drivers_installed_pick">Zainstalowano %1$s – wybierz go w menu</string> + + <string name="session_wait_removal">Poczekaj na zakończenie usuwania środowiska</string> + <string name="session_title_game">Gra</string> + <string name="session_touch_auto_touchpad">touchpad</string> + <string name="session_touch_auto_direct">bezpośredni</string> + <string name="session_swap_failed">Zamiana nie powiodła się: %1$s</string> + <string name="session_profile_save_failed">Zapis profilu nie powiódł się: %1$s</string> + <string name="session_desktop_paused">Pulpit jest wstrzymany</string> + <string name="session_named_paused">%1$s – wstrzymano</string> + <string name="session_program_paused">Program jest wstrzymany</string> + <string name="session_starting_desktop">Uruchamianie pulpitu</string> + <string name="session_starting_named">Uruchamianie %1$s</string> + <string name="session_starting_program">Uruchamianie programu</string> + <string name="session_no_logs">Brak logów dla tej sesji.</string> + <string name="session_logs_archive_failed">Nie udało się utworzyć archiwum logów sesji.</string> + <string name="session_logs_share_failed">Nie udało się udostępnić logów sesji.</string> + <string name="session_starting_session">Uruchamianie sesji…</string> + <string name="session_runtime_downloading">pobieranie środowiska Linux</string> + <string name="session_runtime_downloading_mb">pobieranie środowiska Linux · %1$d z %2$d MB</string> + <string name="session_runtime_checking">sprawdzanie środowiska Linux</string> + <string name="session_runtime_unpacking">rozpakowywanie środowiska Linux</string> + <string name="session_desktop_downloading">pobieranie pulpitu</string> + <string name="session_desktop_downloading_mb">pobieranie pulpitu · %1$d z %2$d MB</string> + <string name="session_desktop_checking">sprawdzanie pulpitu</string> + <string name="session_desktop_unpacking">rozpakowywanie pulpitu</string> + <string name="session_proton_downloading">pobieranie Proton Experimental (ARM64)</string> + <string name="session_proton_downloading_mb">pobieranie Proton Experimental (ARM64) · %1$d z %2$d MB</string> + <string name="session_proton_checking">sprawdzanie Proton Experimental (ARM64)</string> + <string name="session_proton_unpacking">rozpakowywanie Proton Experimental (ARM64)</string> + <string name="session_runtime_catalog_unreachable">Nie udało się połączyć z katalogiem środowisk. Sprawdź połączenie i ponownie naciśnij Graj.</string> + <string name="session_runtime_install_failed">Środowisko Linux nie zostało zainstalowane. Sprawdź środowisko w sekcji Konfiguracja i ponownie naciśnij Graj.</string> + <string name="session_desktop_install_failed">Pulpit nie został zainstalowany (%1$s). Sprawdź połączenie i ponownie naciśnij Pulpit.</string> + <string name="session_ended_named">%1$s – nieoczekiwane zatrzymanie. Udostępnij logi w zgłoszeniu błędu lub spróbuj ponownie.</string> + <string name="session_ended_program">Program nieoczekiwanie się zatrzymał. Udostępnij logi w zgłoszeniu błędu lub spróbuj ponownie.</string> + <string name="session_ended_desktop">Pulpit nieoczekiwanie się zatrzymał. Udostępnij logi w zgłoszeniu błędu lub spróbuj ponownie.</string> + <string name="session_ended_detail">Kod wyjścia %1$d · %2$s</string> + <string name="session_ended_no_log">brak logu</string> + <plurals name="session_hint_enosys"> + <item quantity="one">Log zawiera %1$d błąd „Function not implemented”: przyspieszenie seccomp w proot zawodzi przy procesie pomocniczym na tym urządzeniu. Wybierz %2$s → „%3$s” (lub „%4$s”) i uruchom ponownie.</item> + <item quantity="few">Log zawiera %1$d błędy „Function not implemented”: przyspieszenie seccomp w proot zawodzi przy procesie pomocniczym na tym urządzeniu. Wybierz %2$s → „%3$s” (lub „%4$s”) i uruchom ponownie.</item> + <item quantity="many">Log zawiera %1$d błędów „Function not implemented”: przyspieszenie seccomp w proot zawodzi przy procesie pomocniczym na tym urządzeniu. Wybierz %2$s → „%3$s” (lub „%4$s”) i uruchom ponownie.</item> + <item quantity="other">Log zawiera %1$d błędu „Function not implemented”: przyspieszenie seccomp w proot zawodzi przy procesie pomocniczym na tym urządzeniu. Wybierz %2$s → „%3$s” (lub „%4$s”) i uruchom ponownie.</item> + </plurals> + + <string name="mode_decky_plugins">Wtyczki</string> + <string name="mode_decky_plugins_hint">Zainstaluj wtyczkę z pliku ZIP na następną sesję Steam.</string> + <string name="mode_decky_plugin_downloading">Pobieranie…</string> + <string name="mode_decky_plugin_install_zip">Zainstaluj z pliku ZIP</string> + <string name="store_cat_games">Gry</string> + <string name="store_cat_audio_video">Audio i wideo</string> + <string name="store_cat_graphics">Grafika</string> + <string name="store_cat_internet">Internet</string> + <string name="store_cat_office">Biuro</string> + <string name="store_cat_developer">Programowanie</string> + <string name="store_cat_education">Edukacja</string> + <string name="store_cat_science">Nauka</string> + <string name="store_cat_system">System</string> + <string name="store_cat_utilities">Narzędzia</string> + <string name="store_section_popular">Popularne</string> + <string name="store_section_trending">Na czasie</string> + <string name="store_section_new">Nowe</string> + <string name="store_section_updated">Zaktualizowane</string> + <string name="store_section_loading">wczytywanie…</string> + <string name="store_flatpak_setup">Konfiguracja Flatpak</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Nie udało się sprawdzić aktualizacji w Flathub</string> + <string name="content_rom_installed">zainstalowana</string> + <string name="content_rom_folder">folder</string> + <string name="theme_graphite">Grafit</string> + <string name="theme_graphite_detail">Grafitowe powierzchnie i niebieski kuli z logo dla wszystkiego, co zaznaczone, aktywne lub naciśnięte.</string> + <string name="theme_paper">Papier na czerni</string> + <string name="theme_paper_detail">Biel jest kolorem głównym, a niebieski z ikony wyróżnia linie, pierścienie i kropki.</string> + <string name="theme_icon_blue">Niebieski z ikony</string> + <string name="theme_icon_blue_detail">Czarne tło, biały tekst i niebieski dla wszystkiego, co zaznaczone, aktywne lub naciśnięte.</string> + <string name="theme_electric">Elektryczny granat</string> + <string name="theme_electric_detail">Jaśniejszy niebieski na powierzchniach wpadających w granat zamiast w szarość.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Spacja</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <string name="game_sync_err_runtime">Najpierw zainstaluj środowisko</string> + <string name="game_sync_err_steam_library">Biblioteka Steam jest niedostępna</string> + <string name="game_sync_err_previous_sd">Poprzednia biblioteka na karcie SD jest niedostępna</string> + <string name="game_sync_err_game_storage">Miejsce na gry jest niedostępne</string> + <string name="game_sync_err_added_games">Folder dodanych gier jest niedostępny</string> + <string name="game_sync_err_manifests">Manifesty Steam są niedostępne</string> + <string name="content_system_many">Wiele systemów</string> + <string name="fm_drive_internal">Wewnętrzna</string> + <string name="fm_drive_linux">System Linux</string> + <string name="fm_drive_sd">Karta SD</string> + <string name="fm_drive_storage">Pamięć</string> + <string name="fm_select">Zaznacz</string> + <string name="fm_properties">Właściwości</string> + <string name="fm_favorite_add">Dodaj do ulubionych</string> + <string name="fm_favorite_remove">Usuń z ulubionych</string> + <string name="fm_rename">Zmień nazwę</string> + <string name="fm_copy">Kopiuj</string> + <string name="fm_cut">Wytnij</string> + <string name="fm_actions">Działania</string> + <string name="fm_favorites">Ulubione</string> + <string name="fm_pin_current">Przypnij bieżący folder</string> + <string name="fm_no_favorites">Brak ulubionych – przypnij folder w jego menu ⋮, aby szybko tu wracać.</string> + <string name="fm_unpin_favorite">Usuń z ulubionych</string> + <string name="fm_size_zero">0 bajtów</string> + <string name="fm_unit_bytes">B</string> + <string name="fm_prop_location">Lokalizacja</string> + <string name="fm_prop_type">Typ</string> + <string name="fm_prop_size">Rozmiar</string> + <string name="fm_prop_modified">Zmodyfikowano</string> + <string name="fm_type_folder">Folder</string> + <string name="fm_type_file">Plik</string> + <string name="fm_type_ext_file">Plik %1$s</string> + <string name="fm_read_only">Tylko do odczytu</string> + <string name="fm_read_only_hint">Chroni ten plik przed nadpisaniem lub usunięciem przez gry (i Wine).</string> + <string name="fm_read_only_failed">Nie udało się zmienić atrybutu Tylko do odczytu</string> + <string name="fm_hidden">Ukryty</string> + <string name="fm_hidden_unsupported">Ta pamięć nie obsługuje atrybutu ukrycia.</string> + <string name="fm_hidden_hint">Oznacza plik jako ukryty w Windows (atrybut ukrycia Wine).</string> + <string name="fm_hidden_failed">Nie udało się zmienić atrybutu Ukryty w tej pamięci</string> + <string name="fm_done">Gotowe</string> + <string name="fm_op_deleting">Usuwanie...</string> + <string name="fm_delete_failed">Usuwanie nie powiodło się</string> + <string name="fm_op_moving">Przenoszenie...</string> + <string name="fm_op_copying">Kopiowanie...</string> + <string name="fm_op_moving_item">Przenoszenie - %1$s</string> + <string name="fm_op_copying_item">Kopiowanie - %1$s</string> + <string name="fm_op_moving_batch">Przenoszenie %1$d/%2$d - %3$s</string> + <string name="fm_op_copying_batch">Kopiowanie %1$d/%2$d - %3$s</string> + <string name="fm_paste_failed">Gotowe: %1$d, błędy: %2$d</string> + <string name="fm_paste_skipped">Gotowe: %1$d, pominięte: %2$d</string> + <plurals name="fm_paste_moved"> + <item quantity="one">Przeniesiono %1$d element</item> + <item quantity="few">Przeniesiono %1$d elementy</item> + <item quantity="many">Przeniesiono %1$d elementów</item> + <item quantity="other">Przeniesiono %1$d elementu</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">Skopiowano %1$d element</item> + <item quantity="few">Skopiowano %1$d elementy</item> + <item quantity="many">Skopiowano %1$d elementów</item> + <item quantity="other">Skopiowano %1$d elementu</item> + </plurals> + <string name="fm_already_exists">„%1$s” już istnieje</string> + <string name="fm_op_renaming">Zmiana nazwy...</string> + <string name="fm_rename_failed">Zmiana nazwy nie powiodła się</string> + <string name="fm_op_creating_folder">Tworzenie folderu...</string> + <string name="fm_create_folder_failed">Nie udało się utworzyć folderu</string> + <string name="fm_new_folder">Nowy folder</string> + <string name="fm_folder_name">Nazwa folderu</string> + <string name="fm_create">Utwórz</string> + <string name="fm_new_name">Nowa nazwa</string> + <string name="fm_delete_title">Usunąć?</string> + <string name="fm_delete_text">Trwale usunąć „%1$s”?</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">Usunąć %1$d element?</item> + <item quantity="few">Usunąć %1$d elementy?</item> + <item quantity="many">Usunąć %1$d elementów?</item> + <item quantity="other">Usunąć %1$d elementu?</item> + </plurals> + <string name="fm_cannot_undo">Tej operacji nie można cofnąć.</string> + <plurals name="fm_and_more"> + <item quantity="one">…i jeszcze %1$d</item> + <item quantity="few">…i jeszcze %1$d</item> + <item quantity="many">…i jeszcze %1$d</item> + <item quantity="other">…i jeszcze %1$d</item> + </plurals> + <string name="fm_op_deleting_batch">Usuwanie %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">Nie udało się usunąć %1$d elementu</item> + <item quantity="few">Nie udało się usunąć %1$d elementów</item> + <item quantity="many">Nie udało się usunąć %1$d elementów</item> + <item quantity="other">Nie udało się usunąć %1$d elementu</item> + </plurals> + <string name="fm_conflict_merge_hint">„Scal” dodaje i zastępuje pliki w istniejącym folderze.</string> + <string name="fm_conflict_overwrite_hint">„Zastąp” podmienia istniejący plik.</string> + <string name="fm_conflict_apply_all">Zastosuj do wszystkich konfliktów</string> + <string name="fm_conflict_merge">Scal</string> + <string name="fm_conflict_overwrite">Zastąp</string> + <string name="fm_conflict_keep_both">Zachowaj oba</string> + <string name="fm_conflict_skip">Pomiń</string> + <string name="fm_select_this_folder">Wybierz ten folder</string> + <string name="fm_back">Wstecz</string> + <string name="fm_drive_not_readable">%1$s: zamontowano, ale obecnie nie można odczytać</string> + <string name="fm_list_view">Widok listy</string> + <string name="fm_grid_view">Widok siatki</string> + <string name="fm_search">Szukaj</string> + <string name="fm_sort">Sortuj</string> + <string name="fm_sort_name">Nazwa</string> + <string name="fm_sort_date">Data modyfikacji</string> + <string name="fm_rows_comfortable">Przestronne wiersze</string> + <string name="fm_rows_compact">Kompaktowe wiersze</string> + <string name="fm_hide_hidden">Ukryj ukryte pliki</string> + <string name="fm_show_hidden">Pokaż ukryte pliki</string> + <string name="fm_hide_favorites">Ukryj ulubione</string> + <string name="fm_show_favorites">Pokaż ulubione</string> + <string name="fm_filter_folder">Filtruj ten folder</string> + <string name="fm_free_space">Wolne: %1$s</string> + <string name="fm_selected_count">Zaznaczono: %1$d</string> + <string name="fm_select_none">Nic</string> + <string name="fm_select_all">Wszystko</string> + <string name="fm_paste">Wklej</string> + <plurals name="fm_items"> + <item quantity="one">%1$d element</item> + <item quantity="few">%1$d elementy</item> + <item quantity="many">%1$d elementów</item> + <item quantity="other">%1$d elementu</item> + </plurals> + <string name="fm_paste_here">Wklej tutaj: %1$s</string> + <string name="fm_paste_move_here">Wklej tutaj (przenieś): %1$s</string> + <string name="fm_rail_title">Pliki</string> + <string name="fm_rail_storage">PAMIĘĆ</string> + <string name="fm_rail_quick">SZYBKI DOSTĘP</string> + <string name="fm_rail_favorites">ULUBIONE</string> + <string name="fm_rail_downloads">Pobrane</string> + <string name="fm_rail_downloads_short">Pobrane</string> + <string name="fm_rail_roms">ROM-y</string> + <string name="fm_rail_session_logs">Logi sesji</string> + <string name="fm_rail_pictures">Obrazy</string> + <string name="fm_favorite_added">Dodano „%1$s” do ulubionych</string> + <string name="fm_favorite_removed">Usunięto „%1$s” z ulubionych</string> + <string name="fm_empty_directory">Pusty katalog</string> + <string name="fm_up_one_level">Poziom wyżej</string> + <string name="fm_choose_folder">Wybierz folder</string> + <string name="fm_choose_file">Wybierz plik</string> + <string name="ctrl_tint_steam_blue">Niebieski Steam</string> + <string name="ctrl_tint_sky">Błękitny</string> + <string name="ctrl_tint_white">Biały</string> + <string name="ctrl_tint_violet">Fioletowy</string> + <string name="ctrl_tint_green">Zielony</string> + <string name="ctrl_tint_amber">Bursztynowy</string> + <string name="ctrl_tint_red">Czerwony</string> + <string name="ctrl_tint_pink">Różowy</string> + <string name="ctrl_button_a">Przycisk A</string> + <string name="ctrl_button_b">Przycisk B</string> + <string name="ctrl_button_x">Przycisk X</string> + <string name="ctrl_button_y">Przycisk Y</string> + <string name="ctrl_button_lb">Lewy bumper</string> + <string name="ctrl_button_rb">Prawy bumper</string> + <string name="ctrl_button_lt">Lewy spust</string> + <string name="ctrl_button_rt">Prawy spust</string> + <string name="ctrl_button_view">Przycisk Widok</string> + <string name="ctrl_button_menu">Przycisk Menu</string> + <string name="ctrl_target_view">Widok</string> + <string name="ctrl_target_menu">Menu</string> + <string name="ctrl_target_up">Krzyżak w górę</string> + <string name="ctrl_target_down">Krzyżak w dół</string> + <string name="ctrl_target_left">Krzyżak w lewo</string> + <string name="ctrl_target_right">Krzyżak w prawo</string> + <string name="ctrl_target_hidden">Ukryty</string> + <string name="ctrl_editor_drag">Przeciągnij, aby przenieść</string> + <string name="ctrl_editor_snap">Przyciąganie</string> + <string name="ctrl_editor_save">Zapisz</string> + <string name="second_mode_none">Brak</string> + <string name="second_mode_keyboard_trackpad">Klawiatura + trackpad</string> + <string name="second_mode_terminal">Terminal</string> + <string name="second_mode_deck_controls">Uchwyty Deck + trackpady</string> + + <string name="rtinst_removing">Usuwanie środowiska Linux</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">Usuwanie środowiska Linux · %1$d wpis</item> + <item quantity="few">Usuwanie środowiska Linux · %1$d wpisy</item> + <item quantity="many">Usuwanie środowiska Linux · %1$d wpisów</item> + <item quantity="other">Usuwanie środowiska Linux · %1$d wpisu</item> + </plurals> + <string name="rtinst_removed">Usunięto środowisko Linux</string> + <string name="rtinst_removal_failed">Nie udało się dokończyć usuwania środowiska Linux. Ponów usuwanie w sekcji Konfiguracja.</string> + <string name="rtinst_removing_leftovers">Usuwanie pozostałości środowiska</string> + <string name="rtinst_downloading">Pobieranie</string> + <string name="rtinst_verifying">Weryfikowanie</string> + <string name="rtinst_extracting">Wypakowywanie</string> + <string name="rtinst_channel_description">Wyświetlane podczas instalowania lub usuwania środowiska Linux</string> + <string name="rtinst_title_installing">Instalowanie środowiska Linux</string> + <string name="rtinst_title_removing">Usuwanie środowiska Linux</string> + + <string name="deskpkg_runtime_missing">Środowisko Linux nie jest zainstalowane</string> + <string name="deskpkg_no_checksum">Katalog nie zawiera sumy kontrolnej dla %1$s</string> + <string name="deskpkg_checksum_mismatch">Niezgodna suma kontrolna – niczego nie zmieniono</string> + <string name="deskpkg_installing">Instalowanie %1$s</string> + <string name="deskpkg_place_failed">Nie udało się umieścić pliku AppImage</string> + <string name="deskpkg_extract_failed">Wypakowywanie nie powiodło się</string> + <string name="deskpkg_install_failed">Instalacja nie powiodła się</string> + + <string name="flatpakmgr_setup_failed">Konfiguracja Flatpak nie powiodła się (kod %1$d)</string> + <string name="flatpakmgr_setup_unfinished">Konfiguracja Flatpak nie została ukończona</string> + <string name="flatpakmgr_busy">Trwa inne zadanie sklepu</string> + <string name="flatpakmgr_not_ready">Najpierw skonfiguruj Flatpak</string> + <string name="flatpakmgr_starting">Rozpoczynanie</string> + <string name="flatpakmgr_exit_status">Flatpak zakończył działanie z kodem %1$d</string> + <string name="flatpakmgr_installing">Instalowanie %1$s</string> + <string name="flatpakmgr_updating">Aktualizowanie %1$s</string> + <string name="flatpakmgr_removing">Usuwanie %1$s</string> + <string name="flatpakmgr_installing_step">Instalowanie %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_updating_step">Aktualizowanie %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_removing_step">Usuwanie %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_ref_translations">tłumaczeń</string> + <string name="flatpakmgr_ref_graphics_drivers">sterowników graficznych</string> + <string name="flatpakmgr_ref_media_codecs">kodeków multimedialnych</string> + <string name="flatpakmgr_ref_runtime">środowiska %1$s</string> + <string name="flatpakmgr_ref_runtime_branch">środowiska %1$s %2$s</string> + + <string name="appimg_too_small">Plik jest za mały jak na AppImage</string> + <string name="appimg_unreadable">Nie można odczytać pliku: %1$s</string> + <string name="appimg_not_linux">To nie jest AppImage (to nie jest program dla systemu Linux)</string> + <string name="appimg_wrong_arch">Ten AppImage nie jest zbudowany ani dla ARM64 (aarch64), ani dla komputerów x86</string> + <string name="appimg_not_appimage">To program dla systemu Linux, ale nie AppImage</string> + <string name="appimg_wrong_type">Można importować tylko AppImage typu 2 (ten jest typu %1$d)</string> + <string name="appimg_no_space_import">Za mało wolnego miejsca: import wymaga około %1$s</string> + <string name="appimg_no_space_update">Za mało wolnego miejsca: aktualizacja wymaga około %1$s</string> + <string name="appimg_import_failed">Import nie powiódł się</string> + <string name="appimg_gone">Tego AppImage już nie ma</string> + <string name="appimg_update_failed">Aktualizacja nie powiodła się</string> + <string name="appimg_extracting">Wypakowywanie %1$s</string> + <string name="appimg_extract_failed">Nie udało się wypakować AppImage</string> + <string name="appimg_extract_failed_detail">Nie udało się wypakować AppImage: %1$s</string> + + <string name="deckymgr_stop_session_loader">Zatrzymaj aktywną sesję przed instalacją Decky Loader</string> + <string name="deckymgr_download_failed">Pobieranie Decky Loader nie powiodło się</string> + <string name="deckymgr_size_mismatch">Rozmiar Decky Loader nie zgadza się z metadanymi wydania</string> + <string name="deckymgr_checksum_download_failed">Nie udało się pobrać sumy kontrolnej wydania</string> + <string name="deckymgr_checksum_missing">Brak sumy kontrolnej wydania</string> + <string name="deckymgr_checksum_invalid">Suma kontrolna wydania jest nieprawidłowa</string> + <string name="deckymgr_checksum_mismatch">Niezgodna suma kontrolna Decky Loader</string> + <string name="deckymgr_not_elf">Plik wydania nie jest zgodnym 64-bitowym programem dla systemu Linux</string> + <string name="deckymgr_session_started_loader">Podczas pobierania uruchomiono sesję; zatrzymaj ją przed instalacją Decky Loader</string> + <string name="deckymgr_loader_not_executable">Nie udało się oznaczyć PluginLoader jako wykonywalnego</string> + <string name="deckymgr_cef_failed">Nie udało się włączyć zdalnego debugowania CEF w Steam</string> + <string name="deckymgr_stage_failed">Nie udało się przygotować PluginLoader</string> + <string name="deckymgr_loader_preserve_failed">Nie udało się zachować istniejącego PluginLoader</string> + <string name="deckymgr_loader_activate_failed">Nie udało się aktywować nowego PluginLoader</string> + <string name="deckymgr_stop_session_plugin">Zatrzymaj aktywną sesję przed instalacją wtyczki</string> + <string name="deckymgr_loader_required">Zainstaluj Decky Loader przed importem wtyczki</string> + <string name="deckymgr_zip_unreadable">Nie udało się odczytać wybranego pliku ZIP</string> + <string name="deckymgr_plugins_dir_failed">Nie udało się utworzyć katalogu wtyczek Decky</string> + <string name="deckymgr_extracting_zip">Wypakowywanie pliku ZIP wtyczki</string> + <string name="deckymgr_zip_one_folder">ZIP wtyczki musi zawierać jeden główny folder wtyczki</string> + <string name="deckymgr_zip_missing_files">W pliku ZIP wtyczki brakuje plugin.json, package.json lub dist/index.js</string> + <string name="deckymgr_plugin_name_missing">Brak nazwy wtyczki</string> + <string name="deckymgr_session_started_plugin">Podczas instalacji uruchomiono sesję; zatrzymaj ją przed instalacją wtyczki</string> + <string name="deckymgr_plugin_installed">Zainstalowano wtyczkę · uruchom ponownie Steam, aby ją wczytać</string> + <string name="deckymgr_plugin_install_failed">Instalacja wtyczki z pliku ZIP nie powiodła się</string> + <string name="deckymgr_zip_too_many">ZIP wtyczki zawiera zbyt wiele wpisów</string> + <string name="deckymgr_zip_invalid_path">ZIP wtyczki zawiera nieprawidłową ścieżkę</string> + <string name="deckymgr_zip_duplicate_paths">ZIP wtyczki zawiera powielone ścieżki</string> + <string name="deckymgr_zip_unsafe_path">ZIP wtyczki zawiera niebezpieczną ścieżkę</string> + <string name="deckymgr_folder_failed">Nie udało się utworzyć folderu wtyczki</string> + <string name="deckymgr_zip_too_large">Po rozpakowaniu ZIP wtyczki przekracza limit 1 GiB</string> + <string name="deckymgr_bundled_not_executable">Nie udało się oznaczyć dołączonego pliku binarnego wtyczki jako wykonywalnego</string> + <string name="deckymgr_remote_entry_invalid">Nieprawidłowy wpis remote_binary</string> + <string name="deckymgr_remote_name_invalid">Nieprawidłowa nazwa zdalnego pliku binarnego</string> + <string name="deckymgr_remote_url_https">URL zdalnego pliku binarnego musi używać HTTPS</string> + <string name="deckymgr_remote_hash_invalid">Zdalny plik binarny %1$s ma nieprawidłowy skrót SHA-256</string> + <string name="deckymgr_bin_dir_failed">Nie udało się utworzyć katalogu bin wtyczki</string> + <string name="deckymgr_downloading_binary">Pobieranie pliku binarnego wtyczki · %1$s</string> + <string name="deckymgr_binary_download_failed">Nie udało się pobrać pliku binarnego wtyczki %1$s</string> + <string name="deckymgr_binary_checksum_mismatch">Niezgodna suma kontrolna pliku binarnego wtyczki · %1$s</string> + <string name="deckymgr_binary_not_executable">Nie udało się oznaczyć pliku binarnego wtyczki jako wykonywalnego · %1$s</string> + <string name="deckymgr_binary_replace_failed">Nie udało się zastąpić pliku binarnego wtyczki · %1$s</string> + <string name="deckymgr_binary_install_failed">Nie udało się zainstalować pliku binarnego wtyczki · %1$s</string> + <string name="deckymgr_folder_taken">Folder wtyczki należy do innej wtyczki</string> + <string name="deckymgr_preserve_failed">Nie udało się zachować istniejącej wtyczki</string> + <string name="deckymgr_activate_failed">Nie udało się aktywować zaimportowanej wtyczki</string> + <string name="deckymgr_settings_failed">Nie udało się zaktualizować ustawień wtyczek Decky</string> + <string name="deckymgr_restore_failed">Nie udało się przywrócić istniejącej wtyczki</string> + + <string name="adbfix_save_failed">Nie udało się zapisać tożsamości debugowania bezprzewodowego</string> + <string name="adbfix_command_failed">Polecenie ADB nie powiodło się (%1$d)</string> + <string name="adbfix_not_confirmed">Android nie potwierdził zmiany limitu procesów podrzędnych: %1$s</string> + <string name="adbfix_no_pairing">Brak zapisanego parowania debugowania bezprzewodowego</string> + <string name="adbfix_enter_address">Wpisz bieżący adres IP i port z ekranu Debugowanie bezprzewodowe</string> + + <string name="adbpair_timed_out">Upłynął limit czasu oczekiwania na okienko parowania. Spróbuj ponownie z poziomu DroidDeck.</string> + <string name="adbpair_discovery_failed_code">Nie udało się wyszukać okienka parowania (%1$d). Sprawdź, czy Wi-Fi jest włączone.</string> + <string name="adbpair_discovery_failed">Nie udało się wyszukać okienka parowania: %1$s</string> + <string name="adbpair_six_digits">Wpisz wszystkie 6 cyfr kodu</string> + <string name="adbpair_pairing">Parowanie…</string> + <string name="adbpair_code_rejected">Ten kod nie zadziałał. Otwórz ponownie okienko parowania i wpisz nowy kod.</string> + <string name="adbpair_connecting">Sparowano. Łączenie…</string> + <string name="adbpair_port_not_found">Sparowano, ale nie znaleziono portu debugowania bezprzewodowego. Nie wyłączaj debugowania bezprzewodowego i spróbuj ponownie.</string> + <string name="adbpair_applying">Stosowanie ustawienia…</string> + <string name="adbpair_command_failed">Polecenie debugowania bezprzewodowego nie powiodło się</string> + <string name="adbpair_channel_name">Konfiguracja debugowania bezprzewodowego</string> + <string name="adbpair_channel_description">Prosi o kod parowania debugowania bezprzewodowego, gdy Ustawienia są otwarte</string> + <string name="adbpair_title_pair">Sparuj debugowanie bezprzewodowe</string> + <string name="adbpair_waiting_text">Włącz Debugowanie bezprzewodowe, a potem dotknij „Sparuj urządzenie za pomocą kodu parowania”.</string> + <string name="adbpair_waiting_long">Włącz Debugowanie bezprzewodowe, a potem dotknij „Sparuj urządzenie za pomocą kodu parowania”. Pozostań w Ustawieniach: kod wpiszesz w tym powiadomieniu.</string> + <string name="adbpair_code_label">6-cyfrowy kod parowania</string> + <string name="adbpair_code_hint">Dotknij „Wpisz kod” i podaj 6 cyfr widocznych w Ustawieniach.</string> + <string name="adbpair_title_code">Wpisz kod parowania Wi-Fi</string> + <string name="adbpair_title_retry">Spróbuj wpisać kod ponownie</string> + <string name="adbpair_enter_code">Wpisz kod</string> + <string name="adbpair_title_working">Konfigurowanie Steam</string> + <string name="adbpair_title_done">Steam jest gotowy</string> + <string name="adbpair_done_text">Dotknij, aby wrócić do DroidDeck. Możesz już wyłączyć debugowanie bezprzewodowe.</string> + <string name="adbpair_title_failed">Konfiguracja debugowania bezprzewodowego została przerwana</string> + + <string name="selfinst_http">Serwer pobierania odpowiedział HTTP %1$d</string> + <string name="selfinst_size_mismatch">Rozmiar pobranego pliku nie zgadzał się z opublikowanym APK, więc go odrzucono – spróbuj ponownie</string> + <string name="selfinst_checksum_mismatch">Pobrany plik nie zgadzał się z sumą kontrolną, więc go odrzucono – spróbuj ponownie</string> + <string name="selfinst_wrong_package">Aktualizacja jest dla pakietu Android %1$s, a nie %2$s</string> + <string name="selfinst_metadata_signer">Metadane aktualizacji nie wskazują klucza podpisywania wydań DroidDeck</string> + <string name="selfinst_unreadable">Android nie mógł odczytać pobranego APK</string> + <string name="selfinst_apk_wrong_package">Pobrany APK jest dla %1$s, a nie %2$s</string> + <string name="selfinst_version_mismatch">Pobrany APK ma versionCode %1$d, a nie opublikowany %2$d</string> + <string name="selfinst_downgrade">Android nie zainstaluje versionCode %1$d na zainstalowanym %2$d</string> + <string name="selfinst_no_signing">Pobrany APK nie zawiera informacji o podpisie</string> + <string name="selfinst_wrong_signer">Pobrany APK nie jest podpisany kluczem wydań DroidDeck</string> + <string name="appupd_http">Katalog aktualizacji odpowiedział HTTP %1$d</string> + + <string name="loadstate_starting">Uruchamianie sesji…</string> + <string name="loadstate_elapsed">Minęło %1$d:%2$02d · nadal trwa</string> + <string name="loadstate_steam_update">Pobieranie aktualizacji klienta Steam · %1$d%%</string> + <string name="loadstate_steam_client">Pobieranie klienta Steam · %1$s</string> + + <string name="pextra_busy">Instalacja narzędzia zgodności już trwa</string> + <string name="pextra_stop_session">Zatrzymaj aktywną sesję przed instalacją narzędzi zgodności</string> + <string name="pextra_no_release">Nie znaleziono wydania ARM64 dla %1$s</string> + <string name="pextra_no_space">Do instalacji %1$s potrzeba co najmniej 4 GB wolnego miejsca plus rozmiar pobierania</string> + <string name="pextra_downloading">Pobieranie %1$s %2$s</string> + <string name="pextra_retrying">Ponawianie pobierania · próba %1$d z %2$d</string> + <string name="pextra_interrupted">Pobieranie przerwane</string> + <string name="pextra_download_failed">Pobieranie nie powiodło się; częściowy plik zachowano, aby można było je wznowić</string> + <string name="pextra_too_big">Pobierany plik przekroczył rozmiar wydania i został usunięty</string> + <string name="pextra_incomplete">Pobieranie było niepełne; częściowy plik zachowano, aby można było je wznowić</string> + <string name="pextra_verifying">Weryfikowanie pobranego pliku</string> + <string name="pextra_checksum_unreadable">Nie udało się odczytać sumy kontrolnej wydania; spróbuj ponownie</string> + <string name="pextra_no_checksum">%1$s %2$s nie publikuje sumy kontrolnej; niczego nie zainstalowano</string> + <string name="pextra_checksum_mismatch">Niezgodna suma kontrolna; pobrany plik usunięto</string> + <string name="pextra_session_started">Podczas pobierania uruchomiono sesję; zatrzymaj ją przed instalacją narzędzi zgodności</string> + <string name="pextra_installing">Instalowanie %1$s</string> + <string name="pextra_install_failed">Instalacja %1$s nie powiodła się (kod %2$d)</string> + <string name="pextra_could_not_install">Nie udało się zainstalować %1$s</string> + <string name="esync_fetching">Pobieranie pakietu droiddeck-esync</string> + + <string name="cmgr_fex_linux_helpers">z modułami pomocniczymi Linux</string> + <string name="cmgr_fex_dlls_only">tylko biblioteki DLL</string> + <string name="cmgr_original_proton_updated">Oryginał (Proton zaktualizowany od ostatniej zamiany)</string> + <string name="cmgr_changed_outside">Zmieniono poza sekcją Komponenty</string> + <string name="cmgr_not_present">brak</string> + <string name="cmgr_original_version">Oryginał %1$s</string> + <string name="cmgr_original_from">Oryginał z %1$s</string> + <string name="cmgr_swap_queued">%1$s %2$s trafi do %3$s, gdy zostanie zamknięta działająca na nim gra.</string> + <string name="cmgr_swap_done">%1$s %2$s jest teraz w %3$s. Zadziała przy następnym uruchomieniu gry.</string> + <string name="cmgr_original_missing">Ten oryginalny pakiet nie jest przechowywany</string> + <string name="cmgr_restore_queued">%1$s w %2$s wróci do oryginału, gdy zostanie zamknięta działająca na nim gra.</string> + <string name="cmgr_restore_done">%1$s w %2$s przywrócono z oryginału %3$s.</string> + <string name="cmgr_package_in_use">Pakiet %1$s jest używany w %2$s. Najpierw przełącz ten Proton na coś innego.</string> + <string name="cmgr_package_not_stored">Pakiet %1$s nie jest przechowywany</string> + <string name="cmgr_deleted">Usunięto %1$s.</string> + <string name="cmgr_original_kept">Oryginał zainstalowanej wersji zostaje zachowany: to droga powrotu.</string> + <string name="cmgr_not_stored">Nie przechowywano</string> + <string name="cmgr_deleted_original">Usunięto oryginał %2$s z %1$s.</string> + <string name="cmgr_nightlies_only">Pobieranie odbywa się tylko z wydań Nightlies</string> + <string name="cmgr_list_predates_checksums">Ta lista pakietów jest starsza niż sumy kontrolne – odśwież ją i spróbuj ponownie</string> + <string name="cmgr_checksum_mismatch">Niezgodna suma kontrolna</string> + + <string name="sprefs_upscaler_linear">Liniowy</string> + <string name="sprefs_upscaler_nearest">Najbliższy sąsiad</string> + <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (jakość)</string> + <string name="sprefs_upscaler_sharpen">Tylko wyostrzanie</string> + <string name="sprefs_lod_bias_auto">Auto (zgodnie ze skalowaniem)</string> + + <string name="look_off_desc">Bez efektów.</string> + <string name="look_game_clarity">Wyrazistość</string> + <string name="look_game_clarity_desc">Wyostrzenie z odrobiną kontrastu – na co dzień.</string> + <string name="look_vivid">Żywe kolory</string> + <string name="look_vivid_desc">Mocno podbite kolory. Debanding włączony, by niebo nie miało pasów.</string> + <string name="look_cinematic">Kinowy</string> + <string name="look_cinematic_desc">Głębszy kontrast, chłodniejsze kolory, wygładzone krawędzie.</string> + <string name="look_competitive">Rywalizacja</string> + <string name="look_competitive_desc">Rozjaśnione cienie i mocne wyostrzenie – zauważ przeciwnika pierwszy.</string> + <string name="look_adaptive_sharpen">Adaptacyjne wyostrzanie</string> + <string name="look_adaptive_sharpen_desc">Tylko wyostrzanie, kolory bez zmian.</string> + <string name="look_filmic">Filmowy</string> + <string name="look_filmic_desc">Krzywa jak z taśmy filmowej, lekko stonowana.</string> + <string name="look_arcade">Salon gier</string> + <string name="look_arcade_desc">Głośno i z pazurem.</string> + <string name="look_retro_crt">Retro CRT</string> + <string name="look_retro_crt_desc">Kineskop CRT i analogowy sygnał razem.</string> + <string name="look_upscale_sharp">Ostre skalowanie</string> + <string name="look_upscale_sharp_desc">Do grania poniżej rozdzielczości ekranu.</string> + <string name="look_pixel_clean">Czyste piksele</string> + <string name="look_pixel_clean_desc">Ostre piksele dla gier 2D i starych tytułów.</string> + <string name="look_anime_edge">Krawędzie anime</string> + <string name="look_anime_edge_desc">Kreskówkowe kontury i wyostrzenie.</string> + + <string name="gpuinfo_not_adreno">To nie jest GPU Adreno</string> + <string name="gpuinfo_supported">Obsługiwany</string> + <string name="gpuinfo_experimental">Eksperymentalny: jego sterowniki to wersje testowe</string> + <string name="gpuinfo_below_tested">Poniżej testowanego sprzętu (Adreno 725 i nowsze): może nie działać</string> + <string name="gpuinfo_unsupported">Nieobsługiwany: DroidDeck wymaga GPU Adreno (Snapdragon)</string> + + <string name="turnip_rate_limited">Osiągnięto limit zapytań GitHub – spróbuj ponownie później</string> + <string name="turnip_github_http">GitHub odpowiedział HTTP %1$d</string> + <string name="turnip_download_http">serwer pobierania odpowiedział HTTP %1$d</string> + <string name="turnip_list_predates_checksums">Ta lista sterowników jest starsza niż sumy kontrolne – odśwież ją i spróbuj ponownie</string> + <string name="turnip_checksum_mismatch">Niezgodna suma kontrolna – pobrany plik odrzucono</string> + <string name="turnip_not_adrenotools">To nie jest sterownik AdrenoTools: %1$s</string> + <string name="turnip_reject_no_meta">brak meta.json</string> + <string name="turnip_reject_meta_unreadable">nie można odczytać meta.json (%1$s)</string> + <string name="turnip_reject_linux_driver">to sterownik środowiska Linux (zaimportuj go w sekcji „Sterownik środowiska Linux”)</string> + <string name="turnip_reject_wayland">to sterownik gier Wayland, który nie jest przydatny w tej aplikacji</string> + <string name="turnip_reject_no_library_name">meta.json nie podaje libraryName, więc nie można niczego przekazać do AdrenoTools</string> + <string name="turnip_reject_library_missing">meta.json wskazuje %1$s, którego nie ma w pliku zip</string> + <string name="turnip_reject_not_aarch64">%1$s nie jest 64-bitową biblioteką współdzieloną ELF AArch64</string> + <string name="turnip_reject_glibc">%1$s linkuje glibc – to Turnip „-Linux”, który należy do sekcji „Sterownik środowiska Linux”</string> + <string name="lvd_no_library">Brak libvulkan_freedreno*.so w tym pliku zip. Zip z Turnip dla Android (AdrenoTools) lub -Wayland nie jest sterownikiem środowiska Linux.</string> + <string name="lvd_not_aarch64">%1$s nie jest 64-bitową biblioteką współdzieloną ELF AArch64.</string> + <string name="lvd_not_glibc">%1$s nie jest sterownikiem glibc – linkuje libc systemu Android. Środowisko Linux wymaga pliku zip „-Linux”; zwykłego Turnip ani wersji „-Wayland” klient Steam w ogóle nie wczyta.</string> + <string name="bundle_no_manifest">To nie jest pakiet sterowników: brak manifest.json wskazującego sterownik Android i Linux</string> + <string name="bundle_entry_outside">To nie jest pakiet sterowników: %1$s wskazuje poza niego</string> + <string name="bundle_too_large">To nie jest pakiet sterowników: po rozpakowaniu przekracza %1$d MB</string> + + <string name="gstore_sd_card">Karta SD</string> + <string name="gstore_folder">Folder</string> + + <string name="logshare_chooser">Udostępnij logi sesji</string> + + <string name="hdr_needs_android_10">wymaga systemu Android 10 lub nowszego (warstwy wyświetlania z przestrzenią barw)</string> + <string name="hdr_none">ten wyświetlacz nie zgłasza obsługi HDR</string> + <string name="hdr_not_hdr10">ten wyświetlacz obsługuje %1$s, ale nie HDR10</string> + + <string name="gpu_this_gpu">ten GPU</string> + <string name="phantom_title_on">Limit procesów podrzędnych jest włączony</string> + <string name="phantom_title_unset">Limit procesów podrzędnych nie jest ustawiony</string> + <string name="phantom_title_unreadable">Nie udało się sprawdzić limitu procesów podrzędnych</string> + <string name="phantom_title_off">Limit procesów podrzędnych jest wyłączony</string> + <string name="phantom_title_not_required">Limit procesów podrzędnych: nie dotyczy</string> + <string name="phantom_instructions_fix">%1$s Jeśli to niemożliwe, podłącz to urządzenie do komputera z ADB i uruchom poniższe polecenie. Wróć tutaj i sprawdź ponownie.</string> + <string name="phantom_instructions_off">Sesje Steam mogą się uruchamiać.</string> + <string name="phantom_instructions_not_applicable">Ta wersja systemu Android nie używa tego limitu.</string> + <string name="phantom_fix_toggle">Włącz „Wyłącz ograniczenia procesów podrzędnych” w Opcjach programisty i nie wyłączaj potem Opcji programisty.</string> + <string name="phantom_fix_no_toggle">Ta wersja systemu Android nie ma dla niego przełącznika. Użyj debugowania bezprzewodowego, aby zmienić go z tego urządzenia.</string> + <string name="upd_block_no_apk">Ta wersja nie ma pliku do pobrania dla tej kopii DroidDeck.</string> + <string name="upd_block_version_code">Android nie zainstaluje jej na tej wersji, bo jej versionCode %1$d jest niższy niż zainstalowany %2$d.</string> + <string name="upd_fail_downgrade">Android nie zainstaluje starszej wersji na nowszej. Zostań przy tej wersji, aż kanał ją dogoni.</string> + <string name="upd_fail_signature">Ta kopia DroidDeck ma inny podpis (lokalna kompilacja?), więc Android nie zaktualizuje jej w miejscu.</string> + <string name="upd_fail_storage">Za mało wolnego miejsca na aktualizację.</string> + <string name="upd_fail_aborted">Aktualizacja została anulowana.</string> + <string name="upd_fail_with_message">Aktualizacja nie została zainstalowana: %1$s</string> + <string name="upd_fail_generic">Aktualizacja nie została zainstalowana.</string> + <string name="saves_layout_zip">archiwum zip %1$s</string> + <string name="saves_zip">archiwum zip</string> + <string name="saves_not_launched">Gra %1$s nie była jeszcze uruchamiana</string> + <string name="saves_none_found">Nie znaleziono plików zapisu dla %1$s</string> + <string name="saves_not_save_zip">Plik %1$s nie zawiera folderu drive_c, więc nie jest archiwum zapisów GameHub ani Winlator</string> + <string name="gstore_cannot_create">nie można tworzyć folderów w %1$s</string> + <string name="gstore_cannot_write">nie można zapisywać w %1$s</string> + <string name="gstore_cannot_write_detail">nie można zapisywać w %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">Folder jest niedostępny</string> + <string name="gfiles_name_taken">Inny plik już używa nazwy %1$s</string> + <string name="gfiles_art_unavailable">Folder grafik jest niedostępny</string> + <string name="gfiles_unrelated_inventory">Folder zawiera niepowiązany spis synchronizacji</string> + <string name="gfiles_remove_failed">Nie udało się usunąć %1$s</string> + <string name="gfiles_outside_folder">Plik wychodzi poza folder eksportu</string> + <string name="cmgr_fex_unnamed">FEX (wersja bez nazwy)</string> + <string name="cmgr_no_profile">brak profile.json w %1$s</string> + <string name="cmgr_not_component_package">%1$s nie jest pakietem komponentu Linux (typ %2$s)</string> + <string name="gstore_volume_free">%1$s · wolne: %2$s</string> + <string name="mode_storage_auto_short">Automatycznie</string> + <string name="appupd_catalog_rejected">Katalog aktualizacji nie przeszedł weryfikacji (%1$s)</string> + <string name="main_adb_clip_label">Ustawienie procesów podrzędnych DroidDeck</string> + <string name="setup_signed_in_offline_unnamed">Zalogowano · tryb offline</string> + <string name="setup_signed_in_unnamed">Zalogowano</string> + <string name="driver_tag_bundled">DOŁĄCZONY</string> + <string name="driver_tag_downloaded">POBRANY</string> + <string name="driver_tag_imported">ZAIMPORTOWANY</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Ekran sesji</string> + <string name="display_resolution">Rozdzielczość</string> + <string name="display_game_fit">Dopasowanie gier 16:9</string> + <string name="display_game_fit_hint">Jak gry 16:9 mieszczą się na tym ekranie. Od następnej sesji.</string> + <string name="display_game_fit_unavailable">Wybierz „Dopasuj do ekranu” lub własny rozmiar węższy niż 16:9.</string> + <string name="display_preserve">Zachowaj proporcje</string> + <string name="display_stretch">Rozciągnij do ekranu</string> + <string name="display_image_scaling">Skalowanie obrazu</string> + <string name="display_filter">Filtr skalowania</string> + <string name="display_filter_hint">Wspólny dla Steam i Pulpitu.</string> + <string name="display_filter_live">Działa od razu. Wspólny dla Steam i Pulpitu.</string> + <string name="display_filter_note">FSR, GSR i NIS działają przy powiększaniu obrazu sesji. „Tylko wyostrzanie” działa też bez powiększania.</string> + <string name="display_sharpness">Ostrość filtra</string> + <string name="display_frame_rate">Liczba klatek</string> + <string name="display_look_filter">%1$s Ustawia skalowanie na %2$s.</string> + <string name="display_match_screen">Dopasuj do ekranu (%1$d×%2$d)</string> + <string name="display_follow_screen">Dopasuj do ekranu (zmienia rozmiar po złożeniu lub rozłożeniu)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Pokaż szczegóły urządzenia</string> + <string name="device_info_hide">Skryj szczegóły urządzenia</string> + <string name="device_info_loading">Odczytywanie urządzenia…</string> + <string name="device_info_copy">Kopiuj</string> + <string name="device_info_copy_hint">Skopiuj te szczegóły do zgłoszenia błędu.</string> + <string name="device_info_copied">Skopiowano.</string> + <string name="force_ssbs_title">Wymuś SSBS</string> + <string name="force_ssbs_hint">Utrzymuje ustawienie szybkości CPU, które Wine wciąż wyłącza. Może przyspieszyć gry Windows na Snapdragon 8 Elite; na starszych chipsetach niewielka różnica. Działa od następnego uruchomienia gry.</string> + + <string name="rail_stores">Sklepy</string> + <string name="source_steam">Steam</string> + <string name="source_added">Własna</string> + <string name="setup_stores">Sklepy</string> + <string name="setup_stores_open_on">Otwórz sklep w</string> + <string name="setup_stores_speed">Poziom szybkości pobierania</string> + <string name="setup_stores_engine">Mechanizm sklepów</string> + <string name="setup_stores_engine_chip_ready">● Mechanizm gotowy</string> + <string name="setup_stores_engine_chip_missing">Nie zbudowano</string> + <string name="stores_tier_balanced">Zbalansowany</string> + <string name="stores_tier_fast">Szybki</string> + <string name="stores_tier_max">Maksymalny</string> + <string name="stores_title">Sklepy</string> + <string name="stores_settings">Ustawienia sklepów</string> + <string name="stores_settings_eyebrow">Ustawienia</string> + <string name="stores_chip_signed_out">(niezalogowano)</string> + <string name="stores_tab_store">Sklep</string> + <string name="stores_tab_installed">Zainstalowane</string> + <string name="stores_tab_library">Biblioteka</string> + <string name="stores_tab_all">Wszystkie</string> + <string name="stores_tab_downloads">Pobierania</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Zaloguj się</string> + <string name="stores_signout_named">%1$s · wyloguj się</string> + <string name="stores_signout_title">Wylogować się z %1$s?</string> + <string name="stores_signout_note">Zainstalowane gry zostają. Biblioteka zostanie pobrana ponownie po zalogowaniu.</string> + <string name="stores_signout_stay">Zostań zalogowany</string> + <string name="stores_signout_go">Wyloguj się</string> + <string name="stores_search_catalog">Szukaj w katalogu %1$s…</string> + <string name="stores_search_installed">Szukaj zainstalowanych gier…</string> + <string name="stores_search_library">Szukaj w Twojej bibliotece…</string> + <string name="stores_search_open">Szukaj</string> + <string name="stores_installed_empty">Nic nie zainstalowano</string> + <string name="stores_library_loading">Wczytywanie</string> + <string name="stores_nothing_matches">Brak gier</string> + <string name="stores_shelves_loading">Wczytywanie sklepu…</string> + <string name="stores_amazon_no_catalog">Brak katalogu</string> + <string name="stores_shelf_new">Nowości</string> + <string name="stores_shelf_deals">Promocje</string> + <string name="stores_shelf_free">Darmowe gry</string> + <string name="stores_shelf_trending">Popularne</string> + <string name="stores_shelf_library">Twoja biblioteka</string> + <string name="stores_play">Graj</string> + <string name="stores_install">Zainstaluj</string> + <string name="stores_resume_install">Wznów instalację</string> + <string name="stores_dl_interrupted">Przerwano</string> + <string name="stores_install_size">Instalacja · %1$s</string> + <string name="stores_get_free">Odbierz bezpłatnie</string> + <string name="stores_view_on">Zobacz w %1$s</string> + <string name="stores_buy_price">Kup · %1$s</string> + <string name="stores_installed_chip">Zainstalowano</string> + <string name="stores_in_library">W bibliotece</string> + <string name="stores_free">Bezpłatna</string> + <string name="stores_resume">Wznów</string> + <string name="stores_refresh">Odśwież</string> + <string name="stores_show_mature">Pokaż treści dla dorosłych</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Offline</string> + <string name="epic_card_none">Wyłączone</string> + <string name="epic_eos">Logowanie Epic (EOS)</string> + <string name="epic_offline">Uruchom offline</string> + <string name="epic_resolve">Rozwiąż logowanie Epic</string> + <string name="store_cloud_saves">Zapisy w chmurze</string> + <string name="cloud_upload">Wyślij do chmury</string> + <string name="cloud_download">Pobierz z chmury</string> + <string name="cloud_none">Brak zapisów w chmurze</string> + <string name="cloud_conflict">Konflikt w chmurze</string> + <string name="cloud_conflict_files">Konflikt w chmurze · %1$d plików</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Zachowaj z chmury</string> + <string name="cloud_keep_local">Zachowaj lokalne</string> + <string name="cloud_checking">Sprawdzanie chmury…</string> + <string name="cloud_last_sync">Ostatnia synchronizacja %1$s</string> + <string name="cloud_never">Jeszcze nie zsynchronizowano</string> + <string name="cloud_done">%1$d plików</string> + <string name="stores_eyebrow_installed">zainstalowana</string> + <string name="stores_eyebrow_library">w Twojej bibliotece</string> + <string name="stores_eyebrow_store">sklep</string> + <string name="achievements_title">Osiągnięcia</string> + <string name="achievements_count">%1$d z %2$d odblokowanych</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Odblokowane · %1$d</string> + <string name="achievements_group_locked">Zablokowane · %1$d</string> + <string name="achievements_group_hidden">Ukryte · %1$d</string> + <string name="achievements_hidden">Ukryte osiągnięcie</string> + <string name="achievements_hidden_detail">Szczegóły są widoczne po odblokowaniu.</string> + <string name="achievements_all">Wszystkie osiągnięcia odblokowane</string> + <string name="achievements_reading">Wczytywanie…</string> + <string name="achievements_signed_out">Zaloguj się do %1$s, aby zobaczyć swoje osiągnięcia.</string> + <string name="achievements_unreachable">Nie udało się połączyć z %1$s. Sprawdź ponownie, gdy będziesz online.</string> + <string name="achievements_none">%1$s nie wyświetla osiągnięć dla tej gry.</string> + <string name="achievements_show_all">Pokaż wszystkie (%1$d)</string> + <string name="achievements_dialog_title">Osiągnięcia · %1$s</string> + <string name="stores_in_steam">✓ W Steam</string> + <string name="stores_uninstall">Odinstaluj</string> + <string name="stores_uninstalling">Odinstalowywanie…</string> + <string name="stores_uninstall_title">Odinstalować %1$s?</string> + <string name="stores_uninstall_body_size">Zwolni %1$s i usunie ją ze Steam.</string> + <string name="stores_uninstall_body">Usuwa jej pliki i skrót w Steam.</string> + <string name="stores_launch_not_registered">Steam wykryje grę przy następnym uruchomieniu; uruchom ją stamtąd albo uruchom teraz Steam.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Wybór Steam dla każdej gry</string> + <string name="stores_card_audio">Dźwięk</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Komponenty</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Mechanizm</string> + <string name="stores_card_engine_value">%1$s · natywny</string> + <string name="stores_card_engine_java">%1$s · wbudowany</string> + <string name="stores_how_it_runs">Jak to działa</string> + <string name="stores_kv_fetched">Pobrano z</string> + <string name="stores_kv_fetched_value">%1$s, z Twoim kontem</string> + <string name="stores_kv_installed_to">Zainstalowano w</string> + <string name="stores_kv_installed_to_value">Miejsce na gry (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Uruchamiane przez</string> + <string name="stores_kv_launched_by_value">Steam i Proton, tą samą ścieżką co gry Steam</string> + <string name="stores_kv_launched_by_epic">Steam i Proton · logowanie Epic przy każdym uruchomieniu, nakładka EOS wyłączona</string> + <string name="stores_kv_launched_by_amazon">Steam i Proton · ustawione zmienne Amazon Games Services</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">dodawana do Steam przy instalacji (interfejs Deck + Steam Input)</string> + <string name="stores_downloads_empty">Brak pobierań</string> + <string name="stores_dl_paused">Wstrzymano</string> + <string name="stores_dl_queued">W kolejce</string> + <string name="stores_dl_queued_at">W kolejce · #%1$d</string> + <string name="stores_dl_cancelled">Anulowano</string> + <string name="stores_dl_failed">Niepowodzenie</string> + <string name="stores_dl_failed_reason">Niepowodzenie · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Wstrzymaj</string> + <string name="stores_dl_resume">Wznów</string> + <string name="stores_dl_cancel">Anuluj</string> + <string name="stores_dl_retry">Spróbuj ponownie</string> + <string name="stores_dl_clear">Wyczyść</string> + <string name="stores_dl_clear_all">Wyczyść zakończone</string> + <string name="stores_stage_manifest">Manifest</string> + <string name="stores_stage_download">Pobieranie</string> + <string name="stores_stage_verify">Weryfikacja</string> + <string name="stores_stage_install">Instalacja</string> + <string name="stores_stage_done">Gotowe</string> + <string name="stores_stage_checking">Sprawdzanie</string> + <string name="stores_stage_downloading">Pobieranie</string> + <string name="stores_dl_cancel_confirm">Usunąć pobieranie?</string> + <string name="stores_dl_failed_kept">Niepowodzenie · %1$s zachowano</string> + <string name="stores_stage_verifying">Weryfikowanie</string> + <string name="stores_stage_installing">Instalowanie</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d plików</string> + <string name="stores_download_manager">Menedżer pobierania</string> + <string name="stores_parallel">Pobierania naraz</string> + <string name="stores_install_where">Instaluj w</string> + <string name="stores_install_title">Zainstaluj %1$s:</string> + <string name="stores_target_internal">Wewnętrzna</string> + <string name="stores_target_free">%1$s wolnego miejsca</string> + <string name="stores_notification_channel">Pobierania ze sklepów</string> + <string name="stores_notification_channel_hint">Pokazuje postęp pobierania gier i utrzymuje pobierania w tle</string> + <string name="stores_notification_one">Pobieranie</string> + <string name="stores_notification_many">%1$d pobierań</string> + <string name="stores_notification_waiting">Przygotowywanie…</string> + <string name="stores_notification_done_channel">Zakończone pobierania ze sklepów</string> + <string name="stores_notification_installed">Zainstalowano %1$s</string> + <string name="stores_notification_failed">Pobieranie %1$s nie powiodło się</string> + <string name="stores_notification_delete">Usuń</string> + <string name="stores_notification_keep">Zachowaj</string> + <string name="stores_login_title">Zaloguj się do %1$s</string> + <string name="stores_login_provider_title">Zaloguj się przez swojego dostawcę</string> + <string name="stores_login_reload">Odśwież</string> + <string name="stores_login_close">Zamknij</string> + <string name="stores_login_retry">Spróbuj ponownie</string> + <string name="stores_login_finishing">Kończenie logowania…</string> + <string name="stores_login_error_network">Nie udało się wczytać strony logowania: %1$s</string> + <string name="stores_login_error_idp_blocked">Ten dostawca odmawia logowania z osadzonej strony. Zaloguj się za pomocą hasła do sklepu.</string> + <string name="stores_login_error_http">Strona logowania odpowiedziała błędem (HTTP %1$d).</string> + <string name="stores_login_error_ssl">Połączenie ze stroną logowania nie jest bezpieczne.</string> + <string name="stores_login_error_verification">Nie udało się zweryfikować logowania. Spróbuj ponownie.</string> + <string name="stores_login_error_generic">Logowanie nie powiodło się. Spróbuj ponownie.</string> + <string name="common_close">Zamknij</string> + <string name="common_back">Wstecz</string> + <string name="common_edit">Edytuj</string> + <string name="games_add_all">Dodaj wszystkie gry z tego folderu</string> + <string name="games_added_none">Brak nowych gier w tym folderze</string> + <string name="edit_name">Nazwa</string> + <string name="edit_target">Element docelowy</string> + <string name="edit_start_in">Uruchom w</string> + <string name="edit_launch_options">Opcje uruchamiania</string> + <string name="edit_artwork">Grafika</string> + <string name="edit_other_exe">Inny plik exe…</string> + <string name="edit_remove">Usuń</string> + <string name="art_cover">Okładka</string> + <string name="art_background">Tło</string> + <string name="art_logo">Logo</string> + <string name="art_icon">Ikona</string> + <string name="art_src_folder">Folder gry</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">Twój plik</string> + <string name="art_src_exe">Z pliku exe</string> + <string name="art_src_auto">Automatycznie</string> + <string name="art_pick_image">Wybierz obraz…</string> + <string name="art_pick_title">Wybierz obraz: %1$s</string> + <string name="art_reset">Przywróć automatyczne</string> + <plurals name="games_added_n"> + <item quantity="one">Dodano %1$d grę</item> + <item quantity="few">Dodano %1$d gry</item> + <item quantity="many">Dodano %1$d gier</item> + <item quantity="other">Dodano %1$d gry</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="one">%1$d już w Grach</item> + <item quantity="few">%1$d już w Grach</item> + <item quantity="many">%1$d już w Grach</item> + <item quantity="other">%1$d już w Grach</item> + </plurals> + <string name="hop_back">B · Wstecz do %1$s</string> + <string name="hop_from_games">gry</string> + <string name="hop_from_play">Ustawień uruchamiania</string> + <string name="sheet_graphics_drivers">Sterowniki graficzne</string> + <string name="comp_this_proton">Ten Proton</string> + <string name="comp_every_proton">Każdy Proton</string> + <string name="comp_for_every_proton">Dla każdego Protona</string> + <string name="comp_available_count">Dostępne · %1$d</string> + <string name="comp_checked">Sprawdzono %1$s</string> + <string name="comp_check_all">Sprawdź nowe komponenty i sterowniki</string> + <string name="comp_get">Pobierz</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton i FEX poniżej należą do tego Protona</string> + <string name="comp_device_tested">%1$s · przetestowany</string> + <string name="comp_device_untested">%1$s · nieprzetestowany</string> + <string name="comp_device_unsupported">%1$s · nieobsługiwany</string> + <string name="gpu_auto_keeps">Utrzymuje polecaną parę aktualną</string> + <string name="gpu_bar_auto">Auto · %1$s</string> + <string name="gpu_separately">Sterowniki środowiska i wyświetlania osobno</string> + <string name="gpu_use_title">Użyć %1$s?</string> + <string name="proton_remove_title">Usunąć %1$s?</string> + <string name="setup_tab_diagnostics">Diagnostyka</string> + <string name="setup_ready">Gotowe do gry</string> + <string name="setup_not_ready">Jeszcze nie gotowe do gry</string> + <string name="setup_ready_runtime">środowisko %1$s</string> + <string name="setup_ready_limit_off">limit procesów wyłączony</string> + <string name="setup_ready_signed_in">zalogowano jako %1$s</string> + <string name="setup_needs_you">Gdy coś wymaga Twojej uwagi</string> + <string name="setup_group_look">Wygląd</string> + <string name="setup_group_device">To urządzenie</string> + <string name="setup_group_art">Grafika biblioteki</string> + <string name="flathub_eyebrow">APLIKACJE LINUX</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Aplikacje pulpitowe dla pulpitu Linux, otwierane z Pulpitu.</string> + <string name="flathub_setup">Skonfiguruj Flathub</string> + <string name="flathub_remove_title">Usunąć %1$s?</string> + <string name="flathub_remove_note">Jej ustawienia i pliki zostają zachowane.</string> + <string name="flathub_keep">Zachowaj</string> + <string name="desktop_size">Rozmiar na dysku</string> + <string name="desktop_uninstall_title">Odinstalować pulpit?</string> + <string name="desktop_all_systems">Wszystkie</string> + <string name="gog_achievements">Osiągnięcia GOG</string> + <string name="gog_achievements_hint">Synchronizuje osiągnięcia dla obsługiwanych gier GOG. Zacznie działać od następnego uruchomienia.</string> + <string name="gog_achievements_off">Osiągnięcia wyłączone</string> + <string name="gog_achievements_on">Osiągnięcia włączone</string> + <string name="gog_achievements_signin">Zaloguj się ponownie do GOG</string> + <string name="gog_achievements_uploaded">Osiągnięcia wysłane</string> + <string name="gog_achievements_pending">Wysyłanie osiągnięcia w toku</string> + <string name="gog_achievements_enable_store">Włącz osiągnięcia GOG w ustawieniach Store</string> + <string name="gog_achievements_unavailable">Osiągnięcia niedostępne; sprawdź dzienniki sesji</string> + <string name="achievement_unlocked">Osiągnięcie odblokowane</string> + <string name="touch_paste">Wklej</string> + <string name="touch_dpad">Krzyżak</string> + <string name="touch_left_stick">Lewy drążek</string> + <string name="touch_right_stick">Prawy drążek</string> + <string name="touch_menu">Menu</string> + <string name="touch_keyboard">Klawiatura</string> + <string name="touch_dpad_up">Krzyżak góra</string> + <string name="touch_dpad_down">Krzyżak dół</string> + <string name="touch_dpad_left">Krzyżak lewo</string> + <string name="touch_dpad_right">Krzyżak prawo</string> + <string name="touch_title">Sterowanie dotykowe</string> + <string name="touch_controller">Kontroler dotykowy</string> + <string name="touch_mouse">Mysz</string> + <string name="touch_on">Wł.</string> + <string name="touch_off">Wył.</string> + <string name="touch_speed_slow">Wolna</string> + <string name="touch_speed_normal">Normalna</string> + <string name="touch_speed_fast">Szybka</string> + <string name="touch_edit_layout">Edytuj układ</string> + <string name="touch_droiddeck_menu">Menu DroidDeck</string> + <string name="touch_drag_hint">Przeciągnij elementy sterujące na ekran</string> + <string name="touch_arrange">Rozmieść</string> + <string name="touch_discard">Odrzuć</string> + <string name="touch_saving">Zapisywanie…</string> + <string name="touch_icon">Ikona</string> + <string name="touch_narrower">Sz−</string> + <string name="touch_wider">Sz+</string> + <string name="touch_shorter">W−</string> + <string name="touch_taller">W+</string> + <string name="touch_hide">Skryj</string> + <string name="touch_color_opacity">Kolor i przezroczystość…</string> + <string name="touch_copy_layout">Skopiuj ten układ</string> + <string name="touch_layout_copied">Układ skopiowany; możesz go wkleić w układzie dotykowym każdej gry</string> + <string name="touch_paste_layout">Wklej skopiowany układ</string> + <string name="touch_reset_layout">Przywróć domyślny układ Steam</string> + <string name="touch_no_binding">Ten element sterujący nie ma przypisania w Steam, aby nadać mu ikonę</string> + <string name="touch_icon_for">Ikona dla %1$s</string> + <string name="touch_layout_color">Kolor układu dotykowego</string> + <string name="touch_done">Gotowe</string> + <string name="touch_save_failed">Nie udało się zapisać układu dotykowego</string> + <string name="touch_saved">Układ dotykowy zapisany w Steam</string> + <string name="touch_app_id">Aplikacja %1$d</string> + <string name="touch_label_hint">Etykieta (opcjonalnie)</string> + <string name="touch_icons_missing">Nie znaleziono ikon Steam</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d ikona ze Steam</item> + <item quantity="few">%1$d ikony ze Steam</item> + <item quantity="many">%1$d ikon ze Steam</item> + <item quantity="other">%1$d ikony ze Steam</item> + </plurals> + <string name="touch_icon_color">Kolor ikony</string> + <string name="touch_button_color">Kolor przycisku</string> + <string name="touch_no_icon">Brak ikony</string> + + <string name="stores_sync_sign_in">Sprawdzanie logowania…</string> + <string name="stores_sync_library">Pobieranie biblioteki…</string> + <string name="stores_sync_game_list">Pobieranie listy gier…</string> + <string name="stores_sync_details">Wczytywanie informacji o grach… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">Synchronizowanie %1$d gry…</item> + <item quantity="few">Synchronizowanie %1$d gier…</item> + <item quantity="many">Synchronizowanie %1$d gier…</item> + <item quantity="other">Synchronizowanie %1$d gry…</item> + </plurals> + <string name="stores_sync_progress">Synchronizowanie… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Pobieranie okładek…</string> + <string name="stores_sync_failed">Synchronizacja nie powiodła się</string> + <string name="stores_sync_no_games">Nie znaleziono gier w Twojej bibliotece %1$s</string> + <string name="stores_sync_amazon_no_answer">Amazon Games nie odpowiedział w sprawie Twojej biblioteki: odśwież lub zaloguj się ponownie</string> + <string name="stores_sync_gog_unreachable">Nie udało się połączyć z GOG</string> + <string name="stores_sync_gog_unreadable">Nie udało się wczytać Twojej biblioteki GOG</string> + <string name="stores_registering">Rejestrowanie w Steam…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Tryb Cardboard</string> + <string name="cardboard_mode_hint">Obraz dla lewego i prawego oka do gogli VR na telefon.</string> + <string name="cardboard_source">Obraz</string> + <string name="cardboard_source_screen">Wirtualny ekran</string> + <string name="cardboard_source_sbs">3D obok siebie</string> + <string name="cardboard_viewer">Gogle</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Bez soczewek</string> + <string name="cardboard_ipd">Rozstaw oczu</string> + <string name="cardboard_screen_width">Szerokość ekranu</string> + <string name="cardboard_screen_distance">Odległość ekranu</string> + <string name="cardboard_head_tracking">Śledzenie głowy</string> + <string name="cardboard_no_sensor">To urządzenie nie ma czujnika obrotu.</string> + <string name="cardboard_recenter">Wyśrodkuj widok</string> + <string name="cardboard_recenter_button">Wyśrodkuj</string> + <string name="cardboard_steamvr">SteamVR (eksperymentalne)</string> + <string name="cardboard_steamvr_install">Zainstaluj</string> + <string name="cardboard_steamvr_install_hint">Pobiera SteamVR dla ARM64 od Valve (571 MB).</string> + <string name="cardboard_steamvr_installing">Pobieranie i rozpakowywanie SteamVR…</string> + <string name="cardboard_steamvr_start">Uruchom</string> + <string name="cardboard_steamvr_ready_hint">Uruchamia SteamVR, a twoja głowa działa jak gogle; obraz przełącza się na 3D obok siebie.</string> + <string name="cardboard_steamvr_stop">Zatrzymaj</string> + <string name="cardboard_steamvr_running_hint">SteamVR działa.</string> + <string name="cardboard_steamvr_unavailable">Niedostępne w tej sesji.</string> + <string name="cardboard_steamvr_failed">SteamVR nie odpowiada; sprawdź dziennik sesji.</string> + <string name="drawer_component_downloads">Pobierz komponenty</string> + <string name="drawer_component_downloads_hint">Pobrane pliki są wspólne ze stroną Komponenty. Wybierz powyżej pobraną wersję, aby użyć jej przy następnym uruchomieniu gry.</string> + <string name="drawer_component_choose_download">Wybierz wersję do pobrania</string> + <string name="stores_fact_developer">Producent</string> + <string name="stores_fact_publisher">Wydawca</string> + <string name="stores_fact_released">Premiera</string> + <string name="stores_hltb_main">Fabuła</string> + <string name="stores_hltb_extras">+ Dodatki</string> + <string name="stores_hltb_completionist">100%</string> + <string name="stores_hltb_hours">%1$s godz.</string> + <string name="stores_hltb_minutes">%1$d min</string> +</resources> diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml new file mode 100644 index 000000000..2bb0f9764 --- /dev/null +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -0,0 +1,2260 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>A primeira execução transfere o Steam. Isto pode demorar alguns minutos.</item> + <item>Por predefinição, sair da app coloca o Steam em pausa; ao voltar, continua onde ficou.</item> + <item>O seu comando aparece como um comando Xbox 360. Se não tiver comando, utilize os controlos no ecrã.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>A primeira execução configura o ambiente de trabalho. Isto pode demorar alguns minutos.</item> + <item>Por predefinição, sair da app coloca a sessão em pausa; ao voltar, continua onde ficou.</item> + <item>O toque funciona como um painel tátil: arraste para mover o ponteiro, toque para clicar.</item> + <item>O seu comando aparece como um comando Xbox 360. Se não tiver comando, utilize os controlos no ecrã.</item> + </string-array> + <string name="session_channel">Sessão</string> + <string name="session_channel_description">Apresentada enquanto houver uma sessão do Steam ou do ambiente de trabalho aberta</string> + <string name="session_notification">O Steam está em execução</string> + <string name="session_notification_downloads">Transferência do Steam ativa</string> + <string name="session_notification_paused">Sessão em pausa - toque em Retomar quando estiver pronto</string> + <string name="resume_session">Retomar</string> + <string name="suspend_now">Suspender agora</string> + <string name="stop_session">Terminar sessão</string> + <string name="frame_gen_title">Geração de fotogramas</string> + <string name="frame_gen_hint">Selecione o modo de geração de fotogramas</string> + <string name="frame_gen_off">Desligado</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG Adaptativo %1$d</string> + <string name="lsfg_needs_steam">Requer o Lossless Scaling no Steam</string> + <string name="lsfg_needs_install">Instale o Lossless Scaling no Steam ou importe o Lossless.dll</string> + <string name="lsfg_import">Importar Lossless.dll…</string> + <string name="lsfg_using_steam">A utilizar a cópia do Steam</string> + <string name="lsfg_using_import">A utilizar uma cópia importada</string> + <string name="lsfg_pick_title">Escolher Lossless.dll</string> + <string name="lsfg_imported">Lossless.dll importado</string> + <string name="lsfg_import_unchanged">Este Lossless.dll já está a ser utilizado</string> + <string name="lsfg_import_not_owned">Esta conta Steam não possui o Lossless Scaling</string> + <string name="lsfg_import_invalid">Este ficheiro não é o Lossless.dll do Lossless Scaling</string> + <string name="lsfg_import_failed">Não foi possível preparar o LSFG a partir deste Lossless.dll</string> + <string name="lsfg_problem_missing">O LSFG requer o Lossless Scaling do Steam ou um Lossless.dll importado</string> + <string name="lsfg_problem_failed">O LSFG não conseguiu preparar os shaders a partir do Lossless.dll</string> + <string name="hud_fg_unsupported">%1$s: não suportado</string> + <string name="hud_fg_failed">%1$s: falha do motor</string> + <string name="hud_fg_needs_lossless">%1$s: requer Lossless Scaling</string> + <string name="hud_fg_starting">%1$s %2$s · a iniciar</string> + <string name="second_library_import_hint">Adiciona uma biblioteca Steam e procura jogos Windows nela. Os jogos que possui utilizam as respetivas entradas do Steam; os restantes tornam-se atalhos de jogos não Steam.</string> + <string name="game_env_title">Ambiente do jogo</string> + <string name="game_env_hint">Variáveis de ambiente para jogos Proton.</string> + <string name="game_env_edit">Editar</string> + <string name="game_env_shared">Todos os jogos Proton</string> + <string name="game_env_scope">Perfil</string> + <string name="game_env_app_id">ID da app Steam</string> + <string name="game_env_open">Abrir</string> + <string name="game_env_profile">Jogo %1$s</string> + <string name="game_env_add">Adicionar variável</string> + <string name="game_env_name">Nome da variável</string> + <string name="game_env_value">Valor</string> + <string name="game_env_save">Guardar</string> + <string name="game_env_remove">Remover</string> + <string name="game_env_restore">Voltar a herdar</string> + <string name="game_env_reset">Limpar substituições de ambiente</string> + <string name="game_env_unset">Não definir neste perfil</string> + <string name="game_env_empty">Nenhuma variável definida.</string> + <string name="game_env_inherited">Herdada</string> + <string name="game_env_custom">Variável personalizada</string> + <string name="game_env_error">Não foi possível carregar nem guardar. Verifique o armazenamento livre e tente novamente.</string> + <string name="game_env_invalid">Utilize um nome válido e um valor com até 8192 carateres.</string> + <string name="game_env_unused">O ambiente de execução padrão do Proton não lê esta variável.</string> + <string name="game_env_loading">A carregar…</string> + <string name="game_env_feature">Substituição do nível de funcionalidades D3D12. 12_2 indica as capacidades do DirectX 12 Ultimate; funcionalidades não suportadas podem falhar. Deixe por definir para deteção automática.</string> + <string name="game_env_shader">Substituição do modelo de shader D3D12. 6_9 requer um VKD3D suficientemente recente e funcionalidades de GPU compatíveis. Deixe por definir para deteção automática.</string> + <string name="game_env_cache">Mantenha a cache de shaders do Mesa ativa para reduzir compilações repetidas. false ativa a cache.</string> + <string name="game_env_cache_size">Tamanho máximo da cache de shaders do Mesa, por exemplo 1G. Caches maiores ocupam mais armazenamento.</string> + <string name="game_env_vkd3d_config">Opções do VKD3D para D3D12. nodxr desativa o ray tracing; utilize apenas num jogo que o exija.</string> + <string name="game_env_proton_log">Defina como 1 para obter um registo de diagnóstico do Proton. O registo pode reduzir o desempenho e ocupar armazenamento.</string> + <string name="game_env_wined3d">Defina como 1 para utilizar o WineD3D baseado em OpenGL em vez do DXVK, para testar a compatibilidade com D3D9–11.</string> + <string name="game_env_no_ntsync">Defina como 1 para manter este jogo fora do ntsync (droiddeck-ntsync ou um controlador do kernel); utiliza o droiddeck-esync onde existir um pacote, caso contrário o droiddeck-fsync.</string> + <string name="game_env_no_esync">Defina como 1 para desativar o droiddeck-esync neste jogo; utiliza o droiddeck-fsync em vez dele, ou o droiddeck-ntsync se o fsync também estiver desativado.</string> + <string name="game_env_no_fsync">Defina como 1 para manter este jogo fora do droiddeck-fsync; utiliza o droiddeck-esync onde existir um pacote, caso contrário o droiddeck-ntsync.</string> + <string name="game_env_fsync_first">Defina como 1 para executar este jogo com o droiddeck-fsync, ou 0 para dar prioridade ao droiddeck-esync onde houver um pacote compatível com o respetivo Proton.</string> + <string name="game_env_xalia">Defina como 0 para desativar o assistente de navegação por comando do Proton neste jogo.</string> + <string name="game_env_hud">Sobreposição do DXVK para D3D9–11. fps mostra a taxa de fotogramas; sem valor, fica oculta.</string> + <string name="game_env_dxvk_config">Definições do DXVK separadas por ponto e vírgula. As versões recentes utilizam dxvk.maxFrameRate para limitar os fotogramas; para jogar normalmente, prefira o limitador de fotogramas da sessão.</string> + <string name="game_env_vkd3d_limit">Limite da taxa de fotogramas em D3D12. 0 desativa o limite.</string> + <string name="game_env_glthread">Threading OpenGL do Mesa. true pode ajudar jogos OpenGL limitados pela CPU; teste jogo a jogo.</string> + <string name="sync_backend_title">Sincronização do Wine</string> + <string name="sync_backend_hint">Como os jogos mantêm os seus threads sincronizados. Aplica-se na próxima sessão.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">Predefinição do FEX</string> + <string name="fex_next_launch">Aplica-se quando iniciar o próximo jogo.</string> + <string name="game_settings_title">Jogos</string> + <string name="fex_default_label">Padrão do FEX</string> + <string name="fex_default_detail">Utiliza os valores padrão do FEX.</string> + <string name="fex_stability_label">Estabilidade</string> + <string name="fex_stability_detail">Definições mais seguras e mais lentas.</string> + <string name="fex_compatibility_label">Compatibilidade</string> + <string name="fex_compatibility_detail">TSO completo com multibloco.</string> + <string name="fex_intermediate_label">Intermédio</string> + <string name="fex_intermediate_detail">TSO equilibrado com precisão x87 reduzida.</string> + <string name="fex_performance_label">Desempenho</string> + <string name="fex_performance_detail">Todo o TSO desativado; multibloco.</string> + <string name="fex_performance_tso_label">Desempenho + TSO</string> + <string name="fex_performance_tso_detail">Definições de Desempenho com o TSO principal ativado.</string> + <string name="fex_extreme_label">Extremo</string> + <string name="fex_extreme_detail">Desativa as verificações de código automodificável. Jogos com JIT e DRM podem deixar de funcionar.</string> + <string name="fex_extreme_tso_label">Extremo + TSO</string> + <string name="fex_extreme_tso_detail">Definições de Extremo com o TSO principal ativado.</string> + <string name="fex_extreme_gn_label">Extremo-gn</string> + <string name="fex_extreme_gn_detail">Desempenho com sugestões de TSC e metadados. Experimente antes do Extremo.</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">Desempenho com verificações de código completas e o bit de hipervisor oculto.</string> + <string name="game_env_custom_value">Valor personalizado</string> + <string name="game_env_select_value">Selecionar valor</string> + <string name="user_apps_section">Emuladores e apps</string> + <string name="user_apps_add">Adicionar</string> + <string name="user_apps_add_detail">A sua própria app ou jogo</string> + <string name="user_apps_kind_script">Script</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d%%</string> + <string name="user_apps_starting">A iniciar…</string> + <string name="user_apps_added">%1$s adicionado</string> + <string name="user_apps_removed">%1$s removido</string> + <string name="user_apps_back">Ambiente de trabalho</string> + <string name="user_apps_gone">Essa app já não existe. Escolha-a novamente no Ambiente de trabalho.</string> + <string name="user_apps_open">Abrir %1$s</string> + <string name="user_apps_remove">Remover</string> + <string name="user_apps_remove_confirm">Prima novamente para remover</string> + <string name="user_apps_stop_session">Termine a sessão para remover</string> + <string name="user_apps_script_linked">Executa %1$s a partir da respetiva pasta, ligada à sessão no local onde se encontra. Remover a app não altera essa pasta.</string> + <string name="user_apps_script_copied">Executa %1$s a partir de uma cópia da respetiva pasta dentro do DroidDeck. Remover a app elimina a cópia, não o original.</string> + <string name="user_apps_runtime_required">Instale primeiro o ambiente de execução Linux</string> + <string name="user_apps_file_gone">O ficheiro já não existe</string> + <string name="user_apps_no_space">Espaço livre insuficiente: copiar a pasta requer %1$s</string> + <string name="user_apps_copying">A copiar %1$s</string> + <string name="user_apps_failed">Falha ao adicionar a app</string> + <string name="user_apps_github_checking">A procurar uma versão de %1$s</string> + <string name="user_apps_github_unreachable">Não foi possível ler as versões de %1$s</string> + <string name="user_apps_github_no_appimage">Nenhuma versão de %1$s tem uma AppImage ARM64 ou x86_64</string> + <string name="user_apps_downloading">A transferir %1$s</string> + <string name="user_apps_download_failed">A transferência falhou</string> + <string name="user_apps_update_failed">Falha ao atualizar a app</string> + <string name="user_apps_icon_failed">Não foi possível ler esse ícone</string> + <string name="user_apps_saved">%1$s guardado</string> + <string name="user_apps_edit">Editar</string> + <string name="user_apps_check_updates">Procurar atualizações</string> + <string name="user_apps_checking_updates">A procurar atualizações…</string> + <string name="user_apps_update_to">Atualizar para %1$s</string> + <string name="user_apps_up_to_date">Atualizado · %1$s</string> + <string name="user_apps_updated">%1$s atualizado</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">Adicionar uma app</string> + <string name="add_app_tab_script">Script</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">Um ficheiro .sh. É executado a partir da própria pasta, em ecrã inteiro no gamescope.</string> + <string name="add_app_appimage_hint">Uma AppImage ARM64 (aarch64), ou uma x86_64 executada através do FEX. É extraída para o ambiente de execução Linux uma única vez.</string> + <string name="add_app_github_hint">Instala a AppImage ARM64 da versão mais recente do repositório, ou a x86_64 através do FEX.</string> + <string name="app_fex_title">Utilizar FEX</string> + <string name="app_fex_auto">Automático</string> + <string name="app_fex_on">Ligado</string> + <string name="app_fex_off">Desligado</string> + <string name="app_fex_hint">O FEX executa programas Linux x86 neste dispositivo ARM64. Em Automático, é utilizado quando o programa foi compilado para x86.</string> + <string name="app_fex_x86">%1$s através do FEX</string> + <string name="app_fex_not_ready">Este programa foi compilado para PCs %1$s e é executado através do FEX, que ainda não está configurado. Abra o Steam uma vez com a sessão iniciada: o DroidDeck transfere o FEX e o Steam Linux Runtime necessários e, depois, este programa abre.</string> + <string name="session_end_program">Este programa</string> + <string name="session_end_fex_missing">%1$s foi compilado para PCs x86 e é executado através do FEX, que ainda não está instalado. Abra o Steam uma vez com a sessão iniciada e deixe-o transferir o FEX e o Steam Linux Runtime; depois, volte a iniciá-lo.</string> + <string name="add_app_flatpak_hint">Um ID de app do Flathub ou a respetiva página no Flathub. O Flatpak é configurado primeiro, se ainda não estiver.</string> + <string name="add_app_file">Ficheiro</string> + <string name="add_app_no_file">Nenhum ficheiro escolhido</string> + <string name="add_app_choose_file">Escolher ficheiro</string> + <string name="add_app_change_file">Alterar</string> + <string name="add_app_pick_script">Escolher um script</string> + <string name="add_app_pick_appimage">Escolher uma AppImage</string> + <string name="add_app_pick_icon">Escolher um ícone</string> + <string name="add_app_repository">Repositório</string> + <string name="add_app_repository_placeholder">proprietário/repo ou uma ligação do github.com</string> + <string name="add_app_repository_invalid">Não é um repositório GitHub</string> + <string name="add_app_flatpak_id">ID da app</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">Não é um ID de app do Flathub</string> + <string name="add_app_flatpak_bundle_hint">Ou um ficheiro .flatpak, para uma app que não esteja no Flathub.</string> + <string name="add_app_pick_flatpak">Escolher um ficheiro .flatpak</string> + <string name="add_app_source">Origem</string> + <string name="add_app_appearance">No seu Ambiente de trabalho</string> + <string name="add_app_preview_name">Nova app</string> + <string name="add_app_name">Nome</string> + <string name="add_app_name_placeholder">Da app</string> + <string name="add_app_icon">Ícone</string> + <string name="add_app_icon_default">Ícone padrão</string> + <string name="add_app_choose_icon">Escolher ícone</string> + <string name="add_app_clear_icon">Limpar</string> + <string name="add_app_icon_suggestions">Do repositório</string> + <string name="add_app_icon_searching">A procurar ícones…</string> + <string name="add_app_icon_reset">Padrão</string> + <string name="edit_app_title">Editar %1$s</string> + <string name="edit_app_save">Guardar</string> + <string name="add_app_checking_folder">A verificar a pasta…</string> + <string name="add_app_folder_linked">A pasta está ligada à sessão: %1$s</string> + <string name="add_app_folder_copied">A pasta contém programas Linux, que o armazenamento partilhado não consegue executar, por isso é copiada para o DroidDeck (%1$s).</string> + <string name="add_app_confirm">Adicionar</string> + <string name="add_app_cancel">Cancelar</string> + <string name="add_app_runtime_required">Instale primeiro o ambiente de execução Linux na Configuração.</string> + <string name="add_app_busy">Aguarde que %1$s termine.</string> + <string name="steam_compat_label">Compatível</string> + <string name="proton_compatible_note">As versões marcadas com “%1$s” no menu Compatibilidade do Steam incluem as correções de prefixo do DroidDeck e iniciam jogos aqui. O padrão é o Proton Experimental ARM64 (%1$s), ou o Proton 11.0 ARM64 (%1$s) quando essa versão mais recente estiver instalada. As entradas do Proton da própria Valve sem a marca não conseguem iniciar jogos neste dispositivo.</string> + <string name="proton_installed_compatible">Instalado %1$s · %2$s</string> + <string name="common_cancel">Cancelar</string> + <string name="common_auto">Automático</string> + <string name="common_never">Nunca</string> + <string name="common_always">Sempre</string> + <string name="common_uninstall">Desinstalar</string> + <string name="common_check">Verificar</string> + <string name="common_update">Atualizar</string> + <string name="common_checking">A verificar…</string> + <string name="common_working">A processar…</string> + <string name="common_forget">Esquecer</string> + <string name="common_add_ellipsis">Adicionar…</string> + <string name="common_use">Utilizar</string> + <string name="common_applies_next_session">Aplica-se na próxima sessão.</string> + <string name="mode_steam_title">Sessão do Steam</string> + <string name="mode_desktop_title">Sessão do ambiente de trabalho</string> + <string name="mode_display">Ecrã</string> + <string name="mode_resolution">Resolução</string> + <string name="mode_res_720">Até 720p</string> + <string name="mode_res_900">Até 900p</string> + <string name="mode_res_1080">Até 1080p</string> + <string name="mode_res_native">Nativa</string> + <string name="mode_res_default">%1$s (padrão)</string> + <string name="mode_res_custom_value">Personalizada · %1$d×%2$d</string> + <string name="mode_res_custom">Personalizada…</string> + <string name="mode_res_note">Com 720p, os menus podem responder mais depressa.</string> + <string name="mode_ratio">Proporção do ecrã</string> + <string name="mode_ratio_custom">Definida pela resolução personalizada.</string> + <string name="mode_ratio_auto">Automático utiliza pelo menos 16:9.</string> + <string name="mode_fps">Limite de fotogramas</string> + <string name="mode_fps_note">Limita toda a sessão. O ecrã muda para uma taxa de atualização que seja múltipla do limite.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">Saída HDR10</string> + <string name="mode_hdr_unavailable">Indisponível: %1$s.</string> + <string name="mode_hdr_restart">Reinicie a app para aplicar.</string> + <string name="mode_drivers">Controladores</string> + <string name="mode_gpu_drivers">Controladores da GPU</string> + <string name="mode_gpu_drivers_hint">Partilhados pelo Steam e pelo Ambiente de trabalho. Defina-os na página Componentes.</string> + <string name="mode_touch_controls">Toque e controlos</string> + <string name="mode_touch">Toque</string> + <string name="mode_touch_touchpad">Painel tátil</string> + <string name="mode_touch_direct">Direto</string> + <string name="mode_touch_note">Automático: painel tátil no Ambiente de trabalho, direto no Steam.</string> + <string name="mode_osc">Controlos no ecrã</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">Em Automático, os controlos aparecem quando não há nenhum comando ligado.</string> + <string name="osc_steam_touch">Controlos táteis do Steam (beta)</string> + <string name="osc_style_next_session">Os controlos táteis do Steam começam na próxima sessão do Steam</string> + <string name="osc_steam_layout">Esquema tátil do Steam</string> + <string name="osc_steam_layout_hint">Mova, redimensione ou oculte os controlos táteis deste jogo; é guardado no Steam</string> + <string name="mode_controller">Comando</string> + <string name="mode_controller_hint">Como o Steam vê o seu comando. Aplica-se na próxima sessão.</string> + <string name="mode_controller_deck">Comando Steam Deck</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Comando Xbox 360</string> + <string name="mode_controller_note">Deck: botão de Acesso rápido e giroscópio. Steam Controller: o mesmo, com sensor de pega nos botões traseiros. DualShock 4 e DualSense: giroscópio, e os trackpads como o painel tátil do comando. Xbox 360: abra o Acesso rápido com Guide+A.</string> + <string name="mode_back">Voltar</string> + <string name="back_menu_then_qam">Menu → QAM</string> + <string name="back_qam_then_menu">QAM → Menu</string> + <string name="mode_session">Sessão</string> + <string name="mode_suspend">Comportamento em segundo plano</string> + <string name="mode_suspend_hint">Quando a app fica oculta ou o ecrã se desliga.</string> + <string name="mode_suspend_manual">Manual</string> + <string name="mode_suspend_native">Nativo</string> + <string name="mode_suspend_steam_note">Automático: pausa e retoma ao voltar a ser apresentada. Nativo: prepara primeiro o Steam para a suspensão. Manual: aguarda por Retomar. Nunca: continua em execução.</string> + <string name="mode_background_downloads">Manter transferências ativas</string> + <string name="mode_background_downloads_hint">Adia a suspensão em segundo plano enquanto o Steam está a transferir.</string> + <string name="mode_suspend_note">Automático: pausa em segundo plano e retoma ao voltar a ser apresentada. Manual: aguarda por Retomar. Nunca: continua em execução.</string> + <string name="mode_startup">Arranque</string> + <string name="mode_steam_startup">Iniciar o Steam com o DroidDeck</string> + <string name="mode_steam_startup_hint">Abre a sessão do Steam quando o DroidDeck é iniciado.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">A procurar versões…</string> + <string name="mode_decky_ready">Pronto para instalar %1$s.</string> + <string name="mode_decky_none">Nenhuma versão compatível encontrada. Tente novamente mais tarde.</string> + <string name="mode_decky_installed">Instalado · %1$s</string> + <string name="mode_decky_update">Atualização disponível · %1$s</string> + <string name="mode_decky_current">Atualizado · %1$s</string> + <string name="mode_decky_loader">Carregador</string> + <string name="mode_decky_install_latest">Instalar a versão mais recente</string> + <string name="mode_decky_on">Inicia com o Steam. Enquanto estiver ligado, outras apps neste dispositivo poderão controlar o Steam.</string> + <string name="mode_decky_off">Desligado. A porta de depuração remota do Steam mantém-se fechada.</string> + <string name="mode_decky_remove_title">Desinstalar o Decky Loader?</string> + <string name="mode_decky_remove_text">Remove o carregador. Os seus plugins e definições são mantidos.</string> + <string name="mode_client">Cliente</string> + <string name="mode_mangoapp">Sobreposição de desempenho</string> + <string name="mode_mangoapp_hint">Desative se os jogos falharem ou ficarem a preto no Modo Deck. Aplica-se na próxima sessão.</string> + <string name="mode_steam_repair">Reparar cliente Steam</string> + <string name="mode_steam_repair_hint">Volta a transferir o cliente Steam na próxima vez que o Steam iniciar. Utilize se o Steam ficar preso no ecrã de carregamento. Os jogos, a sessão iniciada e as definições são mantidos.</string> + <string name="mode_steam_repair_queued">O cliente Steam será transferido novamente na próxima vez que o Steam iniciar.</string> + <string name="mode_steam_repair_button">Reparar</string> + <string name="mode_added_art">Imagens</string> + <string name="mode_sgdb_key">Chave da API do SteamGridDB</string> + <string name="mode_sgdb_key_yours">A sua chave</string> + <string name="mode_sgdb_edit">Editar…</string> + <string name="mode_added_art_hint">Preenche as imagens em falta a partir do Steam e, depois, do SteamGridDB. Os seus próprios cover.jpg, hero.jpg, logo.png ou icon.png na pasta do jogo têm prioridade.</string> + <string name="summary_remove_game">Remover %1$s</string> + <string name="mode_added_remove_confirm">Remover?</string> + <string name="games_add">Adicionar jogo</string> + <string name="games_add_title">Adicionar um jogo:</string> + <string name="games_pick_exe_title">Escolha o exe:</string> + <string name="games_add_unreachable">Esta localização não está disponível na sessão</string> + <string name="mode_added_exe_check">Verificar este exe</string> + <string name="wincomp_title">Componentes do Windows</string> + <string name="wincomp_pad_hint">LB / RB alternam entre as secções.</string> + <string name="wincomp_card_none">Nenhum</string> + <string name="wincomp_applies">DLLs do Windows que o Proton não inclui, da lista de componentes. Ative as de que este jogo precisa: vão para a pasta Windows própria do jogo, nunca para os ficheiros dele, e mantêm-se seja qual for o Proton em que é executado.</string> + <string name="wincomp_offline">Não foi possível aceder à lista de componentes. Os componentes já transferidos podem continuar a ser ativados ou desativados.</string> + <string name="wincomp_recommended">Recomendado para este jogo</string> + <string name="wincomp_checking">A verificar de que precisa este jogo…</string> + <string name="wincomp_when_online">Disponível quando houver ligação</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">A ativar %1$d componente…</item> + <item quantity="many">A ativar %1$d componentes…</item> + <item quantity="other">A ativar %1$d componentes…</item> + </plurals> + <string name="wincomp_auto_section">Ativos para este jogo</string> + <string name="wincomp_auto_steam">O Steam instala %1$s com %2$s (app %3$s)</string> + <string name="wincomp_auto_gog">Da lista do GOG para este jogo</string> + <string name="wincomp_auto_epic">Dos pré-requisitos da Epic para este jogo</string> + <string name="wincomp_auto_amazon">Dos passos de instalação da Amazon para este jogo</string> + <string name="wincomp_steam_by_name">O Steam instala %1$s com a app %2$s, identificada pelo nome</string> + <string name="wincomp_suggestions">Também listados</string> + <string name="wincomp_suggestion_reason">Listado por %1$s; não é um componente aqui</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">O jogo inclui %1$s</string> + <string name="wincomp_found_shipped">O jogo tem %1$s (muitas vezes funciona assim)</string> + <string name="wincomp_already_here">Já está neste jogo</string> + <string name="wincomp_no_recommendation">Nada na pasta do jogo aponta para um componente.</string> + <string name="wincomp_active">Instalados para este jogo (%1$d)</string> + <string name="wincomp_all">Todos os componentes (%1$d)</string> + <string name="wincomp_installers">Requerem um instalador do Windows (%1$d)</string> + <string name="wincomp_installers_note">São programas de instalação próprios, sem nenhum pacote no interior para desmontar e ainda sem gravação do que instalam. A maioria são ambientes de execução antigos que o Mono incluído no Proton já substitui; as gravações são adicionadas à medida que são feitas.</string> + <string name="wincomp_show">Mostrar</string> + <string name="wincomp_hide">Ocultar</string> + <string name="wincomp_needs_installer">Executa o seu próprio programa de instalação (ainda sem gravação)</string> + <string name="wincomp_unsupported">Ainda não suportado aqui</string> + <string name="wincomp_loading">A carregar os componentes…</string> + <string name="wincomp_failed">Não foi possível instalar: %1$s</string> + <string name="wincomp_downloaded">Transferido</string> + <string name="wincomp_downloading">A transferir %1$s</string> + <string name="wincomp_installing">A instalar: %1$s</string> + <string name="wincomp_reverting">A desativar…</string> + <string name="wincomp_download_size">Transferência de %1$s</string> + <string name="wincomp_next_launch">Tem efeito na próxima vez que o jogo for iniciado.</string> + <string name="mode_stretch169">Esticar jogos 16:9</string> + <string name="mode_stretch169_hint">Em ecrãs mais quadrados do que 16:9, estica os jogos para preencher o ecrã em vez de mostrar barras. Aplica-se na próxima sessão.</string> + <string name="mode_audio">Áudio</string> + <string name="mode_directaudio">DirectAudio para jogos</string> + <string name="mode_directaudio_hint">Ignora o PulseAudio para reduzir a latência.</string> + <string name="mode_mic">Microfone</string> + <string name="mode_mic_hint">Para chat de voz.</string> + <string name="mode_network">Rede</string> + <string name="mode_wifi_discovery">Deteção de Wi-Fi</string> + <string name="mode_wifi_discovery_hint">Mostrar nomes de Wi-Fi e redes próximas no Steam.</string> + <string name="mode_wifi_location_off">Os serviços de localização estão desativados.</string> + <string name="mode_wifi_permission_denied">Autorização de localização não concedida.</string> + <string name="mode_wifi_open_settings">Abrir definições</string> + <string name="mode_wifi_continue">Continuar</string> + <string name="mode_wifi_explain_title">Ativar a deteção de Wi-Fi?</string> + <string name="mode_wifi_explain_text">O Android requer a autorização de Localização para ler nomes de Wi-Fi e detetar redes próximas. O DroidDeck utiliza este acesso para mostrar essas redes no Steam. Não monitorizamos a sua localização. O acesso à Internet funciona sem esta autorização.</string> + <string name="mode_storage">Armazenamento de jogos</string> + <string name="mode_storage_auto">Automático - o cartão SD quando presente, mais as suas pastas Games</string> + <string name="mode_storage_auto_none">Automático - as suas pastas Games, mais o cartão SD quando presente (nenhum agora)</string> + <string name="mode_storage_internal">Apenas interno</string> + <string name="mode_storage_folder">Pasta: %1$s</string> + <string name="mode_second_library">Segunda biblioteca</string> + <string name="mode_storage_note">Os jogos que carregam recursos em streaming a partir do SD ou do armazenamento partilhado podem apresentar soluços.</string> + <string name="mode_storage_diagnostics">Diagnóstico de armazenamento</string> + <string name="mode_storage_diagnostics_hint">Regista os resultados de alocação e amostras de tempos de armazenamento em Partilhar registos. Pode tornar as instalações mais lentas enquanto estiver ativado. Aplica-se na próxima sessão.</string> + <string name="mode_choose_folder">Escolher uma pasta…</string> + <string name="mode_custom_res_title">Resolução personalizada</string> + <string name="mode_custom_res_text">Substitui o limite e a proporção. Um tamanho que não corresponda ao ecrã fica com barras.</string> + <string name="mode_width">Largura</string> + <string name="mode_height">Altura</string> + <string name="mode_custom_res_range">Entre 320×240 e 3840×2160.</string> + <plurals name="mode_added_count"> + <item quantity="one">%1$d jogo</item> + <item quantity="many">%1$d jogos</item> + <item quantity="other">%1$d jogos</item> + </plurals> + <string name="common_ok">OK</string> + <string name="common_delete">Eliminar</string> + <string name="common_restore">Restaurar</string> + <string name="common_download">Transferir</string> + <string name="common_delete_named">Eliminar %1$s</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Controlador de execução</string> + <string name="comp_runtime_driver_hint">Utilizado pelo Steam, pelos jogos e pelo ambiente de trabalho. Aplica-se na próxima sessão.</string> + <string name="comp_import_turnip">Importar zip do Turnip…</string> + <string name="comp_display_driver">Controlador de ecrã</string> + <string name="comp_display_driver_hint">Desenha os fotogramas no ecrã em ambos os modos. Reinicie a app para aplicar.</string> + <string name="comp_import_adrenotools">Importar um zip do AdrenoTools…</string> + <string name="comp_never">nunca</string> + <string name="comp_nightlies_checked">Última verificação das Nightlies: %1$s</string> + <string name="comp_title">Componentes</string> + <string name="comp_about">Acerca dos Componentes</string> + <string name="comp_check_drivers">Procurar novos controladores</string> + <string name="comp_check_nightlies">Procurar novos pacotes nas Nightlies</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">O Proton escolhido aqui executa todos os jogos que não escolheram o seu próprio no Steam, e uma alteração feita no Steam também aparece aqui. O Steam substitui o Proton da Valve quando o atualiza; os originais são mantidos por versão.</string> + <string name="comp_scope">Configuração</string> + <string name="comp_profile_configure">Configurar perfil</string> + <string name="comp_profile">Perfil</string> + <string name="comp_scope_general">Perfil padrão</string> + <string name="comp_scope_general_detail">Utilizado automaticamente pelos jogos sem substituições.</string> + <string name="comp_profile_game_detail">Definições guardadas, utilizadas automaticamente quando %1$s é iniciado.</string> + <string name="comp_game_hint">Escolha apenas as definições de que este jogo precisa. Tudo o resto continua a utilizar o Perfil padrão.</string> + <string name="comp_game_fex">Predefinição do FEX</string> + <string name="comp_game_fex_inherited">A utilizar a predefinição do Perfil padrão.</string> + <string name="comp_game_fex_override">Predefinição personalizada guardada para este jogo.</string> + <string name="comp_game_fex_custom">Variáveis do FEX personalizadas</string> + <string name="comp_game_use_general">Utilizar padrão</string> + <string name="comp_game_compatibility">Compatibilidade do jogo</string> + <string name="comp_game_environment_hint">Opções de ambiente do Proton, DXVK, VKD3D e sincronização para este jogo.</string> + <string name="comp_game_proton_inherited">A utilizar o Proton do Perfil padrão.</string> + <string name="comp_game_proton_override">Proton personalizado guardado para este jogo através do mapeamento de compatibilidade do Steam.</string> + <string name="comp_no_proton_short">Sem Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Utiliza o pacote do Perfil padrão de %1$s, a menos que este jogo selecione outro pacote.</string> + <string name="comp_game_component_general">Utiliza o pacote de componentes do Perfil padrão.</string> + <string name="comp_game_component_override">Utiliza %1$s apenas neste jogo.</string> + <string name="comp_game_runtime_options">Opções de execução</string> + <string name="comp_game_environment_other_hint">Outras opções de ambiente por jogo do Proton, DXVK, VKD3D-Proton e sincronização.</string> + <string name="comp_game_saved_next">O perfil de %1$s foi guardado para o próximo arranque do jogo.</string> + <string name="game_no_steam_title">Iniciar sem o Steam</string> + <string name="game_no_steam_detail">Para um jogo que fecha de imediato: inicia-o sem a configuração do Steam no Proton. Funcionalidades do Steam, como a sobreposição, ficam desativadas para ele.</string> + <string name="game_no_steam_on">Ativado</string> + <string name="comp_game_proton_live">A escolha de Proton de %1$s foi guardada e já está disponível no Steam.</string> + <string name="comp_game_proton_next">A escolha de Proton de %1$s foi guardada. Reinicie o Steam antes de iniciar o jogo.</string> + <string name="comp_game_proton_not_runnable">%1$s não pode utilizar o Proton selecionado neste dispositivo.</string> + <string name="comp_game_load_failed">Não foi possível carregar o perfil de %1$s: %2$s</string> + <string name="comp_game_save_failed">Não foi possível guardar o perfil do jogo: %1$s</string> + <string name="comp_game_reset">Repor perfil do jogo</string> + <string name="comp_game_reset_title">Repor o perfil de %1$s?</string> + <string name="comp_game_reset_body">Remover as substituições de Proton, FEX, pacote de componentes, ambiente, filtragem de texturas e componentes do Windows deste jogo? Passará a utilizar o Perfil padrão.</string> + <string name="comp_game_reset_done">%1$s utiliza agora o Perfil padrão.</string> + <string name="comp_game_reset_failed">Não foi possível repor totalmente o perfil do jogo: %1$s</string> + <string name="comp_default_live">%1$s executa agora todos os jogos que não escolheram o seu próprio Proton no Steam.</string> + <string name="comp_default_next">A partir do próximo arranque do Steam, %1$s vai executar todos os jogos que não escolheram o seu próprio Proton.</string> + <string name="comp_default_not_runnable">%1$s não consegue iniciar jogos aqui, por isso não pode ser o padrão.</string> + <string name="comp_default_failed">Não foi possível guardar o Proton padrão: %1$s</string> + <string name="comp_swapped">%1$s · %2$d trocados</string> + <string name="comp_all_original">%1$s · todos originais</string> + <string name="comp_game_running_suffix">%1$s · jogo em execução</string> + <string name="comp_esync_pack">%1$s · pacote droiddeck-esync</string> + <string name="comp_esync_wanted">%1$s · ainda sem pacote droiddeck-esync</string> + <string name="comp_esync_builtin">%1$s · esync integrado, não precisa de pacote</string> + <string name="comp_gpu_drivers">Controladores da GPU</string> + <string name="comp_reapplied">%1$s · reaplicado no arranque %2$s</string> + <string name="comp_next_queued">Seguinte: %1$s · depois de o jogo fechar</string> + <string name="comp_in_use">%1$s em utilização: %2$s</string> + <string name="comp_waits">Aguarda até o jogo fechar</string> + <string name="comp_applies_next_game">Aplica-se na próxima vez que um jogo iniciar</string> + <string name="comp_import_zip">Importar .zip</string> + <string name="comp_reading">A ler os Protons…</string> + <string name="comp_no_proton">Ainda não há nenhum Proton instalado. Inicie o Steam uma vez para transferir o respetivo Proton ARM64, ou adicione o GE-Proton, o CachyOS ou o DroidDeck-Proton a partir da Configuração.</string> + <string name="comp_original">Original · %1$s</string> + <string name="comp_this_build">%1$s · esta versão</string> + <string name="comp_earlier_build">%1$s · versão anterior</string> + <string name="comp_installed_count">Instalados · %1$d</string> + <string name="comp_nothing_stored">Ainda nada guardado para %1$s. Os ficheiros deste Proton são guardados como o original.</string> + <string name="comp_waits_long">Há um jogo em execução com este Proton, por isso isto aguarda até que feche.</string> + <string name="comp_applies_long">Aplica-se na próxima vez que um jogo iniciar.</string> + <string name="comp_swap_title">Trocar %1$s?</string> + <string name="comp_swap_body">Colocar %1$s em %2$s? Os ficheiros originais ficam guardados. %3$s</string> + <string name="comp_restore_title">Restaurar %1$s?</string> + <string name="comp_restore_body">Repor o original de %1$s em %2$s? %3$s</string> + <string name="comp_delete_package_body">Remove o pacote da app. Pode transferi-lo novamente.</string> + <string name="comp_delete_original_title">Eliminar este original?</string> + <string name="comp_delete_original_body">Este original de %1$s é de uma versão anterior de %2$s. O original da versão atual é sempre mantido.</string> + <string name="comp_nightlies_count">Nas Nightlies · %1$d</string> + <string name="comp_refresh_to_list">Atualize para listar os pacotes nas Nightlies.</string> + <string name="comp_nothing_new">Nada de novo desde a última verificação.</string> + <string name="comp_import_wcp">Importar .wcp</string> + <string name="comp_restore_original">Restaurar o original deste Proton</string> + <string name="comp_restore_original_body">Repor o %2$s original de %1$s?</string> + <string name="comp_swap">Trocar</string> + <string name="comp_about_text">FEX, DXVK e VKD3D-Proton para cada Proton com que o Steam executa jogos.\n\nSelecione uma linha instalada para a trocar. Aplica-se na próxima vez que um jogo iniciar, ou depois de o jogo em execução fechar.\n\nAs suas escolhas são verificadas novamente antes de cada arranque e repostas se algo as tiver alterado. Os ficheiros originais de cada Proton são mantidos, por isso as atualizações do Steam nunca os perdem.</string> + <string name="comp_tag_original">Original</string> + <string name="comp_tag_in_use">Em utilização</string> + <string name="comp_tag_new">Novo</string> + <string name="comp_tag_stored">Guardado</string> + <string name="perf_title">Desempenho</string> + <string name="perf_lede">Núcleos da CPU, correções da sessão e nome do anfitrião. Aplica-se na próxima sessão.</string> + <string name="perf_client_cores_group">Núcleos do cliente Steam</string> + <string name="perf_override">Substituir a escolha de núcleos do Steam</string> + <string name="perf_override_hint">Fixa o Steam, o respetivo auxiliar de interface e o gamescope nos núcleos abaixo.</string> + <string name="perf_client_cores">Núcleos do cliente</string> + <string name="perf_client_cores_on">Núcleos para o Steam.</string> + <string name="perf_client_cores_off">Ative a substituição para escolher.</string> + <string name="perf_game_cores_group">Núcleos dos jogos</string> + <string name="perf_game_cores">Núcleos dos jogos</string> + <string name="perf_game_cores_hint">Núcleos para os jogos. Com todos selecionados, o Android distribui livremente.</string> + <string name="perf_client_ui">Interface do cliente</string> + <string name="perf_glthread">GL multithread</string> + <string name="perf_glthread_hint">Pode fazer os menus do Steam responder mais depressa.</string> + <string name="perf_zink">Zink: descritores diferidos</string> + <string name="perf_zink_hint">Recomendado para controladores sem buffers de descritores.</string> + <string name="perf_noglerror">Ignorar verificações de erros GL</string> + <string name="perf_noglerror_hint">Desativa a validação GL em cada chamada.</string> + <string name="perf_gsrealtime">gamescope: fila de GPU em tempo real</string> + <string name="perf_gsrealtime_hint">Dá prioridade ao trabalho de GPU do compositor. Pode suavizar o ritmo de fotogramas, com algum custo para os jogos.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">Manter a GPU na frequência máxima</string> + <string name="perf_gpuclock_hint">Apenas Adreno. Menos soluços, mais consumo de bateria e mais calor.</string> + <string name="perf_fixes">Correções da sessão</string> + <string name="perf_sysmem">Turnip: renderização em sysmem</string> + <string name="perf_sysmem_hint">Mais rápido para o Steam e a maioria dos jogos. Obrigatório no Adreno 710/720/722.</string> + <string name="perf_xalia">Ignorar o assistente xalia do Steam</string> + <string name="perf_fastsync">droiddeck-ntsync (experimental)</string> + <string name="perf_fastsync_hint">Um substituto em espaço de utilizador para o controlador ntsync do Linux, para testes. Enquanto estiver ativado, o Proton utiliza-o em vez do droiddeck-esync e do droiddeck-fsync. Funciona com o Proton Experimental ARM64, o GE-Proton e o proton-cachyos. Aplica-se na próxima sessão.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync primeiro</string> + <string name="perf_fsync_hint">Executa o fsync do próprio Proton sobre os ficheiros originais do Proton, com o futex_waitv fornecido em espaço de utilizador, por isso não precisa de pacote e continua a funcionar assim que o Steam atualiza o Proton. Enquanto estiver ativado, os jogos utilizam-no antes do droiddeck-esync, exceto os poucos que o Proton mantém fora do fsync. Aplica-se na próxima sessão.</string> + <string name="perf_esync_hint">Os threads Windows dos jogos esperam uns pelos outros através de eventfd e memória partilhada, em vez de idas e voltas pelo wineserver. Utiliza um pacote droiddeck-esync feito para essa versão exata do Proton; os pacotes são obtidos automaticamente. Até existir um para uma nova versão do Proton, o droiddeck-fsync assume o seu lugar. Aplica-se na próxima sessão.</string> + <string name="perf_xalia_hint">Desativa o assistente de navegação por comando do Proton, que gasta CPU em todos os jogos.</string> + <string name="perf_seccomp">Executar o proot sem seccomp</string> + <string name="perf_seccomp_hint">Pode corrigir erros de chamadas de sistema em falta em alguns kernels. Pode reduzir o desempenho.</string> + <string name="perf_fastpath">Caminho rápido do proot</string> + <string name="perf_fastpath_needs_seccomp">Requer o filtro seccomp do proot.</string> + <string name="perf_fastpath_hint">Pesquisas de ficheiros mais rápidas e menos soluços. Desative se um programa não encontrar os seus ficheiros.</string> + <string name="perf_identity">Identidade da sessão</string> + <string name="perf_hostname">Nome do anfitrião</string> + <string name="perf_hostname_hint">O nome com que o Steam vê este dispositivo. Em branco, repõe %1$s.</string> + <string name="perf_hostname_invalid">Apenas letras, algarismos e hífenes interiores, até 63 carateres.</string> + <string name="perf_phantom_warning">O Android está configurado para terminar esta sessão</string> + <string name="driver_installed">Instalado</string> + <string name="driver_available">Disponível para transferir</string> + <string name="driver_nothing_new">Nada de novo desde a última verificação.</string> + <string name="driver_restore_builtin">Restaurar controladores integrados</string> + <string name="driver_delete_builtin">Integrado na app: isto remove os ficheiros extraídos e oculta-o. Restaurar controladores integrados volta a mostrá-lo.</string> + <string name="driver_delete_bundle">Metade de um pacote Android + Linux: as duas metades são removidas. Um controlador em utilização volta ao padrão.</string> + <string name="driver_delete_plain">Os ficheiros são removidos da app.</string> + <string name="driver_delete_in_use">Os ficheiros são removidos. Como está em utilização, o padrão passa a ser utilizado.</string> + <string name="drawer_page_display">Ecrã</string> + <string name="drawer_page_effects">Efeitos</string> + <string name="drawer_page_controls">Controlos</string> + <string name="drawer_page_components">Componentes</string> + <string name="drawer_page_controller">Comando</string> + <string name="drawer_page_settings">Definições</string> + <string name="drawer_page_desc">Página %1$s</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Ambiente de trabalho</string> + <string name="drawer_steam_menu">Menu do Steam</string> + <string name="drawer_quick_access">Acesso rápido</string> + <string name="drawer_open_qam">Abrir o Menu de acesso rápido</string> + <string name="drawer_prev_page">Página anterior</string> + <string name="drawer_next_page">Página seguinte</string> + <string name="drawer_hud">HUD de desempenho</string> + <string name="drawer_scaling">Modo de escala</string> + <string name="drawer_scaling_sharpness">Nitidez da escala</string> + <string name="drawer_effects">Efeitos de ecrã</string> + <string name="drawer_effects_hint">Em tempo real. Gasta algum tempo de GPU enquanto houver um efeito ativo.</string> + <string name="drawer_look">Predefinições</string> + <string name="drawer_look_custom">Personalizado</string> + <string name="drawer_effects_advanced">Avançado</string> + <string name="widgets_expanded">Expandido</string> + <string name="widgets_collapsed">Recolhido</string> + <string name="drawer_cas">Nitidez (CAS)</string> + <string name="drawer_cas_level">Nível de nitidez</string> + <string name="drawer_fake_hdr">HDR simulado</string> + <string name="drawer_deband">Redução de bandas</string> + <string name="drawer_deband_strength">Intensidade da redução de bandas</string> + <string name="drawer_brightness">Brilho</string> + <string name="drawer_contrast">Contraste</string> + <string name="drawer_gamma">Gama</string> + <string name="drawer_saturation">Saturação</string> + <string name="drawer_fxaa">FXAA (suavização de arestas)</string> + <string name="drawer_toon">Contornos de desenho animado</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Filtragem de texturas</string> + <string name="drawer_texture_hint">Jogos DirectX 9–11, a partir do próximo arranque.</string> + <string name="drawer_anisotropy">Filtragem anisotrópica</string> + <string name="drawer_texture_sharpness">Nitidez das texturas</string> + <string name="drawer_texture_sharpness_note">Um desvio de mip negativo: texturas mais nítidas, algum cintilar. Automático acompanha o quanto a sessão é ampliada para o ecrã.</string> + <string name="drawer_touch_auto">Automático (%1$s)</string> + <string name="drawer_keyboard">Teclado</string> + <string name="drawer_pc_keyboard">Teclado de PC</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">Segundo ecrã</string> + <string name="drawer_second_screen_mode">Modo</string> + <string name="drawer_second_screen_shows">Mostra</string> + <string name="drawer_android_apps">Apps Android</string> + <string name="drawer_launch_app">Iniciar uma app</string> + <string name="common_hide">Ocultar</string> + <string name="common_show">Mostrar</string> + <string name="drawer_no_apps">Nenhuma app para iniciar</string> + <string name="drawer_session_behavior">Comportamento da sessão</string> + <string name="drawer_suspend_hint">Aplica-se agora e às próximas sessões neste modo.</string> + <string name="drawer_next_session">Próxima sessão</string> + <string name="drawer_support">Suporte</string> + <string name="drawer_logs">Registos da sessão</string> + <string name="drawer_logs_hint">Para anexar a um relatório de erros</string> + <string name="drawer_share_logs">Partilhar registos</string> + <string name="drawer_background">Segundo plano</string> + <string name="drawer_stop">Terminar</string> + <string name="drawer_stop_title">Terminar sessão?</string> + <string name="drawer_running_now">Em execução</string> + <string name="drawer_no_game">Nenhum jogo em execução num Proton</string> + <string name="drawer_selected_profile">Perfil do jogo</string> + <string name="drawer_selected_profile_none">Os detalhes do perfil ainda não estão disponíveis.</string> + <string name="drawer_profile_refresh_failed_initial">Não foi possível carregar os perfis dos jogos. A tentar novamente de forma automática.</string> + <string name="drawer_profile_refresh_failed_stale">Não foi possível atualizar os perfis dos jogos. A mostrar os últimos dados disponíveis; a tentar novamente de forma automática.</string> + <string name="drawer_profile_inspect_hint">Ver e editar as definições guardadas que são utilizadas no próximo arranque.</string> + <string name="drawer_profile_default">A utilizar o Perfil padrão</string> + <string name="drawer_profile_custom">Perfil personalizado</string> + <string name="drawer_component_inherited">A utilizar %1$s do Perfil padrão.</string> + <string name="drawer_game_components_note">As alterações são guardadas no perfil de %1$s e aplicam-se na próxima vez que o jogo for iniciado.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d substituição do jogo</item> + <item quantity="many">%1$d substituições do jogo</item> + <item quantity="other">%1$d substituições do jogo</item> + </plurals> + <string name="drawer_no_proton">Ainda não há nenhum Proton instalado.</string> + <string name="drawer_game_running">jogo em execução</string> + <string name="drawer_reapplied">reaplicado no arranque %1$s</string> + <string name="drawer_original">Original</string> + <string name="drawer_components_note">Importe e elimine na página Componentes.</string> + <string name="setup_check_done">Concluído</string> + <string name="setup_check_warn">Requer atenção</string> + <string name="setup_check_busy">A processar</string> + <string name="setup_limit_adb_failed">Verifique o endereço IP e a porta da Depuração sem fios, ou sincronize novamente se o Android tiver removido este dispositivo.</string> + <string name="setup_install">Instalar</string> + <string name="setup_manage">Gerir</string> + <string name="setup_tab_overview">Vista geral</string> + <string name="setup_tab_controller">Comando</string> + <string name="setup_tab_session">Sessão</string> + <string name="setup_tab_launcher">Iniciador</string> + <string name="setup_title">Configuração</string> + <string name="setup_all_set">● Tudo pronto</string> + <string name="setup_n_ready">%1$d de %2$d prontos</string> + <string name="setup_system_check">Verificação do sistema</string> + <string name="setup_gpu_supported">Dispositivo suportado</string> + <string name="setup_gpu_untested">GPU não testada</string> + <string name="setup_gpu_unsupported">GPU não suportada</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">O Steam requer um controlador Adreno; %1$s pode mostrar um ecrã preto</string> + <string name="setup_runtime">Ambiente de execução Linux</string> + <string name="setup_runtime_progress">%1$s · %2$d%%</string> + <string name="setup_runtime_missing">Não instalado · cerca de 3 GB, instalado ao tocar em Jogar pela primeira vez</string> + <string name="setup_installed">Instalado</string> + <string name="setup_runtime_update">%1$s · atualização disponível</string> + <string name="setup_runtime_current">%1$s · atualizado</string> + <string name="setup_limit">Limite de processos secundários</string> + <string name="setup_limit_on">Ativado · o Android pode fechar o Steam. Desativá-lo demora um minuto.</string> + <string name="setup_limit_unset">Não definido · o Android pode fechar o Steam. Desativá-lo demora um minuto.</string> + <string name="setup_limit_unknown">Não foi possível verificar · desativá-lo demora um minuto.</string> + <string name="setup_fix_it">Corrigir</string> + <string name="setup_details">Detalhes</string> + <string name="setup_dev_options">Opções de programador</string> + <string name="setup_use_wireless">Utilizar a Depuração sem fios</string> + <string name="setup_check_again">Verificar novamente</string> + <string name="setup_limit_turn_on">Ativar limite</string> + <string name="setup_account">Conta Steam</string> + <string name="setup_signed_in_offline">Sessão iniciada como %1$s · modo offline</string> + <string name="setup_signed_in">Sessão iniciada como %1$s</string> + <string name="setup_sign_in">Prima Jogar e inicie sessão no Steam</string> + <string name="setup_tools">Ferramentas</string> + <string name="setup_controller">Comando</string> + <string name="setup_controller_unavailable">As definições do comando não estão disponíveis.</string> + <string name="setup_session">Sessão</string> + <string name="setup_logs">Registos da sessão</string> + <string name="setup_logs_hint">Guardados após cada sessão</string> + <string name="setup_agent_commands">Ferramentas de depuração</string> + <string name="setup_latest_logs">Registos da última sessão</string> + <string name="setup_saved_logs">Registos de sessões guardados</string> + <string name="setup_saved_logs_hint">Mantém os %1$d mais recentes</string> + <string name="setup_clear_logs">Limpar registos</string> + <string name="setup_offline">Modo offline</string> + <string name="setup_sign_in_first">Inicie sessão no Steam primeiro</string> + <string name="setup_launcher">Iniciador</string> + <string name="setup_language">Idioma</string> + <string name="setup_language_hint">O Steam também o utiliza, a partir do próximo arranque.</string> + <string name="setup_language_system">Predefinição do sistema (%1$s)</string> + <string name="setup_language_system_short">Predefinição do sistema</string> + <string name="setup_app_scale">Escala da app</string> + <string name="setup_app_scale_hint">Texto e controlos do DroidDeck</string> + <string name="setup_app_scale_default">%1$d%% (padrão)</string> + <string name="setup_theme">Tema</string> + <string name="setup_theme_hint">Aspeto do iniciador</string> + <string name="setup_home">Utilizar como página inicial</string> + <string name="setup_home_on">O DroidDeck pode ser a app de página inicial</string> + <string name="setup_home_off">O DroidDeck nunca é proposto como app de página inicial</string> + <string name="setup_orientation">Orientação</string> + <string name="setup_orientation_auto">Horizontal automática</string> + <string name="setup_orientation_landscape">Horizontal</string> + <string name="setup_orientation_reverse">Horizontal invertida</string> + <string name="setup_fullscreen">Ecrã inteiro</string> + <string name="setup_fullscreen_on">Oculta as barras de estado e de navegação do Android</string> + <string name="setup_fullscreen_off">Mostra as barras de estado e de navegação do Android</string> + <string name="setup_default_home">App de página inicial predefinida</string> + <string name="setup_choose_home">Escolher uma app de página inicial</string> + <string name="setup_choose">Escolher</string> + <string name="setup_linux_apps">Apps Linux (beta)</string> + <string name="setup_tool_files">Ficheiros</string> + <string name="setup_tool_files_hint">Explorar e gerir ficheiros</string> + <string name="setup_tool_protons">Versões do Proton</string> + <string name="setup_tool_protons_hint">Instalar versões ARM64 do Proton</string> + <string name="setup_tool_performance">Desempenho</string> + <string name="setup_tool_performance_hint">Atribuição de núcleos da CPU</string> + <string name="setup_tool_roms">Pasta de ROMs</string> + <string name="setup_tool_roms_hint">Onde são guardados os jogos de emuladores</string> + <string name="setup_card_components">Componentes</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Controlos</string> + <string name="setup_card_controls_hint">Mapeamento de botões</string> + <string name="store_tab_discover">Descobrir</string> + <string name="store_tab_search">Pesquisar</string> + <string name="store_tab_installed">Instaladas</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Loja</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak pronto</string> + <string name="store_needs_runtime">Instale primeiro o ambiente de execução Linux na Configuração.</string> + <string name="store_setup_title">Configurar o Flatpak</string> + <string name="store_setup_text">Adiciona o Flatpak e liga-o ao Flathub (cerca de 20 MB). A primeira app também transfere o respetivo ambiente de execução.</string> + <string name="store_setting_up">A configurar…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Atualizações</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d%%</string> + <string name="store_starting">A iniciar…</string> + <string name="store_categories">Categorias</string> + <string name="store_unreachable">Não foi possível aceder ao Flathub. Verifique a sua ligação e tente novamente.</string> + <string name="store_try_again">Tentar novamente</string> + <string name="store_search_label">Pesquisar no Flathub</string> + <string name="store_search_placeholder">Firefox, jogos, emuladores…</string> + <string name="store_search">Pesquisar</string> + <string name="store_all">Todas</string> + <string name="store_searching">A pesquisar…</string> + <string name="store_search_hint">Pesquise por nome ou escolha uma categoria. Só são listadas apps ARM64.</string> + <string name="store_no_results">Nenhuma app ARM64 no Flathub corresponde a essa pesquisa.</string> + <string name="store_results">Resultados</string> + <string name="store_check_updates">Procurar atualizações</string> + <string name="store_update_all">Atualizar todas (%1$d)</string> + <string name="store_all_current">● Tudo atualizado</string> + <string name="store_no_apps">Ainda não há apps. Encontre algumas em Descobrir ou Pesquisar.</string> + <string name="store_setup_first_note">Configure o Flatpak para instalar apps.</string> + <string name="store_update_available_prefix">Atualização disponível · %1$s</string> + <string name="store_open">Abrir</string> + <string name="store_details">Detalhes</string> + <string name="store_remove">Remover</string> + <string name="store_press_again">Prima novamente para remover</string> + <string name="store_installing">A instalar…</string> + <string name="store_install">Instalar</string> + <string name="store_setup_first">Configure primeiro o Flatpak</string> + <string name="store_download_size">Transferência de %1$s</string> + <string name="store_installed_chip">● Instalada</string> + <string name="store_details_failed">Não foi possível carregar a página desta app no Flathub.</string> + <string name="store_loading">A carregar do Flathub…</string> + <string name="store_no_arm">Não há versão ARM64 no Flathub, por isso não pode ser instalada aqui.</string> + <string name="store_screenshots">Capturas de ecrã</string> + <string name="store_about">Acerca de</string> + <string name="store_app_id">ID da app</string> + <string name="store_license">Licença</string> + <string name="store_runtime">Ambiente de execução</string> + <string name="store_installed_size">Tamanho instalado</string> + <string name="store_installed_size_value">%1$s (mais o ambiente de execução partilhado)</string> + <string name="store_website">Site</string> + <string name="store_verified">Verificado</string> + <string name="upd_title">Atualizações</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Verificado %1$s</string> + <string name="upd_not_checked">Ainda não verificado</string> + <string name="upd_perm_title">Permitir que o DroidDeck se atualize</string> + <string name="upd_perm_text">No ecrã seguinte, ative Permitir desta fonte e depois volte. A atualização continua sozinha.</string> + <string name="upd_open_settings">Abrir definições</string> + <string name="upd_not_now">Agora não</string> + <string name="upd_signed_status">Assinatura diferente</string> + <string name="upd_signed_headline">Não é possível atualizar diretamente</string> + <string name="upd_signed_detail">Esta cópia não está assinada com a chave de lançamento do DroidDeck, por isso as atualizações não podem ser instaladas por cima dela. Desinstale-a para mudar.</string> + <string name="upd_available">Atualização disponível</string> + <string name="upd_blocked_status">Não é possível instalar</string> + <string name="upd_switch">Pronto para mudar</string> + <string name="upd_ahead_status">À frente do Estável</string> + <string name="upd_ahead_headline">Está à frente do Estável</string> + <string name="upd_ahead_detail">Esta versão é mais recente do que a última Estável. Passará para o Estável quando sair a próxima versão.</string> + <string name="upd_gone_status">Teste terminado</string> + <string name="upd_gone_headline">Este teste terminou</string> + <string name="upd_gone_detail">A correção foi integrada ou abandonada. Siga a Antevisão para obter as correções mais recentes.</string> + <string name="upd_current_status">Atualizado</string> + <string name="upd_current_headline">Já tem a versão mais recente: %1$s</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Atualizar%1$s</string> + <string name="upd_install_button">Instalar%1$s</string> + <string name="upd_install_anyway">Instalar a versão Estável %1$s mesmo assim</string> + <string name="upd_follow_preview">Seguir a Antevisão</string> + <string name="upd_stop_session">Termine a sessão em execução para atualizar.</string> + <string name="upd_less">Menos</string> + <string name="upd_more">Mais</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Nova versão de Antevisão</string> + <string name="upd_new_test">Versão de teste do PR #%1$d</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">CANAL DE ATUALIZAÇÃO</string> + <string name="upd_stable">Estável</string> + <string name="upd_stable_hint">Versões testadas, para a maioria das pessoas</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Antevisão</string> + <string name="upd_preview_hint">As correções mais recentes, antes do Estável</string> + <string name="upd_preview_current">Atualizado</string> + <string name="upd_tests">Versões de teste</string> + <string name="upd_tests_hint">Experimente uma correção antes de ser lançada</string> + <string name="upd_tests_none">Nenhuma de momento</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Não compilada para esta cópia do DroidDeck</string> + <string name="upd_updated">Atualizado %1$s</string> + <string name="upd_test_name">o teste do PR #%1$d</string> + <string name="upd_while_ago">há algum tempo</string> + <string name="upd_just_now">agora mesmo</string> + <plurals name="upd_tests_count"> + <item quantity="one">%1$d para experimentar</item> + <item quantity="many">%1$d para experimentar</item> + <item quantity="other">%1$d para experimentar</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="one">Alterações desde a sua versão · %1$d versão em atraso</item> + <item quantity="many">Alterações desde a sua versão · %1$d versões em atraso</item> + <item quantity="other">Alterações desde a sua versão · %1$d versões em atraso</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="one">Alterações recentes · %1$d versão</item> + <item quantity="many">Alterações recentes · %1$d versões</item> + <item quantity="other">Alterações recentes · %1$d versões</item> + </plurals> + <string name="gate_title">Uma definição do Android a alterar</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">O Android termina as apps que iniciam muitos processos em segundo plano, e o Steam inicia dezenas. Enquanto isto não for alterado, os jogos fecham sem qualquer erro.</string> + <string name="gate_fix_for_me">Corrigir por mim</string> + <string name="gate_toggle_steps">Ative “Desativar restrições de processos secundários” e depois volte. Mantenha as Opções de programador ativadas, ou o interruptor volta a desligar-se.</string> + <string name="gate_open_dev">Abrir Opções de programador</string> + <string name="gate_no_toggle">Esta versão do Android não tem um interruptor para isto, por isso o DroidDeck altera-o através da Depuração sem fios. Demora cerca de um minuto e não é preciso computador.</string> + <string name="gate_needs_wifi">A Depuração sem fios requer Wi-Fi. Qualquer rede serve; não precisa de Internet.</string> + <string name="gate_needs_notifications">O código de sincronização é introduzido numa notificação. Permita as notificações do DroidDeck ou utilize Outras formas.</string> + <string name="gate_allow_notifications">Permitir notificações</string> + <string name="gate_step1">Abra Definições → Opções de programador → Depuração sem fios e ative-a</string> + <string name="gate_step2">Toque em “Sincronizar dispositivo com código de sincronização”</string> + <string name="gate_step3">Escreva o código na notificação do DroidDeck</string> + <string name="gate_waiting">A aguardar a janela de sincronização…</string> + <string name="gate_code_needed">Janela encontrada. Introduza o código dela na notificação.</string> + <string name="gate_status">Estado</string> + <string name="gate_polling">A verificar a cada 2 segundos. Esta página fecha-se quando estiver desativado.</string> + <string name="gate_other_ways">Outras formas</string> + <string name="gate_other_toggle">Depuração sem fios, endereço manual, ADB num computador</string> + <string name="gate_other_no_toggle">Endereço manual, ADB num computador, Opções de programador</string> + <string name="gate_use_wireless_instead">Utilizar antes a Depuração sem fios</string> + <string name="gate_some_roms">Algumas ROMs adicionam o interruptor mesmo assim. Procure “processos secundários” nas Opções de programador.</string> + <string name="gate_manual_hint">Se a notificação não funcionar, introduza manualmente o endereço e o código da janela.</string> + <string name="gate_enter_manually">Introduzir endereço manualmente</string> + <string name="gate_from_computer">A partir de um computador com ADB:</string> + <string name="gate_copy_command">Copiar comando</string> + <string name="gate_choose_display">Escolha em que ecrã abrir as Definições do Android.</string> + <string name="gate_main_screen">Ecrã principal</string> + <string name="wadb_finding_port">Sincronizado. A procurar a porta da Depuração sem fios…</string> + <string name="wadb_enter_port">Sincronizado. Introduza a porta indicada nas Definições.</string> + <string name="wadb_applying">Dispositivo encontrado. A aplicar a definição…</string> + <string name="wadb_apply_failed">Não foi possível aplicar automaticamente. Verifique o endereço abaixo e tente novamente. %1$s</string> + <string name="wadb_title_wireless">Depuração sem fios</string> + <string name="wadb_title_done">Limite de processos secundários atualizado</string> + <string name="wadb_title">Alterar o limite de processos secundários</string> + <string name="wadb_lede_compact0">Se a opção não existir, sincronize através da Depuração sem fios.</string> + <string name="wadb_lede_compact1">Sincronizado. Verifique o endereço IP e a porta na Depuração sem fios.</string> + <string name="wadb_lede_done">O limite de processos secundários foi atualizado.</string> + <string name="wadb_lede0">Utilize o passo 2 se as Opções de programador não tiverem a definição de processos secundários. Abra Opções de programador → Depuração sem fios → Sincronizar dispositivo com código de sincronização.</string> + <string name="wadb_lede1">Sincronizado. Utilize o endereço abaixo ou atualize-o em Depuração sem fios → Endereço IP e porta.</string> + <string name="wadb_lede_done_long">O limite de processos secundários foi atualizado através da Depuração sem fios.</string> + <string name="wadb_confirmed_on">O Android confirmou que o limite está ativado.</string> + <string name="wadb_confirmed_off">O Android confirmou que o limite está desativado.</string> + <string name="wadb_pair_group">2 · Sincronizar a Depuração sem fios</string> + <string name="wadb_pair_address">Endereço IP e porta da janela de sincronização</string> + <string name="wadb_pin">Código de sincronização (PIN)</string> + <string name="wadb_pin_placeholder">6 dígitos</string> + <string name="wadb_pair">Sincronizar</string> + <string name="wadb_apply_group">2 · Aplicar através da Depuração sem fios</string> + <string name="wadb_connect_address">Endereço IP e porta de ligação</string> + <string name="wadb_pair_again">Sincronizar novamente</string> + <string name="wadb_turn_off">Desativar limite</string> + <string name="wadb_other_ways">Outras formas de o alterar</string> + <string name="wadb_dev_try_first">1 · Opções de programador (tente primeiro)</string> + <string name="wadb_restrict_on">Ative “Restringir processos secundários”.</string> + <string name="wadb_restrict_off">Desative “Restringir processos secundários”.</string> + <string name="wadb_computer_last">3 · ADB num computador (último recurso)</string> + <string name="wadb_only_if">Apenas se a Depuração sem fios não estiver disponível.</string> + <string name="wadb_computer_group">3 · ADB num computador (último recurso)</string> + <string name="ctrl_osc_hint">Quando os controlos táteis aparecem numa sessão</string> + <string name="ctrl_color">Cor</string> + <string name="ctrl_color_hint">Tonalidade dos botões no ecrã</string> + <string name="ctrl_custom">Personalizada</string> + <string name="ctrl_opacity">Opacidade</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">Tamanho dos botões</string> + <string name="ctrl_size_hint" formatted="false">O tamanho padrão é 100%</string> + <string name="ctrl_stick_click">Clique do analógico</string> + <string name="ctrl_stick_click_hint">Toque duas vezes num analógico e mantenha para L3 ou R3</string> + <string name="ctrl_adaptive">Analógicos adaptáveis</string> + <string name="ctrl_rumble">Vibração do jogo</string> + <string name="ctrl_steam_button">Botão Steam</string> + <string name="ctrl_qam_button">Botão QAM</string> + <string name="ctrl_keyboard_button">Botão do teclado</string> + <string name="ctrl_select_steam">Premir Select para abrir o menu do Steam</string> + <string name="ctrl_select_steam_hint">Em vez de View/Select.</string> + <string name="setup_animations">Animações</string> + <string name="ctrl_adaptive_hint">Os analógicos aparecem onde tocar e ocultam-se ao soltar</string> + <string name="ctrl_layout">Esquema</string> + <string name="ctrl_layout_custom">Posições personalizadas guardadas</string> + <string name="ctrl_layout_default">Ajustado a este ecrã</string> + <string name="ctrl_edit">Editar</string> + <string name="ctrl_reset">Repor</string> + <string name="ctrl_mapping">Mapeamento de botões</string> + <string name="ctrl_mapping_default">Cada botão envia a sua própria entrada</string> + <string name="ctrl_mapping_count">%1$d de %2$d botões remapeados</string> + <string name="ctrl_configure">Configurar</string> + <string name="ctrl_reset_all">Repor comando</string> + <string name="ctrl_reset_all_hint">Restaura todas as definições do comando e o esquema</string> + <string name="ctrl_mapping_lede">O que cada botão no ecrã envia para o jogo. Oculto remove o botão.</string> + <string name="ctrl_onscreen_buttons">Botões no ecrã</string> + <string name="ctrl_defaults">Valores padrão</string> + <string name="ctrl_reset_mapping">Repor mapeamento</string> + <string name="content_android_apps">Apps Android</string> + <string name="content_apps_count">%1$d apps</string> + <string name="content_no_android_apps">Não foram encontradas apps Android para iniciar.</string> + <string name="content_desktop_installed">● Ambiente de trabalho instalado</string> + <string name="content_desktop_first_open">É instalado ao abrir pela primeira vez</string> + <string name="content_available">Disponível para instalar</string> + <string name="content_not_installed">Não instalado.</string> + <string name="content_not_installed_suffix">%1$s · não instalado</string> + <string name="content_open_named">Abrir %1$s</string> + <string name="content_games">Jogos</string> + <string name="content_choose_roms">Escolha uma pasta de ROMs.</string> + <string name="content_retroarch_roms">Navegue até /root/ROMs no RetroArch.</string> + <string name="content_add_roms">Adicione jogos de %1$s a ROMs/%2$s.</string> + <string name="content_install_named">Instalar %1$s</string> + <string name="content_stop_to_install">Termine a sessão para instalar</string> + <string name="content_runtime_required">Ambiente de execução necessário</string> + <string name="content_loading_details">A carregar os detalhes da instalação…</string> + <string name="content_details_unavailable">Os detalhes da instalação não estão disponíveis. Tente novamente mais tarde.</string> + <string name="content_needs_runtime">Instale primeiro o ambiente de execução Linux na Configuração.</string> + <string name="content_stop_first">Termine a sessão em execução para instalar apps do ambiente de trabalho.</string> + <string name="content_install_into">Instala %1$s no ambiente de trabalho Linux.</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">Ocultar remove-o do Ambiente de trabalho. Os ficheiros permanecem no ambiente de execução Linux.</string> + <string name="content_rom_gone">Esse jogo já não está na pasta de ROMs.</string> + <string name="content_desktop_system">Ambiente de trabalho · %1$s</string> + <string name="content_launch_in">Iniciar em %1$s</string> + <string name="content_also_in">Também em %1$s</string> + <string name="content_page_gone">Essa página já não existe. Escolha uma secção à esquerda.</string> + <string name="content_open_desktop">Abrir ambiente de trabalho</string> + <string name="content_install_open_desktop">Instalar e abrir ambiente de trabalho</string> + <string name="desktop_remove_title">Desinstalar ambiente de trabalho</string> + <string name="desktop_remove_text">Remove o ambiente de trabalho e as apps incluídas. Mantém os jogos, as apps adicionadas, as definições e as bibliotecas partilhadas.</string> + <string name="desktop_removing">A remover o ambiente de trabalho</string> + <string name="desktop_removed">Ambiente de trabalho removido</string> + <string name="desktop_cleaning_legacy">A remover ficheiros antigos do LXQt</string> + <string name="desktop_removal_unknown">Atualize o DroidDeck antes de remover esta versão do ambiente de trabalho.</string> + <string name="content_linux_desktop">Ambiente de trabalho Linux</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox e os seus emuladores</string> + <string name="games_empty">Os jogos que instalar no Steam aparecem aqui após a sessão.</string> + <string name="games_play_steam">Jogar no Steam</string> + <string name="games_launch_settings">Definições de arranque</string> + <string name="games_launch">Iniciar</string> + <string name="games_files">Ficheiros do jogo</string> + <string name="games_prefix">Prefixo do Proton</string> + <string name="games_saves_looking">A procurar gravações…</string> + <string name="games_saves_none">Nenhuma pasta de gravações encontrada pelo nome. Exportar inclui toda a pasta do utilizador.</string> + <string name="games_saves_folders">%1$d pastas de gravações</string> + <string name="games_saves_summary">%1$s · %2$d ficheiros · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">Gerir gravações</string> + <string name="games_saves">Gravações do jogo</string> + <string name="games_import_saves">Importar gravações…</string> + <string name="games_export_gamehub">Exportar como zip do GameHub…</string> + <string name="games_export_winlator">Exportar como zip do Winlator…</string> + <string name="games_open_save_folder">Abrir pasta de gravações</string> + <string name="games_never_played">Nunca jogado</string> + <string name="games_runtime_busy_percent">Ambiente de execução ocupado · %1$d%%</string> + <string name="games_runtime_busy">Ambiente de execução ocupado</string> + <string name="games_runtime_first_play">O ambiente de execução é instalado ao tocar em Jogar pela primeira vez</string> + <string name="games_runtime_update">Atualização do ambiente de execução disponível</string> + <string name="games_runtime_ready">● Ambiente de execução pronto</string> + <string name="games_last_played">Jogado pela última vez %1$s</string> + <string name="games_internal">Armazenamento interno</string> + <string name="games_added">Jogo adicionado</string> + <string name="games_custom">Jogo personalizado</string> + <string name="games_store_storage">Armazenamento de jogos</string> + <string name="gpu_auto_hint">O melhor par para esta GPU, sempre atualizado</string> + <string name="gpu_manual_hint">Escolha um par manualmente</string> + <string name="gpu_checking">A procurar controladores…</string> + <string name="gpu_none_listed">Ainda não há controladores listados. Atualize para os procurar.</string> + <string name="gpu_hide_other">Ocultar controladores para outras GPUs</string> + <string name="gpu_show_other">Mostrar controladores para outras GPUs</string> + <string name="gpu_advanced">Avançado</string> + <string name="gpu_set_with_display">Definido em conjunto com o controlador de ecrã: %1$s inclui ambos.</string> + <string name="gpu_set_with_runtime">Definido em conjunto com o controlador de execução: %1$s inclui ambos.</string> + <string name="gpu_runtime_hint">Utilizado pelos jogos e pelo ambiente de trabalho.</string> + <string name="gpu_runtime_default">Padrão do ambiente de execução</string> + <string name="gpu_display_hint">Desenha os fotogramas no ecrã. Aplica-se depois de o DroidDeck reiniciar.</string> + <string name="gpu_auto_by_gpu">Automático - escolhido conforme a GPU</string> + <string name="gpu_manage">Transferir, importar ou eliminar…</string> + <string name="gpu_in_use_caps">EM UTILIZAÇÃO</string> + <string name="gpu_builtin">Os controladores integrados no DroidDeck</string> + <string name="gpu_matched_pair">Controladores de execução e de ecrã definidos em conjunto como um par correspondente.</string> + <string name="gpu_recommended_for">Recomendado para esta GPU: %1$s.</string> + <string name="gpu_refresh_hint">Atualize para procurar controladores para esta GPU.</string> + <string name="gpu_recommended_caps">RECOMENDADO</string> + <string name="gpu_unavailable">Indisponível</string> + <string name="load_runtime">Ambiente de execução Linux</string> + <string name="load_runtime_detail">A preparar o ambiente de execução Linux</string> + <string name="load_session">Sessão</string> + <string name="load_session_detail">A iniciar o ecrã e o áudio</string> + <string name="load_steam">Cliente Steam</string> + <string name="load_steam_detail">A verificar o Steam e as respetivas ferramentas de compatibilidade</string> + <string name="load_opening_steam">A abrir o Steam</string> + <string name="load_opening_steam_detail">A aguardar o primeiro fotograma do Steam</string> + <string name="load_desktop">Ambiente de trabalho</string> + <string name="load_desktop_detail">A preparar o ambiente de trabalho</string> + <string name="load_opening">A abrir</string> + <string name="load_opening_detail">A aguardar o primeiro fotograma</string> + <string name="load_starting_steam">A iniciar o Steam</string> + <string name="load_ended">A sessão terminou</string> + <string name="load_step">Passo %1$d de %2$d</string> + <string name="load_steam_paused">O Steam está em pausa</string> + <string name="deck_tab_grips">Pegas</string> + <string name="deck_tab_left">Painel esquerdo</string> + <string name="deck_tab_right">Painel direito</string> + <string name="deck_tab_both">Ambos</string> + <string name="deck_tab_gamepad">Comando</string> + <string name="deck_surface_stick">Joystick</string> + <string name="deck_surface_trackpad">Trackpad</string> + <string name="deck_surface_switch">Alternar entre joystick e trackpad</string> + <string name="deck_more_controls">Mais controlos do Deck</string> + <string name="deck_panels">PAINÉIS</string> + <string name="deck_close">Fechar controlos do segundo ecrã</string> + <string name="deck_open_steam_menu">Abrir menu Steam</string> + <string name="deck_left_upper">Esquerda · superior</string> + <string name="deck_left_lower">Esquerda · inferior</string> + <string name="deck_right_upper">Direita · superior</string> + <string name="deck_right_lower">Direita · inferior</string> + <string name="deck_click">Clique</string> + <string name="deck_right_trackpad">Trackpad direito</string> + <string name="deck_left_trackpad">Trackpad esquerdo</string> + <string name="second_trackpad">Trackpad</string> + <string name="second_show_keyboard">Mostrar teclado</string> + <string name="second_hide_keyboard">Ocultar teclado</string> + <string name="second_terminal">Terminal Linux</string> + <string name="second_terminal_not_ready">A sessão Linux ainda não está pronta para um terminal.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Jogos</string> + <string name="rail_desktop">Desktop</string> + <string name="rail_store">Loja</string> + <string name="rail_components">Componentes</string> + <string name="rail_apps">Apps</string> + <string name="rail_setup">Configuração</string> + <string name="rail_updates">Atualizações</string> + <string name="rail_percent">%1$d%%</string> + <string name="rail_resume_named">Retomar %1$s</string> + <string name="roms_text">Aparece como /root/ROMs em cada sessão. O armazenamento interno está em /root/Storage.</string> + <string name="roms_none">Nenhuma pasta escolhida</string> + <string name="roms_applies">Aplica-se na próxima sessão. Funciona com cartões SD.</string> + <string name="roms_choose">Escolher pasta</string> + <string name="roms_change">Alterar pasta</string> + <string name="proton_lede">Instale uma versão ARM64 do Proton e escolha-a para cada jogo em Steam → Propriedades → Compatibilidade.</string> + <string name="proton_stop_first">Pare a sessão em curso para instalar ou remover uma ferramenta de compatibilidade.</string> + <string name="proton_available">Versões disponíveis</string> + <string name="proton_installed">Instalado: %1$s</string> + <string name="proton_queued">Pendente de um pedido anterior</string> + <string name="proton_not_installed">Não instalado</string> + <string name="proton_update_available">Atualização disponível → %1$s</string> + <string name="proton_remove_older">Remover versões antigas do %1$s</string> + <string name="proton_remove_older_body">Remove %1$s. Os jogos definidos para estas versões têm de mudar para outro Proton.</string> + <string name="proton_remove_body">Os jogos definidos para esta versão têm de mudar para outro Proton.</string> + <string name="proton_install_now">Instalar agora</string> + <string name="proton_cancel_queued">Cancelar pedido pendente</string> + <string name="pckb_android">Teclado Android</string> + <string name="pckb_hide">Ocultar ✕</string> + <string name="main_launch_named">Iniciar %1$s</string> + <string name="main_choose_screen">Escolha um ecrã. Secundário: %1$s.</string> + <string name="main_secondary_gone">O ecrã secundário já não está disponível.</string> + <string name="main_primary">Ecrã principal</string> + <string name="main_secondary">Ecrã secundário</string> + <string name="widgets_prev_tab">Separador anterior</string> + <string name="widgets_next_tab">Separador seguinte</string> + <string name="widgets_on">Ativado</string> + <string name="widgets_all_n">Todos (%1$d)</string> + <string name="widgets_none">Nenhum</string> + <string name="widgets_n_of_m">%1$d de %2$d</string> + <string name="widgets_off">Desativado</string> + <string name="screen_bottom">Ecrã inferior</string> + <string name="game_env_applies">Aplica-se no próximo arranque do jogo. O perfil de um jogo substitui o partilhado.</string> + <string name="game_env_other">Outro jogo por ID da app…</string> + <string name="game_env_done">Concluído</string> + <string name="game_env_default">Predefinido</string> + <string name="game_env_off">Desativado</string> + <string name="game_env_change">Alterar…</string> + <string name="game_env_change_title">Alterar variável</string> + <string name="game_env_restore_default">Repor predefinição</string> + <string name="game_env_eyebrow">Jogos Proton</string> + <string name="pip_title">Ecrã no ecrã</string> + <string name="pip_auto">Entrar automaticamente em ecrã no ecrã</string> + <string name="unfolded_controls">Controlos no ecrã desdobrado</string> + <string name="unfolded_controls_hint">Desdobrado, o jogo fica acima dos trackpads e das pegas traseiras do Steam Deck. Dobrado, o jogo ocupa o ecrã inteiro.</string> + <string name="pip_suspended">Suspenso</string> + <string name="pip_not_ready">O ecrã no ecrã fica disponível quando a sessão estiver pronta.</string> + <string name="pip_permission">Permita o ecrã no ecrã para o DroidDeck nas definições do Android.</string> + <string name="pip_unavailable">O Android não conseguiu abrir o ecrã no ecrã.</string> + <string name="game_shortcut">Atalho</string> + <string name="game_shortcut_add">Adicionar ao ecrã principal</string> + <string name="game_shortcut_choose">Escolher jogo</string> + <string name="game_shortcut_scanning">A procurar jogos instalados…</string> + <string name="game_link_copy">Copiar link de arranque</string> + <string name="game_link_copied">Link de arranque copiado</string> + <string name="game_shortcut_unsupported">Este iniciador não permite adicionar atalhos. Use antes «Copiar link de arranque».</string> + <string name="game_link_invalid">Link de arranque do jogo inválido.</string> + <string name="game_link_missing">Este jogo já não está na sua biblioteca do DroidDeck.</string> + <string name="game_link_session_busy">Pare a sessão atual antes de iniciar um jogo do Steam.</string> + <string name="game_link_failed">Não foi possível enviar o jogo para o Steam. Tente iniciá-lo novamente.</string> + <string name="setup_retry_removal">Tentar remover novamente</string> + <string name="runtime_removing">A remover</string> + <string name="runtime_removal_incomplete">Remoção incompleta</string> + <string name="game_file_export">Exportar ficheiro do jogo</string> + <string name="game_file_sync">Sincronizar todos os ficheiros de jogos com uma pasta</string> + <string name="game_file_stop_sync">Parar a sincronização dos ficheiros de jogos</string> + <string name="game_file_folder">Escolha uma pasta para os ficheiros de jogos do frontend</string> + <string name="game_file_exported">Ficheiros de jogos guardados em %1$s</string> + <string name="game_frontend_files">Ficheiros para frontends</string> + + <string name="main_wait_runtime">Aguarde que a operação do runtime termine</string> + <string name="main_wait_proton_install">Aguarde que a instalação da ferramenta de compatibilidade termine</string> + <string name="main_wait_desktop_install">Aguarde que a instalação da app de ambiente de trabalho termine</string> + <string name="main_wait_install_game">Aguarde que a instalação termine antes de iniciar um jogo</string> + <string name="main_saves_close_session">Feche primeiro a sessão do Steam, para que o jogo não grave por cima da importação</string> + <string name="main_saves_importing">A importar para %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="one">%1$d ficheiro importado do %2$s para %3$s</item> + <item quantity="many">%1$d ficheiros importados do %2$s para %3$s</item> + <item quantity="other">%1$d ficheiros importados do %2$s para %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">%1$d ficheiro importado do %2$s para %3$s. Cópia de segurança dos dados guardados antigos em Download/DroidDeck/Saves/backups</item> + <item quantity="many">%1$d ficheiros importados do %2$s para %3$s. Cópia de segurança dos dados guardados antigos em Download/DroidDeck/Saves/backups</item> + <item quantity="other">%1$d ficheiros importados do %2$s para %3$s. Cópia de segurança dos dados guardados antigos em Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Escolha um zip de dados guardados para %1$s</string> + <string name="main_saves_exporting">A exportar %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="one">%1$d ficheiro exportado como %2$s: %3$s</item> + <item quantity="many">%1$d ficheiros exportados como %2$s: %3$s</item> + <item quantity="other">%1$d ficheiros exportados como %2$s: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Escolha onde guardar %1$s (%2$s)</string> + <string name="main_action_failed">%1$s: falhou (%2$s)</string> + <string name="main_no_logs_yet">Ainda não há registos de sessão: execute primeiro uma sessão.</string> + <string name="main_no_logs_to_clear">Não há registos de sessão para limpar.</string> + <plurals name="main_logs_cleared"> + <item quantity="one">Foi limpa %1$d pasta de registos de sessão.</item> + <item quantity="many">Foram limpas %1$d pastas de registos de sessão.</item> + <item quantity="other">Foram limpas %1$d pastas de registos de sessão.</item> + </plurals> + <string name="main_wadb_pair_failed">Falha na sincronização da depuração sem fios</string> + <string name="main_wadb_command_failed">Falha no comando de depuração sem fios</string> + <string name="main_wadb_unavailable">A depuração sem fios não está disponível</string> + <string name="main_adb_copied">Comando ADB copiado</string> + <string name="main_pick_roms">Escolha a pasta de ROMs</string> + <string name="main_non_adreno_title">Não é uma GPU Adreno</string> + <string name="main_non_adreno_text">O Turnip suporta GPUs Adreno. Com %1$s, o Steam pode mostrar um ecrã preto. Transferência: %2$.0f MB.</string> + <string name="main_install_anyway">Instalar mesmo assim</string> + <string name="main_remove_runtime_title">Remover runtime Linux</string> + <string name="main_remove_runtime_text">Isto elimina o runtime, o cliente Steam que nele existe e todos os jogos lá instalados.</string> + <string name="main_applied">Aplicado: %1$s</string> + <string name="android_app_open_failed_primary">Não foi possível abrir %1$s no ecrã principal</string> + <string name="android_app_open_failed_on">Não foi possível abrir %1$s em %2$s</string> + <string name="android_app_open_failed_display">Não foi possível abrir %1$s no ecrã %2$d</string> + <string name="comp_busy_importing">A importar</string> + <string name="comp_busy_swapping">A trocar</string> + <string name="comp_busy_restoring">A restaurar</string> + <string name="comp_busy_cancelling">A cancelar</string> + <string name="comp_busy_deleting">A eliminar</string> + <string name="comp_cannot_read">não é possível ler o ficheiro</string> + <string name="comp_imported">%1$s importado</string> + <string name="comp_swap_cancelled">A troca pendente foi cancelada.</string> + <string name="comp_pick_wcp">Escolha um pacote de componente (-linux .wcp)</string> + <string name="comp_nightlies_unreachable">Não foi possível aceder às Nightlies</string> + <string name="comp_stored">%1$s guardado</string> + <string name="comp_download_failed">Falha na transferência: %1$s</string> + <string name="drivers_pick_linux">Escolha um controlador do runtime Linux (zip -Linux ou pacote Android + Linux)</string> + <string name="drivers_pick_android">Escolha um controlador de ecrã (zip AdrenoTools ou pacote Android + Linux)</string> + <string name="drivers_pick_any">Escolha um zip de controlador ou um pacote Android + Linux</string> + <string name="main_pkg_problem">%1$s: %2$s</string> + <string name="main_removing_named">A remover %1$s…</string> + <string name="main_forgetting_named">A esquecer %1$s…</string> + <string name="main_pick_game_storage">Escolha a pasta de armazenamento de jogos</string> + <string name="main_pick_decky_zip">Escolha um ZIP de plugin Decky</string> + <string name="main_storage_not_usable">Não utilizável: %1$s</string> + <string name="main_running_program">Programa</string> + <string name="main_removing_runtime">A remover o runtime Linux</string> + <string name="main_settings_main_screen">Não foi possível abrir as Definições no ecrã inferior; foram abertas no ecrã principal.</string> + <string name="main_open_dev_options">Abra as Opções de programador nas Definições</string> + + <string name="upd_unknown_apps_failed">Não foi possível abrir a definição «Instalar apps desconhecidas» do Android</string> + <string name="upd_couldnt_check">Não foi possível verificar: %1$s</string> + <string name="upd_no_download">Esta versão não tem transferência para esta cópia do DroidDeck</string> + <string name="upd_stop_first">Pare primeiro a sessão em curso e depois atualize</string> + <string name="upd_stage_downloading">A transferir</string> + <string name="upd_stage_installing">A instalar</string> + <string name="upd_stage_confirm">A aguardar a sua confirmação</string> + <string name="upd_failed">A atualização falhou</string> + <string name="upd_not_offered">O Android não ofereceu a instalação da atualização</string> + + <string name="decky_install_failed">Falha na instalação do Decky: %1$s</string> + <string name="decky_plugin_starting">A iniciar a importação do plugin…</string> + <string name="decky_plugin_failed">Falha na importação do plugin: %1$s</string> + <string name="decky_plugin_installed">Plugin instalado. Reinicie o Steam para o carregar.</string> + + <string name="drivers_not_checked">Ainda não verificado - toque em atualizar para procurar novos controladores</string> + <string name="drivers_bundle_sets_display">Android + Linux: define também o controlador de ecrã</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: define também o controlador do runtime</string> + <string name="drivers_runtime_builtin">o Turnip incluído no runtime</string> + <string name="drivers_system_vulkan">Vulkan do sistema: sem versão incluída para esta GPU</string> + <string name="drivers_bundled">%1$s (incluído)</string> + <string name="drivers_manual">Manual</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">De momento, só uma das metades está publicada</string> + <string name="drivers_pair_size">%1$.0f MB para ambos</string> + <string name="drivers_none_to_set">Nenhum controlador para definir: %1$s</string> + <string name="drivers_checking_latest">A procurar os controladores mais recentes…</string> + <string name="drivers_up_to_date_check_failed">Atualizado (não foi possível verificar: %1$s)</string> + <string name="drivers_recommended_unpublished">Os controladores recomendados não estão publicados de momento</string> + <string name="drivers_downloading_pair">A transferir %1$s %2$s…</string> + <string name="drivers_updated_to">Atualizado para %1$s %2$s.</string> + <string name="drivers_updated_to_restart">Atualizado para %1$s %2$s. O controlador de ecrã aplica-se depois de reiniciar o DroidDeck.</string> + <string name="drivers_switched_to">Alterado para %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Alterado para %1$s %2$s. O controlador de ecrã aplica-se depois de reiniciar o DroidDeck.</string> + <string name="drivers_using">A usar %1$s %2$s.</string> + <string name="drivers_using_restart">A usar %1$s %2$s. O controlador de ecrã aplica-se depois de reiniciar o DroidDeck.</string> + <string name="drivers_not_a_driver">Não é um zip de controlador: não é um controlador AdrenoTools, um Turnip -Linux nem um pacote Android + Linux</string> + <string name="drivers_import_failed">Falha na importação: %1$s</string> + <string name="drivers_imported_unnamed">Controlador importado</string> + <string name="drivers_using_bundle">A usar %1$s como controlador do runtime e de ecrã.</string> + <string name="drivers_using_bundle_restart">A usar %1$s como controlador do runtime e de ecrã. O controlador de ecrã aplica-se depois de reiniciar o DroidDeck.</string> + <string name="drivers_deleted_bundle">%1$s eliminado (ambos os controladores)</string> + <string name="drivers_deleted">%1$s eliminado</string> + <string name="drivers_release_status">Mais recentes: %1$s · verificado %2$s</string> + <string name="drivers_release_status_failed">Mais recentes: %1$s · sem acesso a %2$s · verificado %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">há %1$d min</item> + <item quantity="many">há %1$d min</item> + <item quantity="other">há %1$d min</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">há %1$d h</item> + <item quantity="many">há %1$d h</item> + <item quantity="other">há %1$d h</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">há %1$d dia</item> + <item quantity="many">há %1$d dias</item> + <item quantity="other">há %1$d dias</item> + </plurals> + <string name="drivers_checking_sources">A verificar Banners-Turnip, WinNative e DroidDeck…</string> + <string name="drivers_check_failed">falha na verificação</string> + <string name="drivers_installed_pick_either">%1$s instalado - escolha-o em qualquer das listas para definir ambos os controladores</string> + <string name="drivers_installed_pick">%1$s instalado - escolha-o no menu</string> + + <string name="session_wait_removal">Aguarde que a remoção do runtime termine</string> + <string name="session_title_game">Jogo</string> + <string name="session_touch_auto_touchpad">painel tátil</string> + <string name="session_touch_auto_direct">direto</string> + <string name="session_swap_failed">Falha na troca: %1$s</string> + <string name="session_profile_save_failed">Falha ao guardar o perfil: %1$s</string> + <string name="session_desktop_paused">O ambiente de trabalho está em pausa</string> + <string name="session_named_paused">%1$s está em pausa</string> + <string name="session_program_paused">O programa está em pausa</string> + <string name="session_starting_desktop">A iniciar o ambiente de trabalho</string> + <string name="session_starting_named">A iniciar %1$s</string> + <string name="session_starting_program">A iniciar o programa</string> + <string name="session_no_logs">Não há registos para esta sessão.</string> + <string name="session_logs_archive_failed">Não foi possível criar o arquivo dos registos da sessão.</string> + <string name="session_logs_share_failed">Não foi possível partilhar os registos da sessão.</string> + <string name="session_starting_session">A iniciar a sessão…</string> + <string name="session_runtime_downloading">a transferir o runtime Linux</string> + <string name="session_runtime_downloading_mb">a transferir o runtime Linux · %1$d de %2$d MB</string> + <string name="session_runtime_checking">a verificar o runtime Linux</string> + <string name="session_runtime_unpacking">a descompactar o runtime Linux</string> + <string name="session_desktop_downloading">a transferir o ambiente de trabalho</string> + <string name="session_desktop_downloading_mb">a transferir o ambiente de trabalho · %1$d de %2$d MB</string> + <string name="session_desktop_checking">a verificar o ambiente de trabalho</string> + <string name="session_desktop_unpacking">a descompactar o ambiente de trabalho</string> + <string name="session_proton_downloading">a transferir o Proton Experimental (ARM64)</string> + <string name="session_proton_downloading_mb">a transferir o Proton Experimental (ARM64) · %1$d de %2$d MB</string> + <string name="session_proton_checking">a verificar o Proton Experimental (ARM64)</string> + <string name="session_proton_unpacking">a descompactar o Proton Experimental (ARM64)</string> + <string name="session_runtime_catalog_unreachable">Não foi possível aceder ao catálogo de runtimes. Verifique a ligação e prima Jogar novamente.</string> + <string name="session_runtime_install_failed">O runtime Linux não foi instalado. Verifique o runtime em Configuração e prima Jogar novamente.</string> + <string name="session_desktop_install_failed">O ambiente de trabalho não foi instalado (%1$s). Verifique a ligação e prima Ambiente de trabalho novamente.</string> + <string name="session_ended_named">%1$s parou inesperadamente. Partilhe os registos num relatório de erro ou tente novamente.</string> + <string name="session_ended_program">O programa parou inesperadamente. Partilhe os registos num relatório de erro ou tente novamente.</string> + <string name="session_ended_desktop">O ambiente de trabalho parou inesperadamente. Partilhe os registos num relatório de erro ou tente novamente.</string> + <string name="session_ended_detail">Código de saída %1$d · %2$s</string> + <string name="session_ended_no_log">sem registo</string> + <plurals name="session_hint_enosys"> + <item quantity="one">O registo mostra %1$d erro «Function not implemented»: a aceleração seccomp do proot está a fazer falhar um processo auxiliar neste dispositivo. Experimente %2$s → «%3$s» (ou «%4$s») e inicie novamente.</item> + <item quantity="many">O registo mostra %1$d erros «Function not implemented»: a aceleração seccomp do proot está a fazer falhar um processo auxiliar neste dispositivo. Experimente %2$s → «%3$s» (ou «%4$s») e inicie novamente.</item> + <item quantity="other">O registo mostra %1$d erros «Function not implemented»: a aceleração seccomp do proot está a fazer falhar um processo auxiliar neste dispositivo. Experimente %2$s → «%3$s» (ou «%4$s») e inicie novamente.</item> + </plurals> + + <string name="mode_decky_plugins">Plugins</string> + <string name="mode_decky_plugins_hint">Instale um ZIP de plugin para a próxima sessão do Steam.</string> + <string name="mode_decky_plugin_downloading">A transferir…</string> + <string name="mode_decky_plugin_install_zip">Instalar a partir de ZIP</string> + <string name="store_cat_games">Jogos</string> + <string name="store_cat_audio_video">Áudio e vídeo</string> + <string name="store_cat_graphics">Gráficos</string> + <string name="store_cat_internet">Internet</string> + <string name="store_cat_office">Escritório</string> + <string name="store_cat_developer">Programação</string> + <string name="store_cat_education">Educação</string> + <string name="store_cat_science">Ciência</string> + <string name="store_cat_system">Sistema</string> + <string name="store_cat_utilities">Utilitários</string> + <string name="store_section_popular">Populares</string> + <string name="store_section_trending">Tendências</string> + <string name="store_section_new">Novidades</string> + <string name="store_section_updated">Atualizadas</string> + <string name="store_section_loading">a carregar…</string> + <string name="store_flatpak_setup">Configuração do Flatpak</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Não foi possível procurar atualizações no Flathub</string> + <string name="content_rom_installed">instalado</string> + <string name="content_rom_folder">pasta</string> + <string name="theme_graphite">Grafite</string> + <string name="theme_graphite_detail">Superfícies em grafite, com o azul da esfera do logótipo para tudo o que está selecionado, focado ou premido.</string> + <string name="theme_paper">Papel sobre preto</string> + <string name="theme_paper_detail">O branco é a cor principal; o azul do ícone é o sinal para linhas, anéis e pontos.</string> + <string name="theme_icon_blue">Azul do ícone</string> + <string name="theme_icon_blue_detail">Fundo preto, texto branco e o azul para tudo o que está selecionado, focado ou premido.</string> + <string name="theme_electric">Azul-marinho elétrico</string> + <string name="theme_electric_detail">O azul mais vivo, em superfícies que tendem para o azul-marinho em vez do cinzento.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Retrocesso</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Espaço</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <string name="game_sync_err_runtime">Instale primeiro o runtime</string> + <string name="game_sync_err_steam_library">A biblioteca do Steam não está disponível</string> + <string name="game_sync_err_previous_sd">A biblioteca anterior do cartão SD não está disponível</string> + <string name="game_sync_err_game_storage">O armazenamento de jogos não está disponível</string> + <string name="game_sync_err_added_games">A pasta de jogos adicionados não está disponível</string> + <string name="game_sync_err_manifests">Os manifestos do Steam não estão disponíveis</string> + <string name="content_system_many">Vários sistemas</string> + <string name="fm_drive_internal">Interno</string> + <string name="fm_drive_linux">Sistema Linux</string> + <string name="fm_drive_sd">Cartão SD</string> + <string name="fm_drive_storage">Armazenamento</string> + <string name="fm_select">Selecionar</string> + <string name="fm_properties">Propriedades</string> + <string name="fm_favorite_add">Adicionar aos favoritos</string> + <string name="fm_favorite_remove">Remover dos favoritos</string> + <string name="fm_rename">Mudar o nome</string> + <string name="fm_copy">Copiar</string> + <string name="fm_cut">Cortar</string> + <string name="fm_actions">Ações</string> + <string name="fm_favorites">Favoritos</string> + <string name="fm_pin_current">Fixar pasta atual</string> + <string name="fm_no_favorites">Ainda não há favoritos - fixe uma pasta através do respetivo menu ⋮ para voltar aqui rapidamente.</string> + <string name="fm_unpin_favorite">Remover dos favoritos</string> + <string name="fm_size_zero">0 bytes</string> + <string name="fm_unit_bytes">bytes</string> + <string name="fm_prop_location">Localização</string> + <string name="fm_prop_type">Tipo</string> + <string name="fm_prop_size">Tamanho</string> + <string name="fm_prop_modified">Modificado</string> + <string name="fm_type_folder">Pasta</string> + <string name="fm_type_file">Ficheiro</string> + <string name="fm_type_ext_file">Ficheiro %1$s</string> + <string name="fm_read_only">Só de leitura</string> + <string name="fm_read_only_hint">Impede os jogos (e o Wine) de substituir ou eliminar este ficheiro.</string> + <string name="fm_read_only_failed">Não foi possível alterar «Só de leitura»</string> + <string name="fm_hidden">Oculto</string> + <string name="fm_hidden_unsupported">Este armazenamento não suporta o atributo de oculto.</string> + <string name="fm_hidden_hint">Marca o ficheiro como oculto no Windows (atributo oculto do Wine).</string> + <string name="fm_hidden_failed">Não foi possível alterar «Oculto» neste armazenamento</string> + <string name="fm_done">Concluído</string> + <string name="fm_op_deleting">A eliminar...</string> + <string name="fm_delete_failed">Falha ao eliminar</string> + <string name="fm_op_moving">A mover...</string> + <string name="fm_op_copying">A copiar...</string> + <string name="fm_op_moving_item">A mover - %1$s</string> + <string name="fm_op_copying_item">A copiar - %1$s</string> + <string name="fm_op_moving_batch">A mover %1$d/%2$d - %3$s</string> + <string name="fm_op_copying_batch">A copiar %1$d/%2$d - %3$s</string> + <string name="fm_paste_failed">Concluídos: %1$d, com falha: %2$d</string> + <string name="fm_paste_skipped">Concluídos: %1$d, ignorados: %2$d</string> + <plurals name="fm_paste_moved"> + <item quantity="one">%1$d item movido</item> + <item quantity="many">%1$d itens movidos</item> + <item quantity="other">%1$d itens movidos</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">%1$d item copiado</item> + <item quantity="many">%1$d itens copiados</item> + <item quantity="other">%1$d itens copiados</item> + </plurals> + <string name="fm_already_exists">«%1$s» já existe</string> + <string name="fm_op_renaming">A mudar o nome...</string> + <string name="fm_rename_failed">Falha ao mudar o nome</string> + <string name="fm_op_creating_folder">A criar pasta...</string> + <string name="fm_create_folder_failed">Não foi possível criar a pasta</string> + <string name="fm_new_folder">Nova pasta</string> + <string name="fm_folder_name">Nome da pasta</string> + <string name="fm_create">Criar</string> + <string name="fm_new_name">Novo nome</string> + <string name="fm_delete_title">Eliminar?</string> + <string name="fm_delete_text">Eliminar «%1$s» permanentemente?</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">Eliminar %1$d item?</item> + <item quantity="many">Eliminar %1$d itens?</item> + <item quantity="other">Eliminar %1$d itens?</item> + </plurals> + <string name="fm_cannot_undo">Esta ação não pode ser anulada.</string> + <plurals name="fm_and_more"> + <item quantity="one">…e mais %1$d</item> + <item quantity="many">…e mais %1$d</item> + <item quantity="other">…e mais %1$d</item> + </plurals> + <string name="fm_op_deleting_batch">A eliminar %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">Não foi possível eliminar %1$d item</item> + <item quantity="many">Não foi possível eliminar %1$d itens</item> + <item quantity="other">Não foi possível eliminar %1$d itens</item> + </plurals> + <string name="fm_conflict_merge_hint">«Combinar» adiciona e substitui ficheiros dentro da pasta existente.</string> + <string name="fm_conflict_overwrite_hint">«Substituir» troca o ficheiro existente pelo novo.</string> + <string name="fm_conflict_apply_all">Aplicar a todos os conflitos</string> + <string name="fm_conflict_merge">Combinar</string> + <string name="fm_conflict_overwrite">Substituir</string> + <string name="fm_conflict_keep_both">Manter ambos</string> + <string name="fm_conflict_skip">Ignorar</string> + <string name="fm_select_this_folder">Selecionar esta pasta</string> + <string name="fm_back">Voltar</string> + <string name="fm_drive_not_readable">%1$s está montado, mas não é possível lê-lo de momento</string> + <string name="fm_list_view">Vista de lista</string> + <string name="fm_grid_view">Vista de grelha</string> + <string name="fm_search">Pesquisar</string> + <string name="fm_sort">Ordenar</string> + <string name="fm_sort_name">Nome</string> + <string name="fm_sort_date">Data de modificação</string> + <string name="fm_rows_comfortable">Linhas espaçosas</string> + <string name="fm_rows_compact">Linhas compactas</string> + <string name="fm_hide_hidden">Ocultar ficheiros ocultos</string> + <string name="fm_show_hidden">Mostrar ficheiros ocultos</string> + <string name="fm_hide_favorites">Ocultar favoritos</string> + <string name="fm_show_favorites">Mostrar favoritos</string> + <string name="fm_filter_folder">Filtrar esta pasta</string> + <string name="fm_free_space">%1$s livres</string> + <string name="fm_selected_count">Selecionados: %1$d</string> + <string name="fm_select_none">Nenhum</string> + <string name="fm_select_all">Todos</string> + <string name="fm_paste">Colar</string> + <plurals name="fm_items"> + <item quantity="one">%1$d item</item> + <item quantity="many">%1$d itens</item> + <item quantity="other">%1$d itens</item> + </plurals> + <string name="fm_paste_here">Colar %1$s aqui</string> + <string name="fm_paste_move_here">Colar %1$s aqui (mover)</string> + <string name="fm_rail_title">Ficheiros</string> + <string name="fm_rail_storage">ARMAZENAMENTO</string> + <string name="fm_rail_quick">ACESSO RÁPIDO</string> + <string name="fm_rail_favorites">FAVORITOS</string> + <string name="fm_rail_downloads">Transferências</string> + <string name="fm_rail_downloads_short">Transf.</string> + <string name="fm_rail_roms">ROMs</string> + <string name="fm_rail_session_logs">Registos de sessão</string> + <string name="fm_rail_pictures">Imagens</string> + <string name="fm_favorite_added">«%1$s» adicionado aos favoritos</string> + <string name="fm_favorite_removed">«%1$s» removido dos favoritos</string> + <string name="fm_empty_directory">Pasta vazia</string> + <string name="fm_up_one_level">Subir um nível</string> + <string name="fm_choose_folder">Escolha uma pasta</string> + <string name="fm_choose_file">Escolha um ficheiro</string> + <string name="ctrl_tint_steam_blue">Azul Steam</string> + <string name="ctrl_tint_sky">Céu</string> + <string name="ctrl_tint_white">Branco</string> + <string name="ctrl_tint_violet">Violeta</string> + <string name="ctrl_tint_green">Verde</string> + <string name="ctrl_tint_amber">Âmbar</string> + <string name="ctrl_tint_red">Vermelho</string> + <string name="ctrl_tint_pink">Rosa</string> + <string name="ctrl_button_a">Botão A</string> + <string name="ctrl_button_b">Botão B</string> + <string name="ctrl_button_x">Botão X</string> + <string name="ctrl_button_y">Botão Y</string> + <string name="ctrl_button_lb">Botão de ombro esquerdo</string> + <string name="ctrl_button_rb">Botão de ombro direito</string> + <string name="ctrl_button_lt">Gatilho esquerdo</string> + <string name="ctrl_button_rt">Gatilho direito</string> + <string name="ctrl_button_view">Botão Ver</string> + <string name="ctrl_button_menu">Botão Menu</string> + <string name="ctrl_target_view">Ver</string> + <string name="ctrl_target_menu">Menu</string> + <string name="ctrl_target_up">Direcional para cima</string> + <string name="ctrl_target_down">Direcional para baixo</string> + <string name="ctrl_target_left">Direcional para a esquerda</string> + <string name="ctrl_target_right">Direcional para a direita</string> + <string name="ctrl_target_hidden">Oculto</string> + <string name="ctrl_editor_drag">Arraste para mover</string> + <string name="ctrl_editor_snap">Encaixar</string> + <string name="ctrl_editor_save">Guardar</string> + <string name="second_mode_none">Nenhum</string> + <string name="second_mode_keyboard_trackpad">Teclado + trackpad</string> + <string name="second_mode_terminal">Terminal</string> + <string name="second_mode_deck_controls">Pegas + trackpads do Deck</string> + + <string name="rtinst_removing">A remover o runtime Linux</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">A remover o runtime Linux · %1$d entrada</item> + <item quantity="many">A remover o runtime Linux · %1$d entradas</item> + <item quantity="other">A remover o runtime Linux · %1$d entradas</item> + </plurals> + <string name="rtinst_removed">Runtime Linux removido</string> + <string name="rtinst_removal_failed">Não foi possível concluir a remoção do runtime Linux. Use «Tentar remover novamente» em Configuração.</string> + <string name="rtinst_removing_leftovers">A remover restos do runtime</string> + <string name="rtinst_downloading">A transferir</string> + <string name="rtinst_verifying">A verificar</string> + <string name="rtinst_extracting">A extrair</string> + <string name="rtinst_channel_description">Aparece enquanto o runtime Linux é instalado ou removido</string> + <string name="rtinst_title_installing">A instalar o runtime Linux</string> + <string name="rtinst_title_removing">A remover o runtime Linux</string> + + <string name="deskpkg_runtime_missing">O runtime Linux não está instalado</string> + <string name="deskpkg_no_checksum">O catálogo não tem soma de verificação para %1$s</string> + <string name="deskpkg_checksum_mismatch">A soma de verificação não corresponde - nada foi alterado</string> + <string name="deskpkg_installing">A instalar %1$s</string> + <string name="deskpkg_place_failed">Não foi possível colocar a AppImage</string> + <string name="deskpkg_extract_failed">Falha na extração</string> + <string name="deskpkg_install_failed">Falha na instalação</string> + + <string name="flatpakmgr_setup_failed">Falha na configuração do Flatpak (saída %1$d)</string> + <string name="flatpakmgr_setup_unfinished">A configuração do Flatpak não terminou</string> + <string name="flatpakmgr_busy">Está em curso outra tarefa da loja</string> + <string name="flatpakmgr_not_ready">Configure primeiro o Flatpak</string> + <string name="flatpakmgr_starting">A iniciar</string> + <string name="flatpakmgr_exit_status">O Flatpak terminou com o código %1$d</string> + <string name="flatpakmgr_installing">A instalar %1$s</string> + <string name="flatpakmgr_updating">A atualizar %1$s</string> + <string name="flatpakmgr_removing">A remover %1$s</string> + <string name="flatpakmgr_installing_step">A instalar %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_updating_step">A atualizar %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_removing_step">A remover %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_ref_translations">traduções</string> + <string name="flatpakmgr_ref_graphics_drivers">controladores gráficos</string> + <string name="flatpakmgr_ref_media_codecs">codecs multimédia</string> + <string name="flatpakmgr_ref_runtime">runtime %1$s</string> + <string name="flatpakmgr_ref_runtime_branch">runtime %1$s %2$s</string> + + <string name="appimg_too_small">O ficheiro é demasiado pequeno para ser uma AppImage</string> + <string name="appimg_unreadable">Não é possível ler o ficheiro: %1$s</string> + <string name="appimg_not_linux">Isto não é uma AppImage (não é um programa Linux)</string> + <string name="appimg_wrong_arch">Esta AppImage não foi compilada para ARM64 (aarch64) nem para PCs x86</string> + <string name="appimg_not_appimage">Isto é um programa Linux, mas não é uma AppImage</string> + <string name="appimg_wrong_type">Só é possível importar AppImages do tipo 2 (esta é do tipo %1$d)</string> + <string name="appimg_no_space_import">Espaço livre insuficiente: a importação precisa de cerca de %1$s</string> + <string name="appimg_no_space_update">Espaço livre insuficiente: a atualização precisa de cerca de %1$s</string> + <string name="appimg_import_failed">Falha na importação</string> + <string name="appimg_gone">Essa AppImage já não existe</string> + <string name="appimg_update_failed">Falha na atualização</string> + <string name="appimg_extracting">A extrair %1$s</string> + <string name="appimg_extract_failed">Não foi possível extrair a AppImage</string> + <string name="appimg_extract_failed_detail">Não foi possível extrair a AppImage: %1$s</string> + + <string name="deckymgr_stop_session_loader">Pare a sessão ativa antes de instalar o Decky Loader</string> + <string name="deckymgr_download_failed">Falha na transferência do Decky Loader</string> + <string name="deckymgr_size_mismatch">O tamanho do Decky Loader não corresponde aos metadados da versão</string> + <string name="deckymgr_checksum_download_failed">Não foi possível transferir a soma de verificação da versão</string> + <string name="deckymgr_checksum_missing">Falta a soma de verificação da versão</string> + <string name="deckymgr_checksum_invalid">A soma de verificação da versão é inválida</string> + <string name="deckymgr_checksum_mismatch">A soma de verificação do Decky Loader não corresponde</string> + <string name="deckymgr_not_elf">O ficheiro da versão não é um executável Linux de 64 bits compatível</string> + <string name="deckymgr_session_started_loader">Foi iniciada uma sessão durante a transferência; pare-a antes de instalar o Decky Loader</string> + <string name="deckymgr_loader_not_executable">Não foi possível tornar o PluginLoader executável</string> + <string name="deckymgr_cef_failed">Não foi possível ativar a depuração remota do CEF do Steam</string> + <string name="deckymgr_stage_failed">Não foi possível preparar o PluginLoader</string> + <string name="deckymgr_loader_preserve_failed">Não foi possível preservar o PluginLoader existente</string> + <string name="deckymgr_loader_activate_failed">Não foi possível ativar o novo PluginLoader</string> + <string name="deckymgr_stop_session_plugin">Pare a sessão ativa antes de instalar um plugin</string> + <string name="deckymgr_loader_required">Instale o Decky Loader antes de importar um plugin</string> + <string name="deckymgr_zip_unreadable">Não foi possível ler o ficheiro ZIP selecionado</string> + <string name="deckymgr_plugins_dir_failed">Não foi possível criar a pasta de plugins do Decky</string> + <string name="deckymgr_extracting_zip">A extrair o ZIP do plugin</string> + <string name="deckymgr_zip_one_folder">O ZIP do plugin tem de conter uma única pasta de plugin no nível superior</string> + <string name="deckymgr_zip_missing_files">Faltam plugin.json, package.json ou dist/index.js no ZIP do plugin</string> + <string name="deckymgr_plugin_name_missing">Falta o nome do plugin</string> + <string name="deckymgr_session_started_plugin">Foi iniciada uma sessão durante a instalação; pare-a antes de instalar um plugin</string> + <string name="deckymgr_plugin_installed">Plugin instalado · reinicie o Steam para o carregar</string> + <string name="deckymgr_plugin_install_failed">Falha na instalação do ZIP do plugin</string> + <string name="deckymgr_zip_too_many">O ZIP do plugin contém demasiadas entradas</string> + <string name="deckymgr_zip_invalid_path">O ZIP do plugin contém um caminho inválido</string> + <string name="deckymgr_zip_duplicate_paths">O ZIP do plugin contém caminhos duplicados</string> + <string name="deckymgr_zip_unsafe_path">O ZIP do plugin contém um caminho inseguro</string> + <string name="deckymgr_folder_failed">Não foi possível criar a pasta do plugin</string> + <string name="deckymgr_zip_too_large">O ZIP do plugin excede o limite de 1 GiB depois de extraído</string> + <string name="deckymgr_bundled_not_executable">Não foi possível tornar executável o binário incluído no plugin</string> + <string name="deckymgr_remote_entry_invalid">Entrada remote_binary inválida</string> + <string name="deckymgr_remote_name_invalid">Nome de binário remoto inválido</string> + <string name="deckymgr_remote_url_https">O URL do binário remoto tem de usar HTTPS</string> + <string name="deckymgr_remote_hash_invalid">O binário remoto %1$s tem um hash SHA-256 inválido</string> + <string name="deckymgr_bin_dir_failed">Não foi possível criar a pasta bin do plugin</string> + <string name="deckymgr_downloading_binary">A transferir o binário do plugin · %1$s</string> + <string name="deckymgr_binary_download_failed">Não foi possível transferir o binário do plugin %1$s</string> + <string name="deckymgr_binary_checksum_mismatch">A soma de verificação do binário do plugin não corresponde · %1$s</string> + <string name="deckymgr_binary_not_executable">Não foi possível tornar executável o binário do plugin · %1$s</string> + <string name="deckymgr_binary_replace_failed">Não foi possível substituir o binário do plugin · %1$s</string> + <string name="deckymgr_binary_install_failed">Não foi possível instalar o binário do plugin · %1$s</string> + <string name="deckymgr_folder_taken">A pasta do plugin pertence a outro plugin</string> + <string name="deckymgr_preserve_failed">Não foi possível preservar o plugin existente</string> + <string name="deckymgr_activate_failed">Não foi possível ativar o plugin importado</string> + <string name="deckymgr_settings_failed">Não foi possível atualizar as definições de plugins do Decky</string> + <string name="deckymgr_restore_failed">Não foi possível restaurar o plugin existente</string> + + <string name="adbfix_save_failed">Não foi possível guardar a identidade da depuração sem fios</string> + <string name="adbfix_command_failed">O comando ADB falhou (%1$d)</string> + <string name="adbfix_not_confirmed">O Android não confirmou a alteração do limite de processos secundários: %1$s</string> + <string name="adbfix_no_pairing">Não há nenhuma sincronização de depuração sem fios guardada</string> + <string name="adbfix_enter_address">Introduza o endereço IP e a porta atuais da depuração sem fios</string> + + <string name="adbpair_timed_out">Tempo esgotado à espera da janela de sincronização. Tente novamente a partir do DroidDeck.</string> + <string name="adbpair_discovery_failed_code">Não foi possível procurar a janela de sincronização (%1$d). Verifique se o Wi-Fi está ativado.</string> + <string name="adbpair_discovery_failed">Não foi possível procurar a janela de sincronização: %1$s</string> + <string name="adbpair_six_digits">Introduza os 6 dígitos do código</string> + <string name="adbpair_pairing">A sincronizar…</string> + <string name="adbpair_code_rejected">Esse código não funcionou. Abra novamente a janela de sincronização e introduza o novo código.</string> + <string name="adbpair_connecting">Sincronizado. A ligar…</string> + <string name="adbpair_port_not_found">Sincronizado, mas a porta da depuração sem fios não foi encontrada. Mantenha a depuração sem fios ativada e tente novamente.</string> + <string name="adbpair_applying">A aplicar a definição…</string> + <string name="adbpair_command_failed">Falha no comando de depuração sem fios</string> + <string name="adbpair_channel_name">Configuração da depuração sem fios</string> + <string name="adbpair_channel_description">Pede o código de sincronização da depuração sem fios enquanto as Definições estão abertas</string> + <string name="adbpair_title_pair">Sincronizar a depuração sem fios</string> + <string name="adbpair_waiting_text">Ative a depuração sem fios e toque em «Sincronizar dispositivo com código de sincronização».</string> + <string name="adbpair_waiting_long">Ative a depuração sem fios e toque em «Sincronizar dispositivo com código de sincronização». Fique nas Definições: o código é introduzido nesta notificação.</string> + <string name="adbpair_code_label">Código de sincronização de 6 dígitos</string> + <string name="adbpair_code_hint">Toque em Introduzir código e escreva os 6 dígitos mostrados nas Definições.</string> + <string name="adbpair_title_code">Introduza o código de sincronização Wi-Fi</string> + <string name="adbpair_title_retry">Introduza o código novamente</string> + <string name="adbpair_enter_code">Introduzir código</string> + <string name="adbpair_title_working">A configurar o Steam</string> + <string name="adbpair_title_done">O Steam está pronto</string> + <string name="adbpair_done_text">Toque para voltar ao DroidDeck. Já pode desativar a depuração sem fios.</string> + <string name="adbpair_title_failed">A configuração da depuração sem fios parou</string> + + <string name="selfinst_http">A transferência respondeu HTTP %1$d</string> + <string name="selfinst_size_mismatch">O tamanho da transferência não correspondia ao APK publicado e foi descartada - tente novamente</string> + <string name="selfinst_checksum_mismatch">A transferência não correspondia à soma de verificação e foi descartada - tente novamente</string> + <string name="selfinst_wrong_package">A atualização é para o pacote Android %1$s, não para %2$s</string> + <string name="selfinst_metadata_signer">Os metadados da atualização não indicam a chave de assinatura de lançamento do DroidDeck</string> + <string name="selfinst_unreadable">O Android não conseguiu ler o APK transferido</string> + <string name="selfinst_apk_wrong_package">O APK transferido é para %1$s, não para %2$s</string> + <string name="selfinst_version_mismatch">O APK transferido indica o versionCode %1$d, não o %2$d publicado</string> + <string name="selfinst_downgrade">O Android não instala o versionCode %1$d por cima do %2$d instalado</string> + <string name="selfinst_no_signing">O APK transferido não tem informações de assinatura</string> + <string name="selfinst_wrong_signer">O APK transferido não está assinado com a chave de lançamento do DroidDeck</string> + <string name="appupd_http">O catálogo de atualizações respondeu HTTP %1$d</string> + + <string name="loadstate_starting">A iniciar a sessão…</string> + <string name="loadstate_elapsed">%1$d:%2$02d decorridos · ainda a trabalhar</string> + <string name="loadstate_steam_update">A transferir a atualização do cliente Steam · %1$d%%</string> + <string name="loadstate_steam_client">A transferir o cliente Steam · %1$s</string> + + <string name="pextra_busy">Já está em curso a instalação de uma ferramenta de compatibilidade</string> + <string name="pextra_stop_session">Pare a sessão ativa antes de instalar ferramentas de compatibilidade</string> + <string name="pextra_no_release">Não foi possível encontrar uma versão ARM64 de %1$s</string> + <string name="pextra_no_space">São necessários pelo menos 4 GB livres, mais o tamanho da transferência, para instalar %1$s</string> + <string name="pextra_downloading">A transferir %1$s %2$s</string> + <string name="pextra_retrying">A repetir a transferência · tentativa %1$d de %2$d</string> + <string name="pextra_interrupted">Transferência interrompida</string> + <string name="pextra_download_failed">A transferência falhou; o ficheiro parcial é mantido para que uma nova tentativa a possa retomar</string> + <string name="pextra_too_big">A transferência excedeu o tamanho da versão e foi eliminada</string> + <string name="pextra_incomplete">A transferência ficou incompleta; o ficheiro parcial é mantido para ser retomado numa nova tentativa</string> + <string name="pextra_verifying">A verificar a transferência</string> + <string name="pextra_checksum_unreadable">Não foi possível ler a soma de verificação da versão; tente novamente</string> + <string name="pextra_no_checksum">%1$s %2$s não publica soma de verificação; nada foi instalado</string> + <string name="pextra_checksum_mismatch">A soma de verificação não corresponde; a transferência foi eliminada</string> + <string name="pextra_session_started">Foi iniciada uma sessão durante a transferência; pare-a antes de instalar ferramentas de compatibilidade</string> + <string name="pextra_installing">A instalar %1$s</string> + <string name="pextra_install_failed">Falha na instalação de %1$s (saída %2$d)</string> + <string name="pextra_could_not_install">Não foi possível instalar %1$s</string> + <string name="esync_fetching">A obter o pacote droiddeck-esync</string> + + <string name="cmgr_fex_linux_helpers">com auxiliares Linux</string> + <string name="cmgr_fex_dlls_only">Só DLLs</string> + <string name="cmgr_original_proton_updated">Original (Proton atualizado desde a última troca)</string> + <string name="cmgr_changed_outside">Alterado fora de Componentes</string> + <string name="cmgr_not_present">não presente</string> + <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 será colocado em %3$s quando o jogo que o está a usar fechar.</string> + <string name="cmgr_swap_done">%1$s %2$s está agora em %3$s. Aplica-se da próxima vez que um jogo iniciar.</string> + <string name="cmgr_original_missing">Esse pacote original não está guardado</string> + <string name="cmgr_restore_queued">%1$s de %2$s volta ao original quando o jogo que o está a usar fechar.</string> + <string name="cmgr_restore_done">%1$s de %2$s restaurado a partir do original de %3$s.</string> + <string name="cmgr_package_in_use">%1$s está em uso em %2$s. Troque-o primeiro por outro nesse Proton.</string> + <string name="cmgr_package_not_stored">%1$s não está guardado</string> + <string name="cmgr_deleted">%1$s eliminado.</string> + <string name="cmgr_original_kept">O original da versão instalada é mantido: é o caminho de volta.</string> + <string name="cmgr_not_stored">Não guardado</string> + <string name="cmgr_deleted_original">Original de %1$s de %2$s eliminado.</string> + <string name="cmgr_nightlies_only">As transferências vêm apenas das versões Nightlies</string> + <string name="cmgr_list_predates_checksums">Esta lista de pacotes é anterior às somas de verificação - atualize-a e tente novamente</string> + <string name="cmgr_checksum_mismatch">A soma de verificação não corresponde</string> + + <string name="sprefs_upscaler_linear">Linear</string> + <string name="sprefs_upscaler_nearest">Mais próximo</string> + <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (qualidade)</string> + <string name="sprefs_upscaler_sharpen">Só nitidez</string> + <string name="sprefs_lod_bias_auto">Automático (acompanha a escala)</string> + + <string name="look_off_desc">Nada aplicado.</string> + <string name="look_game_clarity">Clareza de jogo</string> + <string name="look_game_clarity_desc">Nitidez com um toque de contraste - a do dia a dia.</string> + <string name="look_vivid">Vívido</string> + <string name="look_vivid_desc">Cor bem carregada. Debanding ativo para o céu não ficar às riscas.</string> + <string name="look_cinematic">Cinematográfico</string> + <string name="look_cinematic_desc">Contraste mais profundo, cor mais fria, contornos suavizados.</string> + <string name="look_competitive">Competitivo</string> + <string name="look_competitive_desc">Sombras realçadas e nitidez forte - aviste os adversários primeiro.</string> + <string name="look_adaptive_sharpen">Nitidez adaptativa</string> + <string name="look_adaptive_sharpen_desc">Só nitidez, cor intacta.</string> + <string name="look_filmic">Fílmico</string> + <string name="look_filmic_desc">Curva tipo película, ligeiramente esbatida.</string> + <string name="look_arcade">Arcade</string> + <string name="look_arcade_desc">Forte e com impacto.</string> + <string name="look_retro_crt">CRT retro</string> + <string name="look_retro_crt_desc">As passagens de tubo CRT e de sinal analógico, em conjunto.</string> + <string name="look_upscale_sharp">Ampliação nítida</string> + <string name="look_upscale_sharp_desc">Para jogar abaixo da resolução do ecrã.</string> + <string name="look_pixel_clean">Píxeis limpos</string> + <string name="look_pixel_clean_desc">Píxeis nítidos para 2D e jogos antigos.</string> + <string name="look_anime_edge">Contornos anime</string> + <string name="look_anime_edge_desc">Contornos de desenho animado mais nitidez.</string> + + <string name="gpuinfo_not_adreno">Não é uma GPU Adreno</string> + <string name="gpuinfo_supported">Suportada</string> + <string name="gpuinfo_experimental">Experimental: os controladores são versões de teste</string> + <string name="gpuinfo_below_tested">Abaixo do hardware testado (Adreno 725 e mais recentes): pode não funcionar</string> + <string name="gpuinfo_unsupported">Não suportada: o DroidDeck precisa de uma GPU Adreno (Snapdragon)</string> + + <string name="turnip_rate_limited">Foi atingido o limite de pedidos do GitHub - tente novamente mais tarde</string> + <string name="turnip_github_http">O GitHub respondeu HTTP %1$d</string> + <string name="turnip_download_http">a transferência respondeu HTTP %1$d</string> + <string name="turnip_list_predates_checksums">Esta lista de controladores é anterior às somas de verificação - atualize-a e tente novamente</string> + <string name="turnip_checksum_mismatch">A soma de verificação não corresponde - a transferência foi descartada</string> + <string name="turnip_not_adrenotools">Não é um controlador AdrenoTools: %1$s</string> + <string name="turnip_reject_no_meta">sem meta.json</string> + <string name="turnip_reject_meta_unreadable">não é possível ler meta.json (%1$s)</string> + <string name="turnip_reject_linux_driver">este é um controlador do runtime Linux (importe-o em «Controlador do runtime Linux»)</string> + <string name="turnip_reject_wayland">este é um controlador de jogos Wayland, que esta app não utiliza</string> + <string name="turnip_reject_no_library_name">meta.json não indica libraryName, por isso nada pôde ser passado ao AdrenoTools</string> + <string name="turnip_reject_library_missing">meta.json indica %1$s, que não está no zip</string> + <string name="turnip_reject_not_aarch64">%1$s não é uma biblioteca partilhada ELF AArch64 de 64 bits</string> + <string name="turnip_reject_glibc">%1$s liga à glibc - é um Turnip «-Linux», que pertence a «Controlador do runtime Linux»</string> + <string name="lvd_no_library">Não há libvulkan_freedreno*.so neste zip. Um zip Turnip Android (AdrenoTools) ou -Wayland não é um controlador do runtime Linux.</string> + <string name="lvd_not_aarch64">%1$s não é uma biblioteca partilhada ELF AArch64 de 64 bits.</string> + <string name="lvd_not_glibc">%1$s não é um controlador glibc - liga à libc do Android. O runtime Linux precisa de um zip «-Linux»; um Turnip simples ou «-Wayland» não pode ser carregado pelo cliente Steam.</string> + <string name="bundle_no_manifest">Não é um pacote de controladores: não há manifest.json que indique um controlador Android e um Linux</string> + <string name="bundle_entry_outside">Não é um pacote de controladores: %1$s aponta para fora dele</string> + <string name="bundle_too_large">Não é um pacote de controladores: descompactado ocupa mais de %1$d MB</string> + + <string name="gstore_sd_card">Cartão SD</string> + <string name="gstore_folder">Pasta</string> + + <string name="logshare_chooser">Partilhar registos da sessão</string> + + <string name="hdr_needs_android_10">requer Android 10 ou mais recente (camadas de ecrã com espaço de cor)</string> + <string name="hdr_none">este ecrã não indica suporte HDR</string> + <string name="hdr_not_hdr10">este ecrã suporta %1$s, não HDR10</string> + + <string name="gpu_this_gpu">esta GPU</string> + <string name="phantom_title_on">O limite de processos secundários está ativado</string> + <string name="phantom_title_unset">O limite de processos secundários não está definido</string> + <string name="phantom_title_unreadable">Não foi possível verificar o limite de processos secundários</string> + <string name="phantom_title_off">O limite de processos secundários está desativado</string> + <string name="phantom_title_not_required">Limite de processos secundários não necessário</string> + <string name="phantom_instructions_fix">%1$s Se não for possível, ligue este dispositivo a um computador com ADB e execute o comando abaixo. Volte aqui e verifique novamente.</string> + <string name="phantom_instructions_off">As sessões Steam podem ser iniciadas.</string> + <string name="phantom_instructions_not_applicable">Esta versão do Android não usa este limite.</string> + <string name="phantom_fix_toggle">Ative «Desativar restrições de processos secundários» nas Opções de programador e mantenha as Opções de programador ativadas depois.</string> + <string name="phantom_fix_no_toggle">Esta versão do Android não tem um interruptor para isto. Use a depuração sem fios para o alterar a partir deste dispositivo.</string> + <string name="upd_block_no_apk">Esta versão não tem transferência para esta cópia do DroidDeck.</string> + <string name="upd_block_version_code">O Android não a instala por cima desta versão porque o versionCode %1$d é inferior ao %2$d instalado.</string> + <string name="upd_fail_downgrade">O Android não instala uma versão mais antiga por cima de uma mais recente. Mantenha esta versão até o canal a alcançar.</string> + <string name="upd_fail_signature">Esta cópia do DroidDeck tem uma assinatura diferente (compilação local?), por isso o Android não a atualiza no lugar.</string> + <string name="upd_fail_storage">Não há espaço livre suficiente para a atualização.</string> + <string name="upd_fail_aborted">A atualização foi cancelada.</string> + <string name="upd_fail_with_message">A atualização não foi instalada: %1$s</string> + <string name="upd_fail_generic">A atualização não foi instalada.</string> + <string name="saves_layout_zip">zip do %1$s</string> + <string name="saves_zip">zip</string> + <string name="saves_not_launched">%1$s ainda não foi iniciado</string> + <string name="saves_none_found">Nenhum ficheiro de dados guardados encontrado para %1$s</string> + <string name="saves_not_save_zip">%1$s não tem pasta drive_c, por isso não é um zip de dados guardados do GameHub nem do Winlator</string> + <string name="gstore_cannot_create">não é possível criar pastas em %1$s</string> + <string name="gstore_cannot_write">não é possível escrever em %1$s</string> + <string name="gstore_cannot_write_detail">não é possível escrever em %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">A pasta não está disponível</string> + <string name="gfiles_name_taken">Outro ficheiro já usa %1$s</string> + <string name="gfiles_art_unavailable">A pasta de imagens não está disponível</string> + <string name="gfiles_unrelated_inventory">A pasta tem um inventário de sincronização não relacionado</string> + <string name="gfiles_remove_failed">Não foi possível remover %1$s</string> + <string name="gfiles_outside_folder">O ficheiro sai da pasta de exportação</string> + <string name="cmgr_fex_unnamed">FEX (compilação sem nome de versão)</string> + <string name="cmgr_no_profile">sem profile.json em %1$s</string> + <string name="cmgr_not_component_package">%1$s não é um pacote de componente Linux (tipo %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s livres</string> + <string name="mode_storage_auto_short">Automático</string> + <string name="appupd_catalog_rejected">O catálogo de atualizações falhou as verificações (%1$s)</string> + <string name="main_adb_clip_label">Definição de processos secundários do DroidDeck</string> + <string name="setup_signed_in_offline_unnamed">Sessão iniciada · modo offline</string> + <string name="setup_signed_in_unnamed">Sessão iniciada</string> + <string name="driver_tag_bundled">INCLUÍDO</string> + <string name="driver_tag_downloaded">TRANSFERIDO</string> + <string name="driver_tag_imported">IMPORTADO</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Ecrã da sessão</string> + <string name="display_resolution">Resolução</string> + <string name="display_game_fit">Ajuste de jogos 16:9</string> + <string name="display_game_fit_hint">Como os jogos 16:9 se ajustam a este ecrã. Próxima sessão.</string> + <string name="display_game_fit_unavailable">Escolha «Igual ao ecrã» ou um tamanho personalizado mais estreito do que 16:9.</string> + <string name="display_preserve">Manter proporções</string> + <string name="display_stretch">Esticar até ao ecrã</string> + <string name="display_image_scaling">Escala da imagem</string> + <string name="display_filter">Filtro de escala</string> + <string name="display_filter_hint">Partilhado pelo Steam e pelo Ambiente de trabalho.</string> + <string name="display_filter_live">Aplica-se em tempo real. Partilhado pelo Steam e pelo Ambiente de trabalho.</string> + <string name="display_filter_note">FSR, GSR e NIS aplicam-se ao ampliar a imagem da sessão. «Só nitidez» também funciona sem ampliação.</string> + <string name="display_sharpness">Nitidez do filtro</string> + <string name="display_frame_rate">Taxa de fotogramas</string> + <string name="display_look_filter">%1$s Define a escala como %2$s.</string> + <string name="display_match_screen">Igual ao ecrã (%1$d×%2$d)</string> + <string name="display_follow_screen">Seguir o ecrã (redimensiona ao dobrar ou desdobrar)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Mostrar detalhes do dispositivo</string> + <string name="device_info_hide">Ocultar detalhes do dispositivo</string> + <string name="device_info_loading">A ler o dispositivo…</string> + <string name="device_info_copy">Copiar</string> + <string name="device_info_copy_hint">Copie estes detalhes para um relatório de erro.</string> + <string name="device_info_copied">Copiado.</string> + <string name="force_ssbs_title">Forçar SSBS</string> + <string name="force_ssbs_hint">Mantém ativa uma definição de velocidade da CPU que o Wine está sempre a desativar. Pode tornar os jogos Windows mais rápidos no Snapdragon 8 Elite; pouca diferença nos chips mais antigos. Aplica-se no próximo arranque do jogo.</string> + + <string name="rail_stores">Lojas</string> + <string name="source_steam">Steam</string> + <string name="source_added">Personalizados</string> + <string name="setup_stores">Lojas</string> + <string name="setup_stores_open_on">Abrir uma loja em</string> + <string name="setup_stores_speed">Nível de velocidade de transferência</string> + <string name="setup_stores_engine">Motor das lojas</string> + <string name="setup_stores_engine_chip_ready">● Motor pronto</string> + <string name="setup_stores_engine_chip_missing">Não compilado</string> + <string name="stores_tier_balanced">Equilibrado</string> + <string name="stores_tier_fast">Rápido</string> + <string name="stores_tier_max">Máximo</string> + <string name="stores_title">Lojas</string> + <string name="stores_settings">Definições das lojas</string> + <string name="stores_settings_eyebrow">Definições</string> + <string name="stores_chip_signed_out">(sessão não iniciada)</string> + <string name="stores_tab_store">Loja</string> + <string name="stores_tab_installed">Instalados</string> + <string name="stores_tab_library">Biblioteca</string> + <string name="stores_tab_all">Todos</string> + <string name="stores_tab_downloads">Transferências</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Iniciar sessão</string> + <string name="stores_signout_named">%1$s · terminar sessão</string> + <string name="stores_signout_title">Terminar sessão em %1$s?</string> + <string name="stores_signout_note">Os jogos instalados mantêm-se. A biblioteca volta a ser obtida quando iniciar sessão novamente.</string> + <string name="stores_signout_stay">Manter sessão iniciada</string> + <string name="stores_signout_go">Terminar sessão</string> + <string name="stores_search_catalog">Pesquisar no catálogo %1$s…</string> + <string name="stores_search_installed">Pesquisar jogos instalados…</string> + <string name="stores_search_library">Pesquisar na sua biblioteca…</string> + <string name="stores_search_open">Pesquisar</string> + <string name="stores_installed_empty">Nada instalado</string> + <string name="stores_library_loading">A carregar</string> + <string name="stores_nothing_matches">Nenhum jogo</string> + <string name="stores_shelves_loading">A carregar a loja…</string> + <string name="stores_amazon_no_catalog">Sem catálogo</string> + <string name="stores_shelf_new">Novidades</string> + <string name="stores_shelf_deals">Promoções</string> + <string name="stores_shelf_free">Jogos gratuitos</string> + <string name="stores_shelf_trending">Tendências</string> + <string name="stores_shelf_library">A sua biblioteca</string> + <string name="stores_play">Jogar</string> + <string name="stores_install">Instalar</string> + <string name="stores_resume_install">Retomar instalação</string> + <string name="stores_dl_interrupted">Interrompida</string> + <string name="stores_install_size">Instalar · %1$s</string> + <string name="stores_get_free">Obter grátis</string> + <string name="stores_view_on">Ver em %1$s</string> + <string name="stores_buy_price">Comprar · %1$s</string> + <string name="stores_installed_chip">Instalado</string> + <string name="stores_in_library">Na biblioteca</string> + <string name="stores_free">Grátis</string> + <string name="stores_resume">Retomar</string> + <string name="stores_refresh">Atualizar</string> + <string name="stores_show_mature">Mostrar conteúdo para adultos</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Offline</string> + <string name="epic_card_none">Desativado</string> + <string name="epic_eos">Início de sessão Epic (EOS)</string> + <string name="epic_offline">Iniciar offline</string> + <string name="epic_resolve">Resolver início de sessão Epic</string> + <string name="store_cloud_saves">Dados guardados na nuvem</string> + <string name="cloud_upload">Carregar para a nuvem</string> + <string name="cloud_download">Transferir da nuvem</string> + <string name="cloud_none">Sem dados guardados na nuvem</string> + <string name="cloud_conflict">Conflito na nuvem</string> + <string name="cloud_conflict_files">Conflito na nuvem · %1$d ficheiros</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Manter nuvem</string> + <string name="cloud_keep_local">Manter local</string> + <string name="cloud_checking">A verificar a nuvem…</string> + <string name="cloud_last_sync">Última sincronização: %1$s</string> + <string name="cloud_never">Ainda não sincronizado</string> + <string name="cloud_done">%1$d ficheiros</string> + <string name="stores_eyebrow_installed">instalado</string> + <string name="stores_eyebrow_library">na sua biblioteca</string> + <string name="stores_eyebrow_store">loja</string> + <string name="achievements_title">Conquistas</string> + <string name="achievements_count">%1$d de %2$d desbloqueadas</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Desbloqueadas · %1$d</string> + <string name="achievements_group_locked">Bloqueadas · %1$d</string> + <string name="achievements_group_hidden">Ocultas · %1$d</string> + <string name="achievements_hidden">Conquista oculta</string> + <string name="achievements_hidden_detail">Os detalhes são mostrados quando for desbloqueada.</string> + <string name="achievements_all">Todas as conquistas desbloqueadas</string> + <string name="achievements_reading">A carregar…</string> + <string name="achievements_signed_out">Inicia sessão no %1$s para ver as tuas conquistas.</string> + <string name="achievements_unreachable">Não foi possível aceder ao %1$s. Verifica novamente quando estiveres online.</string> + <string name="achievements_none">O %1$s não lista conquistas para este jogo.</string> + <string name="achievements_show_all">Mostrar todas as %1$d</string> + <string name="achievements_dialog_title">Conquistas · %1$s</string> + <string name="stores_in_steam">✓ No Steam</string> + <string name="stores_uninstall">Desinstalar</string> + <string name="stores_uninstalling">A desinstalar…</string> + <string name="stores_uninstall_title">Desinstalar %1$s?</string> + <string name="stores_uninstall_body_size">Liberta %1$s e remove-o do Steam.</string> + <string name="stores_uninstall_body">Remove os ficheiros e o atalho do Steam.</string> + <string name="stores_launch_not_registered">O Steam deteta o jogo no próximo arranque; inicie-o a partir daí ou inicie o Steam agora.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Escolha do Steam por jogo</string> + <string name="stores_card_audio">Áudio</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Componentes</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Motor</string> + <string name="stores_card_engine_value">%1$s · nativo</string> + <string name="stores_card_engine_java">%1$s · integrado</string> + <string name="stores_how_it_runs">Como funciona</string> + <string name="stores_kv_fetched">Obtido de</string> + <string name="stores_kv_fetched_value">%1$s, com a sua conta</string> + <string name="stores_kv_installed_to">Instalado em</string> + <string name="stores_kv_installed_to_value">Armazenamento de jogos (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Iniciado por</string> + <string name="stores_kv_launched_by_value">Steam e Proton, o mesmo caminho dos jogos Steam</string> + <string name="stores_kv_launched_by_epic">Steam e Proton · início de sessão Epic em cada arranque, sobreposição EOS desativada</string> + <string name="stores_kv_launched_by_amazon">Steam e Proton · variáveis do Amazon Games Services definidas</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">adicionado ao Steam na instalação (interface Deck + Steam Input)</string> + <string name="stores_downloads_empty">Sem transferências</string> + <string name="stores_dl_paused">Em pausa</string> + <string name="stores_dl_queued">Em fila</string> + <string name="stores_dl_queued_at">Em fila · #%1$d</string> + <string name="stores_dl_cancelled">Cancelada</string> + <string name="stores_dl_failed">Falhou</string> + <string name="stores_dl_failed_reason">Falhou · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Pausar</string> + <string name="stores_dl_resume">Retomar</string> + <string name="stores_dl_cancel">Cancelar</string> + <string name="stores_dl_retry">Tentar novamente</string> + <string name="stores_dl_clear">Limpar</string> + <string name="stores_dl_clear_all">Limpar concluídas</string> + <string name="stores_stage_manifest">Manifesto</string> + <string name="stores_stage_download">Transferência</string> + <string name="stores_stage_verify">Verificação</string> + <string name="stores_stage_install">Instalação</string> + <string name="stores_stage_done">Concluído</string> + <string name="stores_stage_checking">A verificar</string> + <string name="stores_stage_downloading">A transferir</string> + <string name="stores_dl_cancel_confirm">Eliminar transferência?</string> + <string name="stores_dl_failed_kept">Falhou · %1$s mantidos</string> + <string name="stores_stage_verifying">A verificar</string> + <string name="stores_stage_installing">A instalar</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d ficheiros</string> + <string name="stores_download_manager">Gestor de transferências</string> + <string name="stores_parallel">Transferências em simultâneo</string> + <string name="stores_install_where">Instalar em</string> + <string name="stores_install_title">Instalar %1$s:</string> + <string name="stores_target_internal">Interno</string> + <string name="stores_target_free">%1$s livres</string> + <string name="stores_notification_channel">Transferências da loja</string> + <string name="stores_notification_channel_hint">Mostra o progresso das transferências de jogos e mantém as transferências ativas em segundo plano</string> + <string name="stores_notification_one">A transferir</string> + <string name="stores_notification_many">%1$d transferências</string> + <string name="stores_notification_waiting">A preparar…</string> + <string name="stores_notification_done_channel">Transferências da loja concluídas</string> + <string name="stores_notification_installed">%1$s instalado</string> + <string name="stores_notification_failed">Falha na transferência de %1$s</string> + <string name="stores_notification_delete">Eliminar</string> + <string name="stores_notification_keep">Manter</string> + <string name="stores_login_title">Iniciar sessão em %1$s</string> + <string name="stores_login_provider_title">Iniciar sessão com o seu fornecedor</string> + <string name="stores_login_reload">Recarregar</string> + <string name="stores_login_close">Fechar</string> + <string name="stores_login_retry">Tentar novamente</string> + <string name="stores_login_finishing">A concluir o início de sessão…</string> + <string name="stores_login_error_network">Não foi possível carregar a página de início de sessão: %1$s</string> + <string name="stores_login_error_idp_blocked">Este fornecedor recusa o início de sessão a partir de uma página incorporada. Inicie sessão com a palavra-passe da loja.</string> + <string name="stores_login_error_http">A página de início de sessão respondeu com um erro (HTTP %1$d).</string> + <string name="stores_login_error_ssl">A ligação à página de início de sessão não é segura.</string> + <string name="stores_login_error_verification">Não foi possível verificar o início de sessão. Tente novamente.</string> + <string name="stores_login_error_generic">Falha no início de sessão. Tente novamente.</string> + <string name="common_close">Fechar</string> + <string name="common_back">Voltar</string> + <string name="common_edit">Editar</string> + <string name="games_add_all">Adicionar todos os jogos desta pasta</string> + <string name="games_added_none">Nada de novo nesta pasta</string> + <string name="edit_name">Nome</string> + <string name="edit_target">Destino</string> + <string name="edit_start_in">Iniciar em</string> + <string name="edit_launch_options">Opções de arranque</string> + <string name="edit_artwork">Imagens</string> + <string name="edit_other_exe">Outro exe…</string> + <string name="edit_remove">Remover</string> + <string name="art_cover">Capa</string> + <string name="art_background">Fundo</string> + <string name="art_logo">Logótipo</string> + <string name="art_icon">Ícone</string> + <string name="art_src_folder">Pasta do jogo</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">O seu ficheiro</string> + <string name="art_src_exe">Do exe</string> + <string name="art_src_auto">Automático</string> + <string name="art_pick_image">Escolher imagem…</string> + <string name="art_pick_title">Escolha uma imagem: %1$s</string> + <string name="art_reset">Repor automático</string> + <plurals name="games_added_n"> + <item quantity="one">%1$d jogo adicionado</item> + <item quantity="many">%1$d jogos adicionados</item> + <item quantity="other">%1$d jogos adicionados</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="one">%1$d já está em Jogos</item> + <item quantity="many">%1$d já estão em Jogos</item> + <item quantity="other">%1$d já estão em Jogos</item> + </plurals> + <string name="hop_back">B · Voltar para %1$s</string> + <string name="hop_from_games">o jogo</string> + <string name="hop_from_play">as definições de jogo</string> + <string name="sheet_graphics_drivers">Controladores gráficos</string> + <string name="comp_this_proton">Este Proton</string> + <string name="comp_every_proton">Todos os Proton</string> + <string name="comp_for_every_proton">Para todos os Proton</string> + <string name="comp_available_count">Disponíveis · %1$d</string> + <string name="comp_checked">Verificado %1$s</string> + <string name="comp_check_all">Procurar novos componentes e controladores</string> + <string name="comp_get">Obter</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton e FEX abaixo são deste Proton</string> + <string name="comp_device_tested">%1$s · testado</string> + <string name="comp_device_untested">%1$s · não testado</string> + <string name="comp_device_unsupported">%1$s · não suportado</string> + <string name="gpu_auto_keeps">Mantém o par recomendado atualizado</string> + <string name="gpu_bar_auto">Automático · %1$s</string> + <string name="gpu_separately">Controladores do runtime e de ecrã em separado</string> + <string name="gpu_use_title">Usar %1$s?</string> + <string name="proton_remove_title">Remover %1$s?</string> + <string name="setup_tab_diagnostics">Diagnóstico</string> + <string name="setup_ready">Pronto para jogar</string> + <string name="setup_not_ready">Ainda não está pronto para jogar</string> + <string name="setup_ready_runtime">runtime %1$s</string> + <string name="setup_ready_limit_off">limite de processos desativado</string> + <string name="setup_ready_signed_in">sessão iniciada como %1$s</string> + <string name="setup_needs_you">Quando algo precisa de si</string> + <string name="setup_group_look">Aspeto</string> + <string name="setup_group_device">Este dispositivo</string> + <string name="setup_group_art">Imagens da biblioteca</string> + <string name="flathub_eyebrow">APPS LINUX</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Apps para o ambiente de trabalho Linux, abertas a partir de Ambiente de trabalho.</string> + <string name="flathub_setup">Configurar o Flathub</string> + <string name="flathub_remove_title">Remover %1$s?</string> + <string name="flathub_remove_note">As definições e os ficheiros da app são mantidos.</string> + <string name="flathub_keep">Manter</string> + <string name="desktop_size">Tamanho no disco</string> + <string name="desktop_uninstall_title">Desinstalar o ambiente de trabalho?</string> + <string name="desktop_all_systems">Todos</string> + <string name="gog_achievements">Conquistas da GOG</string> + <string name="gog_achievements_hint">Sincroniza conquistas dos jogos compatíveis da GOG. Aplica-se no próximo arranque.</string> + <string name="gog_achievements_off">Conquistas desativadas</string> + <string name="gog_achievements_on">Conquistas ativadas</string> + <string name="gog_achievements_signin">Inicia sessão na GOG novamente</string> + <string name="gog_achievements_uploaded">Conquistas enviadas</string> + <string name="gog_achievements_pending">Envio da conquista pendente</string> + <string name="gog_achievements_enable_store">Ativa as conquistas da GOG nas definições de Lojas</string> + <string name="gog_achievements_unavailable">Conquistas indisponíveis; verifica os registos da sessão</string> + <string name="achievement_unlocked">Conquista desbloqueada</string> + <string name="touch_paste">Colar</string> + <string name="touch_dpad">Direcional</string> + <string name="touch_left_stick">Stick esquerdo</string> + <string name="touch_right_stick">Stick direito</string> + <string name="touch_menu">Menu</string> + <string name="touch_keyboard">Teclado</string> + <string name="touch_dpad_up">Direcional para cima</string> + <string name="touch_dpad_down">Direcional para baixo</string> + <string name="touch_dpad_left">Direcional para a esquerda</string> + <string name="touch_dpad_right">Direcional para a direita</string> + <string name="touch_title">Controlos táteis</string> + <string name="touch_controller">Controlador tátil</string> + <string name="touch_mouse">Rato</string> + <string name="touch_on">Ativado</string> + <string name="touch_off">Desativado</string> + <string name="touch_speed_slow">Lenta</string> + <string name="touch_speed_normal">Normal</string> + <string name="touch_speed_fast">Rápida</string> + <string name="touch_edit_layout">Editar esquema</string> + <string name="touch_droiddeck_menu">Menu do DroidDeck</string> + <string name="touch_drag_hint">Arrasta os controlos para o ecrã</string> + <string name="touch_arrange">Organizar</string> + <string name="touch_discard">Rejeitar</string> + <string name="touch_saving">A guardar…</string> + <string name="touch_icon">Ícone</string> + <string name="touch_narrower">L−</string> + <string name="touch_wider">L+</string> + <string name="touch_shorter">A−</string> + <string name="touch_taller">A+</string> + <string name="touch_hide">Ocultar</string> + <string name="touch_color_opacity">Cor e opacidade…</string> + <string name="touch_copy_layout">Copiar este esquema</string> + <string name="touch_layout_copied">Esquema copiado; cola-o no esquema tátil de qualquer jogo</string> + <string name="touch_paste_layout">Colar esquema copiado</string> + <string name="touch_reset_layout">Restaurar o padrão da Steam</string> + <string name="touch_no_binding">Este controlo não tem uma ação atribuída na Steam para receber um ícone</string> + <string name="touch_icon_for">Ícone para %1$s</string> + <string name="touch_layout_color">Cor do esquema tátil</string> + <string name="touch_done">Concluído</string> + <string name="touch_save_failed">Não foi possível guardar o esquema tátil</string> + <string name="touch_saved">Esquema tátil guardado na Steam</string> + <string name="touch_app_id">Aplicação %1$d</string> + <string name="touch_label_hint">Etiqueta (opcional)</string> + <string name="touch_icons_missing">Os ícones da Steam não foram encontrados</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d ícone da Steam</item> + <item quantity="many">%1$d ícones da Steam</item> + <item quantity="other">%1$d ícones da Steam</item> + </plurals> + <string name="touch_icon_color">Cor do ícone</string> + <string name="touch_button_color">Cor do botão</string> + <string name="touch_no_icon">Sem ícone</string> + + <string name="stores_sync_sign_in">A verificar o início de sessão…</string> + <string name="stores_sync_library">A obter a tua biblioteca…</string> + <string name="stores_sync_game_list">A obter a lista de jogos…</string> + <string name="stores_sync_details">A carregar os detalhes dos jogos… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">A sincronizar %1$d jogo…</item> + <item quantity="many">A sincronizar %1$d jogos…</item> + <item quantity="other">A sincronizar %1$d jogos…</item> + </plurals> + <string name="stores_sync_progress">A sincronizar… %1$d/%2$d</string> + <string name="stores_sync_cover_art">A obter as capas…</string> + <string name="stores_sync_failed">Falha na sincronização</string> + <string name="stores_sync_no_games">Não foram encontrados jogos na tua biblioteca da %1$s</string> + <string name="stores_sync_amazon_no_answer">A Amazon Games não respondeu sobre a tua biblioteca: atualiza, ou inicia sessão novamente</string> + <string name="stores_sync_gog_unreachable">Não foi possível contactar a GOG</string> + <string name="stores_sync_gog_unreadable">Não foi possível ler a tua biblioteca da GOG</string> + <string name="stores_registering">A registar na Steam…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Modo Cardboard</string> + <string name="cardboard_mode_hint">Imagens para o olho esquerdo e o direito, para uns óculos de RV para telemóvel.</string> + <string name="cardboard_source">Imagem</string> + <string name="cardboard_source_screen">Ecrã virtual</string> + <string name="cardboard_source_sbs">3D lado a lado</string> + <string name="cardboard_viewer">Óculos</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Sem lente</string> + <string name="cardboard_ipd">Distância entre os olhos</string> + <string name="cardboard_screen_width">Largura do ecrã</string> + <string name="cardboard_screen_distance">Distância do ecrã</string> + <string name="cardboard_head_tracking">Seguimento da cabeça</string> + <string name="cardboard_no_sensor">Este dispositivo não tem sensor de rotação.</string> + <string name="cardboard_recenter">Recentrar vista</string> + <string name="cardboard_recenter_button">Recentrar</string> + <string name="cardboard_steamvr">SteamVR (experimental)</string> + <string name="cardboard_steamvr_install">Instalar</string> + <string name="cardboard_steamvr_install_hint">Transfere o SteamVR ARM64 da Valve (571 MB).</string> + <string name="cardboard_steamvr_installing">A transferir e a descompactar o SteamVR…</string> + <string name="cardboard_steamvr_start">Iniciar</string> + <string name="cardboard_steamvr_ready_hint">Executa o SteamVR com a tua cabeça como headset; a imagem muda para 3D lado a lado.</string> + <string name="cardboard_steamvr_stop">Parar</string> + <string name="cardboard_steamvr_running_hint">O SteamVR está em execução.</string> + <string name="cardboard_steamvr_unavailable">Indisponível nesta sessão.</string> + <string name="cardboard_steamvr_failed">O SteamVR não respondeu; verifica o registo da sessão.</string> + <string name="drawer_component_downloads">Transferir componentes</string> + <string name="drawer_component_downloads_hint">As transferências são partilhadas com a página Componentes. Escolhe acima uma versão transferida para a usar quando iniciares o próximo jogo.</string> + <string name="drawer_component_choose_download">Escolhe uma versão para transferir</string> + <string name="stores_fact_developer">Programadora</string> + <string name="stores_fact_publisher">Editora</string> + <string name="stores_fact_released">Lançamento</string> + <string name="stores_hltb_main">Principal</string> + <string name="stores_hltb_extras">+ Extras</string> + <string name="stores_hltb_completionist">Completista</string> + <string name="stores_hltb_hours">%1$s h</string> + <string name="stores_hltb_minutes">%1$d min</string> +</resources> diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml new file mode 100644 index 000000000..ae5b83b25 --- /dev/null +++ b/app/src/main/res/values-pt/strings.xml @@ -0,0 +1,2260 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>Na primeira execução, a Steam é baixada. Isso pode levar alguns minutos.</item> + <item>Por padrão, sair do app pausa a Steam; ela continua de onde parou quando você voltar.</item> + <item>Seu controle aparece como um controle de Xbox 360. Sem controle, use os controles na tela.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>Na primeira execução, a área de trabalho é preparada. Isso pode levar alguns minutos.</item> + <item>Por padrão, sair do app pausa a sessão; ela continua de onde parou quando você voltar.</item> + <item>O toque funciona como um touchpad: arraste para mover o ponteiro, toque para clicar.</item> + <item>Seu controle aparece como um controle de Xbox 360. Sem controle, use os controles na tela.</item> + </string-array> + <string name="session_channel">Sessão</string> + <string name="session_channel_description">Aparece enquanto uma sessão da Steam ou da área de trabalho está aberta</string> + <string name="session_notification">A Steam está em execução</string> + <string name="session_notification_downloads">Download da Steam em andamento</string> + <string name="session_notification_paused">Sessão pausada - toque em Retomar quando quiser</string> + <string name="resume_session">Retomar</string> + <string name="suspend_now">Suspender agora</string> + <string name="stop_session">Encerrar sessão</string> + <string name="frame_gen_title">Geração de quadros</string> + <string name="frame_gen_hint">Selecione o modo de geração de quadros</string> + <string name="frame_gen_off">Desativada</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG Adaptativo %1$d</string> + <string name="lsfg_needs_steam">Requer o Lossless Scaling na Steam</string> + <string name="lsfg_needs_install">Instale o Lossless Scaling na Steam ou importe o Lossless.dll</string> + <string name="lsfg_import">Importar Lossless.dll…</string> + <string name="lsfg_using_steam">Usando a cópia da Steam</string> + <string name="lsfg_using_import">Usando uma cópia importada</string> + <string name="lsfg_pick_title">Escolha o Lossless.dll</string> + <string name="lsfg_imported">Lossless.dll importado</string> + <string name="lsfg_import_unchanged">Este Lossless.dll já está em uso</string> + <string name="lsfg_import_not_owned">Esta conta Steam não tem o Lossless Scaling</string> + <string name="lsfg_import_invalid">Este arquivo não é o Lossless.dll do Lossless Scaling</string> + <string name="lsfg_import_failed">Não foi possível preparar o LSFG com este Lossless.dll</string> + <string name="lsfg_problem_missing">O LSFG requer o Lossless Scaling da Steam ou um Lossless.dll importado</string> + <string name="lsfg_problem_failed">O LSFG não conseguiu preparar os shaders a partir do Lossless.dll</string> + <string name="hud_fg_unsupported">%1$s: sem suporte</string> + <string name="hud_fg_failed">%1$s: falha no mecanismo</string> + <string name="hud_fg_needs_lossless">%1$s: requer Lossless Scaling</string> + <string name="hud_fg_starting">%1$s %2$s · iniciando</string> + <string name="second_library_import_hint">Adiciona uma biblioteca Steam e procura jogos de Windows nela. Os jogos da sua conta usam as entradas da Steam; os demais viram atalhos não Steam.</string> + <string name="game_env_title">Ambiente do jogo</string> + <string name="game_env_hint">Variáveis de ambiente para jogos com Proton.</string> + <string name="game_env_edit">Editar</string> + <string name="game_env_shared">Todos os jogos com Proton</string> + <string name="game_env_scope">Perfil</string> + <string name="game_env_app_id">ID do app na Steam</string> + <string name="game_env_open">Abrir</string> + <string name="game_env_profile">Jogo %1$s</string> + <string name="game_env_add">Adicionar variável</string> + <string name="game_env_name">Nome da variável</string> + <string name="game_env_value">Valor</string> + <string name="game_env_save">Salvar</string> + <string name="game_env_remove">Remover</string> + <string name="game_env_restore">Herdar novamente</string> + <string name="game_env_reset">Limpar substituições de ambiente</string> + <string name="game_env_unset">Não definir neste perfil</string> + <string name="game_env_empty">Nenhuma variável definida.</string> + <string name="game_env_inherited">Herdada</string> + <string name="game_env_custom">Variável personalizada</string> + <string name="game_env_error">Não foi possível carregar ou salvar. Verifique o espaço livre e tente novamente.</string> + <string name="game_env_invalid">Use um nome válido e um valor de até 8192 caracteres.</string> + <string name="game_env_unused">O runtime padrão do Proton não lê esta variável.</string> + <string name="game_env_loading">Carregando…</string> + <string name="game_env_feature">Substitui o nível de recursos do D3D12. 12_2 informa os recursos do DirectX 12 Ultimate; recursos sem suporte podem falhar. Deixe sem definir para detecção automática.</string> + <string name="game_env_shader">Substitui o shader model do D3D12. 6_9 requer um VKD3D recente o bastante e recursos de GPU compatíveis. Deixe sem definir para detecção automática.</string> + <string name="game_env_cache">Mantenha o cache de shaders do Mesa ativado para reduzir compilações repetidas. false ativa o cache.</string> + <string name="game_env_cache_size">Tamanho máximo do cache de shaders do Mesa, como 1G. Caches maiores ocupam mais armazenamento.</string> + <string name="game_env_vkd3d_config">Opções do VKD3D para D3D12. nodxr desativa o ray tracing; use só em um jogo que precise disso.</string> + <string name="game_env_proton_log">Defina como 1 para gerar um log de diagnóstico do Proton. O log pode reduzir o desempenho e ocupar armazenamento.</string> + <string name="game_env_wined3d">Defina como 1 para usar o WineD3D (baseado em OpenGL) em vez do DXVK ao testar a compatibilidade com D3D9–11.</string> + <string name="game_env_no_ntsync">Defina como 1 para manter este jogo fora do ntsync (droiddeck-ntsync ou um driver do kernel); ele usa o droiddeck-esync onde houver um pacote, senão o droiddeck-fsync.</string> + <string name="game_env_no_esync">Defina como 1 para desativar o droiddeck-esync neste jogo; ele passa a usar o droiddeck-fsync, ou o droiddeck-ntsync onde o fsync também estiver desativado.</string> + <string name="game_env_no_fsync">Defina como 1 para manter este jogo fora do droiddeck-fsync; ele usa o droiddeck-esync onde houver um pacote, senão o droiddeck-ntsync.</string> + <string name="game_env_fsync_first">Defina como 1 para rodar este jogo no droiddeck-fsync, ou como 0 para priorizar o droiddeck-esync onde houver um pacote compatível com o Proton dele.</string> + <string name="game_env_xalia">Defina como 0 para desativar o assistente de navegação por controle do Proton neste jogo.</string> + <string name="game_env_hud">Overlay do DXVK para D3D9–11. fps mostra a taxa de quadros; sem definir, fica oculto.</string> + <string name="game_env_dxvk_config">Configurações do DXVK separadas por ponto e vírgula. Versões recentes usam dxvk.maxFrameRate para limitar os quadros; para jogar normalmente, prefira o limitador de quadros da sessão.</string> + <string name="game_env_vkd3d_limit">Limite de taxa de quadros do D3D12. 0 desativa o limite.</string> + <string name="game_env_glthread">Threading do OpenGL no Mesa. true pode ajudar jogos OpenGL limitados pela CPU; teste jogo a jogo.</string> + <string name="sync_backend_title">Sincronização do Wine</string> + <string name="sync_backend_hint">Como os jogos mantêm suas threads em sincronia. Vale a partir da próxima sessão.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">Predefinição do FEX</string> + <string name="fex_next_launch">Vale a partir da próxima vez que um jogo for aberto.</string> + <string name="game_settings_title">Jogos</string> + <string name="fex_default_label">Padrões do FEX</string> + <string name="fex_default_detail">Usa os padrões do FEX.</string> + <string name="fex_stability_label">Estabilidade</string> + <string name="fex_stability_detail">Configurações mais seguras e mais lentas.</string> + <string name="fex_compatibility_label">Compatibilidade</string> + <string name="fex_compatibility_detail">TSO completo com multiblock.</string> + <string name="fex_intermediate_label">Intermediário</string> + <string name="fex_intermediate_detail">TSO equilibrado com precisão x87 reduzida.</string> + <string name="fex_performance_label">Desempenho</string> + <string name="fex_performance_detail">Todo o TSO desativado; multiblock.</string> + <string name="fex_performance_tso_label">Desempenho + TSO</string> + <string name="fex_performance_tso_detail">Configurações de Desempenho com o TSO principal ativado.</string> + <string name="fex_extreme_label">Extremo</string> + <string name="fex_extreme_detail">Desativa as verificações de código automodificável. Jogos com JIT e DRM podem quebrar.</string> + <string name="fex_extreme_tso_label">Extremo + TSO</string> + <string name="fex_extreme_tso_detail">Configurações do Extremo com o TSO principal ativado.</string> + <string name="fex_extreme_gn_label">Extremo-gn</string> + <string name="fex_extreme_gn_detail">Desempenho com dicas de TSC e de metadados. Tente antes do Extremo.</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">Desempenho com verificações de código completas e o bit de hypervisor oculto.</string> + <string name="game_env_custom_value">Valor personalizado</string> + <string name="game_env_select_value">Selecionar valor</string> + <string name="user_apps_section">Emuladores e apps</string> + <string name="user_apps_add">Adicionar</string> + <string name="user_apps_add_detail">Seu próprio app ou jogo</string> + <string name="user_apps_kind_script">Script</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d%%</string> + <string name="user_apps_starting">Iniciando…</string> + <string name="user_apps_added">%1$s adicionado</string> + <string name="user_apps_removed">%1$s removido</string> + <string name="user_apps_back">Área de trabalho</string> + <string name="user_apps_gone">Esse app não existe mais. Escolha-o de novo na Área de trabalho.</string> + <string name="user_apps_open">Abrir %1$s</string> + <string name="user_apps_remove">Remover</string> + <string name="user_apps_remove_confirm">Pressione de novo para remover</string> + <string name="user_apps_stop_session">Encerre a sessão para remover</string> + <string name="user_apps_script_linked">Executa %1$s a partir da própria pasta, vinculada à sessão no local onde está. Remover o app não mexe nessa pasta.</string> + <string name="user_apps_script_copied">Executa %1$s a partir de uma cópia da pasta dele dentro do DroidDeck. Remover o app apaga a cópia, não o original.</string> + <string name="user_apps_runtime_required">Instale primeiro o runtime do Linux</string> + <string name="user_apps_file_gone">O arquivo não existe mais</string> + <string name="user_apps_no_space">Espaço livre insuficiente: copiar a pasta requer %1$s</string> + <string name="user_apps_copying">Copiando %1$s</string> + <string name="user_apps_failed">Falha ao adicionar o app</string> + <string name="user_apps_github_checking">Procurando uma versão de %1$s</string> + <string name="user_apps_github_unreachable">Não foi possível ler as versões de %1$s</string> + <string name="user_apps_github_no_appimage">Nenhuma versão de %1$s tem um AppImage ARM64 ou x86_64</string> + <string name="user_apps_downloading">Baixando %1$s</string> + <string name="user_apps_download_failed">Falha no download</string> + <string name="user_apps_update_failed">Falha ao atualizar o app</string> + <string name="user_apps_icon_failed">Não foi possível ler esse ícone</string> + <string name="user_apps_saved">%1$s salvo</string> + <string name="user_apps_edit">Editar</string> + <string name="user_apps_check_updates">Verificar atualizações</string> + <string name="user_apps_checking_updates">Verificando atualizações…</string> + <string name="user_apps_update_to">Atualizar para %1$s</string> + <string name="user_apps_up_to_date">Atualizado · %1$s</string> + <string name="user_apps_updated">%1$s atualizado</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">Adicionar um app</string> + <string name="add_app_tab_script">Script</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">Um arquivo .sh. Ele roda a partir da própria pasta, em tela cheia no gamescope.</string> + <string name="add_app_appimage_hint">Um AppImage ARM64 (aarch64) ou um x86_64 que roda pelo FEX. Ele é descompactado no runtime do Linux uma única vez.</string> + <string name="add_app_github_hint">Instala o AppImage ARM64 da versão mais recente do repositório, ou o x86_64 dela pelo FEX.</string> + <string name="app_fex_title">Usar FEX</string> + <string name="app_fex_auto">Automático</string> + <string name="app_fex_on">Ativado</string> + <string name="app_fex_off">Desativado</string> + <string name="app_fex_hint">O FEX roda programas Linux x86 neste dispositivo ARM64. No Automático, ele é usado quando o programa é feito para x86.</string> + <string name="app_fex_x86">%1$s pelo FEX</string> + <string name="app_fex_not_ready">Este programa é feito para PCs %1$s e roda pelo FEX, que ainda não está configurado. Abra a Steam uma vez com a sessão iniciada: o DroidDeck faz com que ela baixe o FEX e o Steam Linux Runtime necessários, e então isto abre.</string> + <string name="session_end_program">Este programa</string> + <string name="session_end_fex_missing">%1$s é feito para PCs x86 e roda pelo FEX, que ainda não está instalado. Abra a Steam uma vez com a sessão iniciada e deixe-a baixar o FEX e o Steam Linux Runtime; depois, abra isto de novo.</string> + <string name="add_app_flatpak_hint">Um ID de app do Flathub ou a página dele no Flathub. O Flatpak é configurado antes, se ainda não estiver.</string> + <string name="add_app_file">Arquivo</string> + <string name="add_app_no_file">Nenhum arquivo escolhido</string> + <string name="add_app_choose_file">Escolher arquivo</string> + <string name="add_app_change_file">Alterar</string> + <string name="add_app_pick_script">Escolha um script</string> + <string name="add_app_pick_appimage">Escolha um AppImage</string> + <string name="add_app_pick_icon">Escolha um ícone</string> + <string name="add_app_repository">Repositório</string> + <string name="add_app_repository_placeholder">dono/repo ou um link do github.com</string> + <string name="add_app_repository_invalid">Não é um repositório do GitHub</string> + <string name="add_app_flatpak_id">ID do app</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">Não é um ID de app do Flathub</string> + <string name="add_app_flatpak_bundle_hint">Ou um arquivo .flatpak, para um app que não está no Flathub.</string> + <string name="add_app_pick_flatpak">Escolha um arquivo .flatpak</string> + <string name="add_app_source">Origem</string> + <string name="add_app_appearance">Na sua Área de trabalho</string> + <string name="add_app_preview_name">Novo app</string> + <string name="add_app_name">Nome</string> + <string name="add_app_name_placeholder">Do app</string> + <string name="add_app_icon">Ícone</string> + <string name="add_app_icon_default">Ícone padrão</string> + <string name="add_app_choose_icon">Escolher ícone</string> + <string name="add_app_clear_icon">Limpar</string> + <string name="add_app_icon_suggestions">Do repositório</string> + <string name="add_app_icon_searching">Procurando ícones…</string> + <string name="add_app_icon_reset">Padrão</string> + <string name="edit_app_title">Editar %1$s</string> + <string name="edit_app_save">Salvar</string> + <string name="add_app_checking_folder">Verificando a pasta…</string> + <string name="add_app_folder_linked">A pasta dele é vinculada à sessão: %1$s</string> + <string name="add_app_folder_copied">A pasta dele contém programas Linux, que o armazenamento compartilhado não consegue executar, então ela é copiada para o DroidDeck (%1$s).</string> + <string name="add_app_confirm">Adicionar</string> + <string name="add_app_cancel">Cancelar</string> + <string name="add_app_runtime_required">Instale primeiro o runtime do Linux em Configuração.</string> + <string name="add_app_busy">Aguarde %1$s terminar.</string> + <string name="steam_compat_label">Compatível</string> + <string name="proton_compatible_note">As versões marcadas como “%1$s” no menu Compatibilidade da Steam têm as correções de prefixo do DroidDeck e iniciam jogos aqui. O padrão é o Proton Experimental ARM64 (%1$s), ou o Proton 11.0 ARM64 (%1$s) quando essa versão mais nova estiver instalada. As entradas do Proton da própria Valve sem a marca não conseguem iniciar jogos neste dispositivo.</string> + <string name="proton_installed_compatible">%1$s instalado · %2$s</string> + <string name="common_cancel">Cancelar</string> + <string name="common_auto">Automático</string> + <string name="common_never">Nunca</string> + <string name="common_always">Sempre</string> + <string name="common_uninstall">Desinstalar</string> + <string name="common_check">Verificar</string> + <string name="common_update">Atualizar</string> + <string name="common_checking">Verificando…</string> + <string name="common_working">Processando…</string> + <string name="common_forget">Esquecer</string> + <string name="common_add_ellipsis">Adicionar…</string> + <string name="common_use">Usar</string> + <string name="common_applies_next_session">Vale a partir da próxima sessão.</string> + <string name="mode_steam_title">Sessão da Steam</string> + <string name="mode_desktop_title">Sessão da área de trabalho</string> + <string name="mode_display">Tela</string> + <string name="mode_resolution">Resolução</string> + <string name="mode_res_720">Até 720p</string> + <string name="mode_res_900">Até 900p</string> + <string name="mode_res_1080">Até 1080p</string> + <string name="mode_res_native">Nativa</string> + <string name="mode_res_default">%1$s (padrão)</string> + <string name="mode_res_custom_value">Personalizada · %1$d×%2$d</string> + <string name="mode_res_custom">Personalizada…</string> + <string name="mode_res_note">Em 720p, os menus podem responder mais rápido.</string> + <string name="mode_ratio">Proporção da tela</string> + <string name="mode_ratio_custom">Definida pela resolução personalizada.</string> + <string name="mode_ratio_auto">O Automático usa no mínimo 16:9.</string> + <string name="mode_fps">Limite de quadros</string> + <string name="mode_fps_note">Limita a sessão inteira. A tela muda para uma taxa de atualização que seja múltipla do limite.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">Saída HDR10</string> + <string name="mode_hdr_unavailable">Indisponível: %1$s.</string> + <string name="mode_hdr_restart">Reinicie o app para aplicar.</string> + <string name="mode_drivers">Drivers</string> + <string name="mode_gpu_drivers">Drivers de GPU</string> + <string name="mode_gpu_drivers_hint">Compartilhados pela Steam e pela Área de trabalho. Defina na página Componentes.</string> + <string name="mode_touch_controls">Toque e controles</string> + <string name="mode_touch">Toque</string> + <string name="mode_touch_touchpad">Touchpad</string> + <string name="mode_touch_direct">Direto</string> + <string name="mode_touch_note">Automático: touchpad na Área de trabalho, direto na Steam.</string> + <string name="mode_osc">Controles na tela</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">No Automático, os controles na tela aparecem quando não há um controle conectado.</string> + <string name="osc_steam_touch">Toque da Steam (beta)</string> + <string name="osc_style_next_session">Os controles de toque da Steam começam na próxima sessão da Steam</string> + <string name="osc_steam_layout">Layout de toque da Steam</string> + <string name="osc_steam_layout_hint">Mova, redimensione ou oculte os controles de toque deste jogo; salvo na Steam</string> + <string name="mode_controller">Controle</string> + <string name="mode_controller_hint">Como a Steam vê seu controle. Vale a partir da próxima sessão.</string> + <string name="mode_controller_deck">Controle do Steam Deck</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Controle de Xbox 360</string> + <string name="mode_controller_note">Deck: botão de Acesso rápido e giroscópio. Steam Controller: o mesmo, com detecção de pegada nos botões traseiros. DualShock 4 e DualSense: giroscópio, e os trackpads como o touchpad deles. Xbox 360: abra o Acesso rápido com Guide+A.</string> + <string name="mode_back">Voltar</string> + <string name="back_menu_then_qam">Menu → QAM</string> + <string name="back_qam_then_menu">QAM → Menu</string> + <string name="mode_session">Sessão</string> + <string name="mode_suspend">Comportamento em segundo plano</string> + <string name="mode_suspend_hint">Quando o app fica oculto ou a tela se apaga.</string> + <string name="mode_suspend_manual">Manual</string> + <string name="mode_suspend_native">Nativo</string> + <string name="mode_suspend_steam_note">Automático pausa e retoma quando o app reaparece. Nativo prepara a Steam para a suspensão antes. Manual aguarda Retomar. Nunca continua em execução.</string> + <string name="mode_background_downloads">Manter downloads ativos</string> + <string name="mode_background_downloads_hint">Adia a suspensão em segundo plano enquanto a Steam estiver baixando.</string> + <string name="mode_suspend_note">Automático pausa em segundo plano e retoma quando o app reaparece. Manual aguarda Retomar. Nunca continua em execução.</string> + <string name="mode_startup">Inicialização</string> + <string name="mode_steam_startup">Iniciar a Steam com o DroidDeck</string> + <string name="mode_steam_startup_hint">Abre a sessão da Steam quando o DroidDeck é iniciado.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">Procurando versões…</string> + <string name="mode_decky_ready">Pronto para instalar %1$s.</string> + <string name="mode_decky_none">Nenhuma versão compatível encontrada. Tente novamente mais tarde.</string> + <string name="mode_decky_installed">Instalado · %1$s</string> + <string name="mode_decky_update">Atualização disponível · %1$s</string> + <string name="mode_decky_current">Atualizado · %1$s</string> + <string name="mode_decky_loader">Loader</string> + <string name="mode_decky_install_latest">Instalar a mais recente</string> + <string name="mode_decky_on">Inicia com a Steam. Enquanto estiver ativado, outros apps neste dispositivo podem conseguir controlar a Steam.</string> + <string name="mode_decky_off">Desativado. A porta de depuração remota da Steam fica fechada.</string> + <string name="mode_decky_remove_title">Desinstalar o Decky Loader?</string> + <string name="mode_decky_remove_text">Remove o loader. Seus plugins e configurações são mantidos.</string> + <string name="mode_client">Cliente</string> + <string name="mode_mangoapp">Overlay de desempenho</string> + <string name="mode_mangoapp_hint">Desative se os jogos travarem ou ficarem com a tela preta no modo Deck. Vale a partir da próxima sessão.</string> + <string name="mode_steam_repair">Reparar o cliente Steam</string> + <string name="mode_steam_repair_hint">Baixa o cliente Steam de novo na próxima vez que a Steam iniciar. Use se a Steam ficar presa na tela de carregamento. Jogos, login e configurações são mantidos.</string> + <string name="mode_steam_repair_queued">O cliente Steam será baixado de novo na próxima vez que a Steam iniciar.</string> + <string name="mode_steam_repair_button">Reparar</string> + <string name="mode_added_art">Artes</string> + <string name="mode_sgdb_key">Chave de API do SteamGridDB</string> + <string name="mode_sgdb_key_yours">Sua chave</string> + <string name="mode_sgdb_edit">Editar…</string> + <string name="mode_added_art_hint">Preenche as artes que faltam com a Steam e, depois, com o SteamGridDB. Seus próprios cover.jpg, hero.jpg, logo.png ou icon.png na pasta do jogo têm prioridade.</string> + <string name="summary_remove_game">Remover %1$s</string> + <string name="mode_added_remove_confirm">Remover?</string> + <string name="games_add">Adicionar jogo</string> + <string name="games_add_title">Adicionar um jogo:</string> + <string name="games_pick_exe_title">Escolha o exe:</string> + <string name="games_add_unreachable">Este local não está disponível na sessão</string> + <string name="mode_added_exe_check">Verificar este exe</string> + <string name="wincomp_title">Componentes do Windows</string> + <string name="wincomp_pad_hint">LB / RB alternam entre as seções.</string> + <string name="wincomp_card_none">Nenhum</string> + <string name="wincomp_applies">DLLs do Windows que o Proton não inclui, da lista de componentes. Ative as de que este jogo precisa: elas vão para a pasta do Windows do próprio jogo, nunca para os arquivos dele, e continuam lá em qualquer Proton que ele usar.</string> + <string name="wincomp_offline">Não foi possível acessar a lista de componentes. Os componentes já baixados ainda podem ser ativados ou desativados.</string> + <string name="wincomp_recommended">Recomendados para este jogo</string> + <string name="wincomp_checking">Verificando do que este jogo precisa…</string> + <string name="wincomp_when_online">Disponível quando on-line</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">Ativando %1$d componente…</item> + <item quantity="many">Ativando %1$d componentes…</item> + <item quantity="other">Ativando %1$d componentes…</item> + </plurals> + <string name="wincomp_auto_section">Ativados para este jogo</string> + <string name="wincomp_auto_steam">A Steam instala %1$s com %2$s (app %3$s)</string> + <string name="wincomp_auto_gog">Da lista do GOG para este jogo</string> + <string name="wincomp_auto_epic">Dos pré-requisitos da Epic para este jogo</string> + <string name="wincomp_auto_amazon">Das etapas de instalação da Amazon para este jogo</string> + <string name="wincomp_steam_by_name">A Steam instala %1$s com o app %2$s, identificado pelo nome</string> + <string name="wincomp_suggestions">Também listados</string> + <string name="wincomp_suggestion_reason">Listado por %1$s; não é um componente aqui</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">O jogo vem com %1$s</string> + <string name="wincomp_found_shipped">O jogo tem %1$s (costuma funcionar assim mesmo)</string> + <string name="wincomp_already_here">Já está neste jogo</string> + <string name="wincomp_no_recommendation">Nada na pasta do jogo indica um componente.</string> + <string name="wincomp_active">Instalados para este jogo (%1$d)</string> + <string name="wincomp_all">Todos os componentes (%1$d)</string> + <string name="wincomp_installers">Requerem um instalador do Windows (%1$d)</string> + <string name="wincomp_installers_note">Estes são programas de instalação próprios, sem um pacote dentro para desmontar, e ainda sem uma gravação do que instalam. A maioria são runtimes antigos que o Mono do Proton já substitui; as gravações são adicionadas à medida que são feitas.</string> + <string name="wincomp_show">Mostrar</string> + <string name="wincomp_hide">Ocultar</string> + <string name="wincomp_needs_installer">Executa o próprio programa de instalação (ainda sem gravação)</string> + <string name="wincomp_unsupported">Ainda sem suporte aqui</string> + <string name="wincomp_loading">Carregando os componentes…</string> + <string name="wincomp_failed">Não foi possível instalar: %1$s</string> + <string name="wincomp_downloaded">Baixado</string> + <string name="wincomp_downloading">Baixando %1$s</string> + <string name="wincomp_installing">Instalando: %1$s</string> + <string name="wincomp_reverting">Desativando…</string> + <string name="wincomp_download_size">Download de %1$s</string> + <string name="wincomp_next_launch">Entra em vigor na próxima vez que o jogo iniciar.</string> + <string name="mode_stretch169">Esticar jogos 16:9</string> + <string name="mode_stretch169_hint">Em telas mais quadradas que 16:9, estica os jogos para preencher a tela em vez de mostrar barras. Vale a partir da próxima sessão.</string> + <string name="mode_audio">Áudio</string> + <string name="mode_directaudio">DirectAudio para jogos</string> + <string name="mode_directaudio_hint">Ignora o PulseAudio para reduzir a latência.</string> + <string name="mode_mic">Microfone</string> + <string name="mode_mic_hint">Para chat de voz.</string> + <string name="mode_network">Rede</string> + <string name="mode_wifi_discovery">Descoberta de Wi-Fi</string> + <string name="mode_wifi_discovery_hint">Mostra os nomes de Wi-Fi e as redes próximas na Steam.</string> + <string name="mode_wifi_location_off">Os serviços de localização estão desativados.</string> + <string name="mode_wifi_permission_denied">Permissão de localização não concedida.</string> + <string name="mode_wifi_open_settings">Abrir configurações</string> + <string name="mode_wifi_continue">Continuar</string> + <string name="mode_wifi_explain_title">Ativar a descoberta de Wi-Fi?</string> + <string name="mode_wifi_explain_text">O Android exige a permissão de localização para ler nomes de Wi-Fi e descobrir redes próximas. O DroidDeck usa esse acesso para mostrar essas redes na Steam. Não rastreamos sua localização. O acesso à internet funciona sem essa permissão.</string> + <string name="mode_storage">Armazenamento de jogos</string> + <string name="mode_storage_auto">Automático - o cartão SD, se houver, mais suas pastas Games</string> + <string name="mode_storage_auto_none">Automático - suas pastas Games, mais o cartão SD, se houver (nenhum agora)</string> + <string name="mode_storage_internal">Somente interno</string> + <string name="mode_storage_folder">Pasta: %1$s</string> + <string name="mode_second_library">Segunda biblioteca</string> + <string name="mode_storage_note">Jogos que carregam recursos do SD ou do armazenamento compartilhado durante o jogo podem ter engasgos.</string> + <string name="mode_storage_diagnostics">Diagnóstico de armazenamento</string> + <string name="mode_storage_diagnostics_hint">Registra os resultados de alocação e amostras de tempos de armazenamento em Compartilhar logs. Pode deixar as instalações mais lentas enquanto estiver ativado. Vale a partir da próxima sessão.</string> + <string name="mode_choose_folder">Escolher uma pasta…</string> + <string name="mode_custom_res_title">Resolução personalizada</string> + <string name="mode_custom_res_text">Substitui o limite e a proporção. Um tamanho que não corresponda à tela fica com barras.</string> + <string name="mode_width">Largura</string> + <string name="mode_height">Altura</string> + <string name="mode_custom_res_range">Entre 320×240 e 3840×2160.</string> + <plurals name="mode_added_count"> + <item quantity="one">%1$d jogo</item> + <item quantity="many">%1$d jogos</item> + <item quantity="other">%1$d jogos</item> + </plurals> + <string name="common_ok">OK</string> + <string name="common_delete">Excluir</string> + <string name="common_restore">Restaurar</string> + <string name="common_download">Baixar</string> + <string name="common_delete_named">Excluir %1$s</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Driver do runtime</string> + <string name="comp_runtime_driver_hint">Usado pela Steam, pelos jogos e pela área de trabalho. Vale a partir da próxima sessão.</string> + <string name="comp_import_turnip">Importar zip do Turnip…</string> + <string name="comp_display_driver">Driver de exibição</string> + <string name="comp_display_driver_hint">Desenha os quadros na tela nos dois modos. Reinicie o app para aplicar.</string> + <string name="comp_import_adrenotools">Importar um zip do AdrenoTools…</string> + <string name="comp_never">nunca</string> + <string name="comp_nightlies_checked">Nightlies verificadas: %1$s</string> + <string name="comp_title">Componentes</string> + <string name="comp_about">Sobre Componentes</string> + <string name="comp_check_drivers">Procurar novos drivers</string> + <string name="comp_check_nightlies">Procurar novos pacotes nas Nightlies</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">O Proton escolhido aqui roda todos os jogos que não escolheram o próprio na Steam, e uma mudança feita na Steam também aparece aqui. A Steam substitui o Proton da Valve quando atualiza; os originais são mantidos para cada versão.</string> + <string name="comp_scope">Configuração</string> + <string name="comp_profile_configure">Configurar perfil</string> + <string name="comp_profile">Perfil</string> + <string name="comp_scope_general">Perfil padrão</string> + <string name="comp_scope_general_detail">Usado automaticamente por jogos sem substituições.</string> + <string name="comp_profile_game_detail">Configurações salvas, usadas automaticamente quando %1$s é iniciado.</string> + <string name="comp_game_hint">Escolha só as configurações de que este jogo precisa. Todo o resto continua usando o Perfil padrão.</string> + <string name="comp_game_fex">Predefinição do FEX</string> + <string name="comp_game_fex_inherited">Usando a predefinição do Perfil padrão.</string> + <string name="comp_game_fex_override">Predefinição personalizada salva para este jogo.</string> + <string name="comp_game_fex_custom">Variáveis personalizadas do FEX</string> + <string name="comp_game_use_general">Usar padrão</string> + <string name="comp_game_compatibility">Compatibilidade do jogo</string> + <string name="comp_game_environment_hint">Opções de ambiente do Proton, DXVK, VKD3D e de sincronização para este jogo.</string> + <string name="comp_game_proton_inherited">Usando o Proton do Perfil padrão.</string> + <string name="comp_game_proton_override">Proton personalizado salvo para este jogo pelo mapeamento de compatibilidade da Steam.</string> + <string name="comp_no_proton_short">Sem Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Usa o pacote do Perfil padrão de %1$s, a menos que este jogo selecione outro pacote.</string> + <string name="comp_game_component_general">Usa o pacote de componentes do Perfil padrão.</string> + <string name="comp_game_component_override">Usa %1$s só neste jogo.</string> + <string name="comp_game_runtime_options">Opções de runtime</string> + <string name="comp_game_environment_other_hint">Outras opções de ambiente por jogo do Proton, DXVK, VKD3D-Proton e de sincronização.</string> + <string name="comp_game_saved_next">O perfil de %1$s foi salvo para a próxima vez que o jogo for iniciado.</string> + <string name="game_no_steam_title">Iniciar sem o Steam</string> + <string name="game_no_steam_detail">Para um jogo que fecha na hora: inicia ele sem a configuração do Steam no Proton. Recursos do Steam, como a sobreposição, ficam desativados para ele.</string> + <string name="game_no_steam_on">Ativado</string> + <string name="comp_game_proton_live">A escolha de Proton de %1$s foi salva e já está disponível na Steam.</string> + <string name="comp_game_proton_next">A escolha de Proton de %1$s foi salva. Reinicie a Steam antes de iniciar o jogo.</string> + <string name="comp_game_proton_not_runnable">%1$s não pode usar o Proton selecionado neste dispositivo.</string> + <string name="comp_game_load_failed">Não foi possível carregar o perfil de %1$s: %2$s</string> + <string name="comp_game_save_failed">Não foi possível salvar o perfil do jogo: %1$s</string> + <string name="comp_game_reset">Redefinir perfil do jogo</string> + <string name="comp_game_reset_title">Redefinir o perfil de %1$s?</string> + <string name="comp_game_reset_body">Remover as substituições de Proton, FEX, pacote de componentes, ambiente, filtragem de texturas e componentes do Windows deste jogo? Ele passará a usar o Perfil padrão.</string> + <string name="comp_game_reset_done">%1$s agora usa o Perfil padrão.</string> + <string name="comp_game_reset_failed">Não foi possível redefinir completamente o perfil do jogo: %1$s</string> + <string name="comp_default_live">%1$s agora roda todos os jogos que não escolheram o próprio Proton na Steam.</string> + <string name="comp_default_next">%1$s vai rodar todos os jogos que não escolheram o próprio Proton a partir da próxima vez que a Steam iniciar.</string> + <string name="comp_default_not_runnable">%1$s não consegue iniciar jogos aqui, então não pode ser o padrão.</string> + <string name="comp_default_failed">Não foi possível salvar o Proton padrão: %1$s</string> + <string name="comp_swapped">%1$s · %2$d trocados</string> + <string name="comp_all_original">%1$s · todos originais</string> + <string name="comp_game_running_suffix">%1$s · jogo em execução</string> + <string name="comp_esync_pack">%1$s · pacote droiddeck-esync</string> + <string name="comp_esync_wanted">%1$s · ainda sem pacote droiddeck-esync</string> + <string name="comp_esync_builtin">%1$s · esync integrado, sem necessidade de pacote</string> + <string name="comp_gpu_drivers">Drivers de GPU</string> + <string name="comp_reapplied">%1$s · reaplicado ao iniciar %2$s</string> + <string name="comp_next_queued">Próximo: %1$s · depois que o jogo fechar</string> + <string name="comp_in_use">%1$s em uso: %2$s</string> + <string name="comp_waits">Aguarda o jogo fechar</string> + <string name="comp_applies_next_game">Vale a partir da próxima vez que um jogo iniciar</string> + <string name="comp_import_zip">Importar .zip</string> + <string name="comp_reading">Lendo as versões do Proton…</string> + <string name="comp_no_proton">Nenhum Proton instalado ainda. Inicie a Steam uma vez para baixar o Proton ARM64 dela, ou adicione o GE-Proton, CachyOS ou DroidDeck-Proton em Configuração.</string> + <string name="comp_original">Original · %1$s</string> + <string name="comp_this_build">%1$s · esta versão</string> + <string name="comp_earlier_build">%1$s · versão anterior</string> + <string name="comp_installed_count">Instalados · %1$d</string> + <string name="comp_nothing_stored">Nada armazenado para %1$s ainda. Os arquivos deste Proton são salvos como o original dele.</string> + <string name="comp_waits_long">Há um jogo rodando neste Proton, então isto aguarda até ele fechar.</string> + <string name="comp_applies_long">Vale a partir da próxima vez que um jogo iniciar.</string> + <string name="comp_swap_title">Trocar %1$s?</string> + <string name="comp_swap_body">Colocar %1$s em %2$s? Os arquivos originais continuam salvos. %3$s</string> + <string name="comp_restore_title">Restaurar %1$s?</string> + <string name="comp_restore_body">Devolver o %1$s original para %2$s? %3$s</string> + <string name="comp_delete_package_body">Remove o pacote do app. Você pode baixá-lo de novo.</string> + <string name="comp_delete_original_title">Excluir este original?</string> + <string name="comp_delete_original_body">Este %1$s original é de uma versão anterior de %2$s. O original da versão atual é sempre mantido.</string> + <string name="comp_nightlies_count">Nas Nightlies · %1$d</string> + <string name="comp_refresh_to_list">Atualize para listar os pacotes nas Nightlies.</string> + <string name="comp_nothing_new">Nada novo desde a última verificação.</string> + <string name="comp_import_wcp">Importar .wcp</string> + <string name="comp_restore_original">Restaurar o original deste Proton</string> + <string name="comp_restore_original_body">Devolver o %2$s original de %1$s?</string> + <string name="comp_swap">Trocar</string> + <string name="comp_about_text">FEX, DXVK e VKD3D-Proton para cada Proton com que a Steam roda jogos.\n\nSelecione uma linha instalada para colocá-la em uso. A troca vale a partir da próxima vez que um jogo iniciar, ou depois que o jogo em execução fechar.\n\nSuas escolhas são verificadas de novo antes de cada inicialização e restauradas se algo as tiver alterado. Os arquivos originais de cada Proton são mantidos, então as atualizações da Steam nunca os perdem.</string> + <string name="comp_tag_original">Original</string> + <string name="comp_tag_in_use">Em uso</string> + <string name="comp_tag_new">Novo</string> + <string name="comp_tag_stored">Armazenado</string> + <string name="perf_title">Desempenho</string> + <string name="perf_lede">Núcleos da CPU, correções da sessão e o nome do host. Vale a partir da próxima sessão.</string> + <string name="perf_client_cores_group">Núcleos do cliente Steam</string> + <string name="perf_override">Substituir a escolha de núcleos da Steam</string> + <string name="perf_override_hint">Fixa a Steam, o auxiliar de interface dela e o gamescope nos núcleos abaixo.</string> + <string name="perf_client_cores">Núcleos do cliente</string> + <string name="perf_client_cores_on">Núcleos para a Steam.</string> + <string name="perf_client_cores_off">Ative a substituição para escolher.</string> + <string name="perf_game_cores_group">Núcleos dos jogos</string> + <string name="perf_game_cores">Núcleos dos jogos</string> + <string name="perf_game_cores_hint">Núcleos para os jogos. Com todos selecionados, o Android distribui livremente.</string> + <string name="perf_client_ui">Interface do cliente</string> + <string name="perf_glthread">GL em threads</string> + <string name="perf_glthread_hint">Pode deixar os menus da Steam mais responsivos.</string> + <string name="perf_zink">Zink: descritores sob demanda</string> + <string name="perf_zink_hint">Recomendado para drivers sem buffers de descritores.</string> + <string name="perf_noglerror">Pular verificações de erro do GL</string> + <string name="perf_noglerror_hint">Desativa a validação do GL a cada chamada.</string> + <string name="perf_gsrealtime">gamescope: fila de GPU em tempo real</string> + <string name="perf_gsrealtime_hint">Prioriza o trabalho de GPU do compositor. Pode suavizar o ritmo dos quadros, com algum custo para os jogos.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">Manter a GPU no clock máximo</string> + <string name="perf_gpuclock_hint">Só em Adreno. Menos engasgos, mais gasto de bateria e mais calor.</string> + <string name="perf_fixes">Correções da sessão</string> + <string name="perf_sysmem">Turnip: renderização sysmem</string> + <string name="perf_sysmem_hint">Mais rápido para a Steam e a maioria dos jogos. Obrigatório nas Adreno 710/720/722.</string> + <string name="perf_xalia">Pular o assistente xalia da Steam</string> + <string name="perf_fastsync">droiddeck-ntsync (experimental)</string> + <string name="perf_fastsync_hint">Um substituto em espaço de usuário para o driver ntsync do Linux, para testes. Enquanto estiver ativado, o Proton o usa no lugar do droiddeck-esync e do droiddeck-fsync. Funciona com o Proton Experimental ARM64, o GE-Proton e o proton-cachyos. Vale a partir da próxima sessão.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync primeiro</string> + <string name="perf_fsync_hint">Roda o fsync do próprio Proton nos arquivos originais do Proton, com o futex_waitv fornecido em espaço de usuário, então não precisa de pacote e continua funcionando assim que a Steam atualiza o Proton. Enquanto estiver ativado, os jogos o usam antes do droiddeck-esync, exceto os poucos que o Proton mantém fora do fsync. Vale a partir da próxima sessão.</string> + <string name="perf_esync_hint">As threads do Windows dos jogos esperam umas pelas outras via eventfd e memória compartilhada, em vez de idas e voltas pelo wineserver. Usa um pacote droiddeck-esync feito para aquela versão exata do Proton; os pacotes são obtidos automaticamente. Até existir um para uma nova versão do Proton, o droiddeck-fsync faz o papel dele. Vale a partir da próxima sessão.</string> + <string name="perf_xalia_hint">Desativa o assistente de navegação por controle do Proton, que consome CPU em todos os jogos.</string> + <string name="perf_seccomp">Rodar o proot sem seccomp</string> + <string name="perf_seccomp_hint">Pode corrigir erros de syscall ausente em alguns kernels. Pode reduzir o desempenho.</string> + <string name="perf_fastpath">Caminho rápido do proot</string> + <string name="perf_fastpath_needs_seccomp">Requer o filtro seccomp do proot.</string> + <string name="perf_fastpath_hint">Buscas de arquivos mais rápidas e menos engasgos. Desative se um programa não encontrar os arquivos dele.</string> + <string name="perf_identity">Identidade da sessão</string> + <string name="perf_hostname">Nome do host</string> + <string name="perf_hostname_hint">O nome que a Steam vê para este dispositivo. Em branco, volta a ser %1$s.</string> + <string name="perf_hostname_invalid">Só letras, dígitos e hifens internos, até 63 caracteres.</string> + <string name="perf_phantom_warning">O Android está configurado para encerrar esta sessão</string> + <string name="driver_installed">Instalados</string> + <string name="driver_available">Disponíveis para download</string> + <string name="driver_nothing_new">Nada novo desde a última verificação.</string> + <string name="driver_restore_builtin">Restaurar drivers integrados</string> + <string name="driver_delete_builtin">Integrado ao app: isto remove os arquivos descompactados dele e o oculta. Restaurar drivers integrados o traz de volta.</string> + <string name="driver_delete_bundle">Metade de um pacote Android + Linux: as duas metades são removidas. Um driver em uso volta para o padrão.</string> + <string name="driver_delete_plain">Os arquivos dele são removidos do app.</string> + <string name="driver_delete_in_use">Os arquivos dele são removidos. Ele está em uso, então o padrão assume.</string> + <string name="drawer_page_display">Tela</string> + <string name="drawer_page_effects">Efeitos</string> + <string name="drawer_page_controls">Controles</string> + <string name="drawer_page_components">Componentes</string> + <string name="drawer_page_controller">Controle</string> + <string name="drawer_page_settings">Configurações</string> + <string name="drawer_page_desc">Página %1$s</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Área de trabalho</string> + <string name="drawer_steam_menu">Menu Steam</string> + <string name="drawer_quick_access">Acesso rápido</string> + <string name="drawer_open_qam">Abrir o menu de Acesso rápido</string> + <string name="drawer_prev_page">Página anterior</string> + <string name="drawer_next_page">Próxima página</string> + <string name="drawer_hud">HUD de desempenho</string> + <string name="drawer_scaling">Modo de escala</string> + <string name="drawer_scaling_sharpness">Nitidez da escala</string> + <string name="drawer_effects">Efeitos de tela</string> + <string name="drawer_effects_hint">Em tempo real. Custa um pouco de tempo de GPU enquanto algum efeito estiver ativado.</string> + <string name="drawer_look">Predefinições</string> + <string name="drawer_look_custom">Personalizado</string> + <string name="drawer_effects_advanced">Avançado</string> + <string name="widgets_expanded">Expandido</string> + <string name="widgets_collapsed">Recolhido</string> + <string name="drawer_cas">Nitidez (CAS)</string> + <string name="drawer_cas_level">Nível de nitidez</string> + <string name="drawer_fake_hdr">HDR falso</string> + <string name="drawer_deband">Redução de banding</string> + <string name="drawer_deband_strength">Intensidade da redução de banding</string> + <string name="drawer_brightness">Brilho</string> + <string name="drawer_contrast">Contraste</string> + <string name="drawer_gamma">Gama</string> + <string name="drawer_saturation">Saturação</string> + <string name="drawer_fxaa">FXAA (suavização de bordas)</string> + <string name="drawer_toon">Contornos cartoon</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Filtragem de texturas</string> + <string name="drawer_texture_hint">Jogos DirectX 9–11, a partir da próxima vez que forem iniciados.</string> + <string name="drawer_anisotropy">Filtragem anisotrópica</string> + <string name="drawer_texture_sharpness">Nitidez das texturas</string> + <string name="drawer_texture_sharpness_note">Um mip bias negativo: texturas mais nítidas, com um pouco de cintilação. O Automático acompanha o quanto a sessão é ampliada para a tela.</string> + <string name="drawer_touch_auto">Automático (%1$s)</string> + <string name="drawer_keyboard">Teclado</string> + <string name="drawer_pc_keyboard">Teclado de PC</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">Segunda tela</string> + <string name="drawer_second_screen_mode">Modo</string> + <string name="drawer_second_screen_shows">Mostra</string> + <string name="drawer_android_apps">Apps Android</string> + <string name="drawer_launch_app">Abrir um app</string> + <string name="common_hide">Ocultar</string> + <string name="common_show">Mostrar</string> + <string name="drawer_no_apps">Nenhum app que possa ser aberto</string> + <string name="drawer_session_behavior">Comportamento da sessão</string> + <string name="drawer_suspend_hint">Vale agora e nas próximas sessões neste modo.</string> + <string name="drawer_next_session">Próxima sessão</string> + <string name="drawer_support">Suporte</string> + <string name="drawer_logs">Logs da sessão</string> + <string name="drawer_logs_hint">Anexe a um relatório de bug</string> + <string name="drawer_share_logs">Compartilhar logs</string> + <string name="drawer_background">Segundo plano</string> + <string name="drawer_stop">Encerrar</string> + <string name="drawer_stop_title">Encerrar a sessão?</string> + <string name="drawer_running_now">Em execução agora</string> + <string name="drawer_no_game">Nenhum jogo está rodando em um Proton</string> + <string name="drawer_selected_profile">Perfil do jogo</string> + <string name="drawer_selected_profile_none">Os detalhes do perfil ainda não estão disponíveis.</string> + <string name="drawer_profile_refresh_failed_initial">Não foi possível carregar os perfis de jogos. Tentando de novo automaticamente.</string> + <string name="drawer_profile_refresh_failed_stale">Não foi possível atualizar os perfis de jogos. Mostrando os últimos dados disponíveis; tentando de novo automaticamente.</string> + <string name="drawer_profile_inspect_hint">Veja e edite as configurações salvas usadas na próxima inicialização.</string> + <string name="drawer_profile_default">Usando o Perfil padrão</string> + <string name="drawer_profile_custom">Perfil personalizado</string> + <string name="drawer_component_inherited">Usando %1$s do Perfil padrão.</string> + <string name="drawer_game_components_note">As alterações são salvas no perfil de %1$s e valem a partir da próxima vez que o jogo for iniciado.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d substituição do jogo</item> + <item quantity="many">%1$d substituições do jogo</item> + <item quantity="other">%1$d substituições do jogo</item> + </plurals> + <string name="drawer_no_proton">Nenhum Proton instalado ainda.</string> + <string name="drawer_game_running">jogo em execução</string> + <string name="drawer_reapplied">reaplicado ao iniciar %1$s</string> + <string name="drawer_original">Original</string> + <string name="drawer_components_note">Importe e exclua na página Componentes.</string> + <string name="setup_check_done">Concluído</string> + <string name="setup_check_warn">Requer atenção</string> + <string name="setup_check_busy">Processando</string> + <string name="setup_limit_adb_failed">Verifique o endereço IP e a porta da Depuração por Wi-Fi, ou pareie de novo se o Android removeu este dispositivo.</string> + <string name="setup_install">Instalar</string> + <string name="setup_manage">Gerenciar</string> + <string name="setup_tab_overview">Visão geral</string> + <string name="setup_tab_controller">Controle</string> + <string name="setup_tab_session">Sessão</string> + <string name="setup_tab_launcher">Inicializador</string> + <string name="setup_title">Configuração</string> + <string name="setup_all_set">● Tudo pronto</string> + <string name="setup_n_ready">%1$d de %2$d prontos</string> + <string name="setup_system_check">Verificação do sistema</string> + <string name="setup_gpu_supported">Dispositivo compatível</string> + <string name="setup_gpu_untested">GPU não testada</string> + <string name="setup_gpu_unsupported">GPU sem suporte</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">A Steam requer um driver Adreno; %1$s pode mostrar uma tela preta</string> + <string name="setup_runtime">Runtime do Linux</string> + <string name="setup_runtime_progress">%1$s · %2$d%%</string> + <string name="setup_runtime_missing">Não instalado · cerca de 3 GB, instalado no primeiro Jogar</string> + <string name="setup_installed">Instalado</string> + <string name="setup_runtime_update">%1$s · atualização disponível</string> + <string name="setup_runtime_current">%1$s · atualizado</string> + <string name="setup_limit">Limite de processos filhos</string> + <string name="setup_limit_on">Ativado · o Android pode fechar a Steam. Desativá-lo leva um minuto.</string> + <string name="setup_limit_unset">Não definido · o Android pode fechar a Steam. Desativá-lo leva um minuto.</string> + <string name="setup_limit_unknown">Não foi possível verificar · desativá-lo leva um minuto.</string> + <string name="setup_fix_it">Corrigir</string> + <string name="setup_details">Detalhes</string> + <string name="setup_dev_options">Opções do desenvolvedor</string> + <string name="setup_use_wireless">Usar a Depuração por Wi-Fi</string> + <string name="setup_check_again">Verificar de novo</string> + <string name="setup_limit_turn_on">Ativar o limite</string> + <string name="setup_account">Conta Steam</string> + <string name="setup_signed_in_offline">Sessão iniciada como %1$s · modo offline</string> + <string name="setup_signed_in">Sessão iniciada como %1$s</string> + <string name="setup_sign_in">Pressione Jogar e inicie sessão na Steam</string> + <string name="setup_tools">Ferramentas</string> + <string name="setup_controller">Controle</string> + <string name="setup_controller_unavailable">As configurações de controle não estão disponíveis.</string> + <string name="setup_session">Sessão</string> + <string name="setup_logs">Logs da sessão</string> + <string name="setup_logs_hint">Salvos após cada sessão</string> + <string name="setup_agent_commands">Ferramentas de depuração</string> + <string name="setup_latest_logs">Logs da última sessão</string> + <string name="setup_saved_logs">Logs de sessão salvos</string> + <string name="setup_saved_logs_hint">Mantém os %1$d mais recentes</string> + <string name="setup_clear_logs">Limpar logs</string> + <string name="setup_offline">Modo offline</string> + <string name="setup_sign_in_first">Inicie sessão na Steam primeiro</string> + <string name="setup_launcher">Inicializador</string> + <string name="setup_language">Idioma</string> + <string name="setup_language_hint">A Steam também o usa, a partir da próxima vez que iniciar.</string> + <string name="setup_language_system">Padrão do sistema (%1$s)</string> + <string name="setup_language_system_short">Padrão do sistema</string> + <string name="setup_app_scale">Escala do app</string> + <string name="setup_app_scale_hint">Texto e controles do DroidDeck</string> + <string name="setup_app_scale_default">%1$d%% (padrão)</string> + <string name="setup_theme">Tema</string> + <string name="setup_theme_hint">Aparência do inicializador</string> + <string name="setup_home">Usar como tela inicial</string> + <string name="setup_home_on">O DroidDeck pode ser o app de início</string> + <string name="setup_home_off">O DroidDeck nunca é oferecido como app de início</string> + <string name="setup_orientation">Orientação</string> + <string name="setup_orientation_auto">Paisagem automática</string> + <string name="setup_orientation_landscape">Paisagem</string> + <string name="setup_orientation_reverse">Paisagem invertida</string> + <string name="setup_fullscreen">Tela cheia</string> + <string name="setup_fullscreen_on">Oculta as barras de status e de navegação do Android</string> + <string name="setup_fullscreen_off">Mostra as barras de status e de navegação do Android</string> + <string name="setup_default_home">App de início padrão</string> + <string name="setup_choose_home">Escolher um app de início</string> + <string name="setup_choose">Escolher</string> + <string name="setup_linux_apps">Apps Linux (beta)</string> + <string name="setup_tool_files">Arquivos</string> + <string name="setup_tool_files_hint">Navegue e gerencie arquivos</string> + <string name="setup_tool_protons">Versões do Proton</string> + <string name="setup_tool_protons_hint">Instale versões ARM64 do Proton</string> + <string name="setup_tool_performance">Desempenho</string> + <string name="setup_tool_performance_hint">Atribuição de núcleos da CPU</string> + <string name="setup_tool_roms">Pasta de ROMs</string> + <string name="setup_tool_roms_hint">Onde os jogos de emuladores ficam armazenados</string> + <string name="setup_card_components">Componentes</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Controles</string> + <string name="setup_card_controls_hint">Mapeamento de botões</string> + <string name="store_tab_discover">Descobrir</string> + <string name="store_tab_search">Buscar</string> + <string name="store_tab_installed">Instalados</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Loja</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak pronto</string> + <string name="store_needs_runtime">Instale primeiro o runtime do Linux em Configuração.</string> + <string name="store_setup_title">Configurar o Flatpak</string> + <string name="store_setup_text">Adiciona o Flatpak e o conecta ao Flathub (cerca de 20 MB). O primeiro app também baixa o runtime dele.</string> + <string name="store_setting_up">Configurando…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Atualizações</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d%%</string> + <string name="store_starting">Iniciando…</string> + <string name="store_categories">Categorias</string> + <string name="store_unreachable">Não foi possível acessar o Flathub. Verifique sua conexão e tente novamente.</string> + <string name="store_try_again">Tentar novamente</string> + <string name="store_search_label">Buscar no Flathub</string> + <string name="store_search_placeholder">Firefox, jogos, emuladores…</string> + <string name="store_search">Buscar</string> + <string name="store_all">Todos</string> + <string name="store_searching">Buscando…</string> + <string name="store_search_hint">Busque pelo nome ou escolha uma categoria. Só apps ARM64 são listados.</string> + <string name="store_no_results">Nenhum app ARM64 no Flathub corresponde a isso.</string> + <string name="store_results">Resultados</string> + <string name="store_check_updates">Verificar atualizações</string> + <string name="store_update_all">Atualizar tudo (%1$d)</string> + <string name="store_all_current">● Tudo está atualizado</string> + <string name="store_no_apps">Nenhum app ainda. Encontre alguns em Descobrir ou Buscar.</string> + <string name="store_setup_first_note">Configure o Flatpak para instalar apps.</string> + <string name="store_update_available_prefix">Atualização disponível · %1$s</string> + <string name="store_open">Abrir</string> + <string name="store_details">Detalhes</string> + <string name="store_remove">Remover</string> + <string name="store_press_again">Pressione de novo para remover</string> + <string name="store_installing">Instalando…</string> + <string name="store_install">Instalar</string> + <string name="store_setup_first">Configure o Flatpak primeiro</string> + <string name="store_download_size">Download de %1$s</string> + <string name="store_installed_chip">● Instalado</string> + <string name="store_details_failed">Não foi possível carregar a página deste app no Flathub.</string> + <string name="store_loading">Carregando do Flathub…</string> + <string name="store_no_arm">Não há versão ARM64 no Flathub, então não é possível instalar aqui.</string> + <string name="store_screenshots">Capturas de tela</string> + <string name="store_about">Sobre</string> + <string name="store_app_id">ID do app</string> + <string name="store_license">Licença</string> + <string name="store_runtime">Runtime</string> + <string name="store_installed_size">Tamanho instalado</string> + <string name="store_installed_size_value">%1$s (mais o runtime compartilhado)</string> + <string name="store_website">Site</string> + <string name="store_verified">Verificado</string> + <string name="upd_title">Atualizações</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Verificado %1$s</string> + <string name="upd_not_checked">Ainda não verificado</string> + <string name="upd_perm_title">Permita que o DroidDeck se atualize</string> + <string name="upd_perm_text">Na próxima tela, ative Permitir desta fonte e volte. A atualização continua sozinha.</string> + <string name="upd_open_settings">Abrir configurações</string> + <string name="upd_not_now">Agora não</string> + <string name="upd_signed_status">Assinatura diferente</string> + <string name="upd_signed_headline">Não é possível atualizar por cima</string> + <string name="upd_signed_detail">Esta cópia não está assinada com a chave de lançamento do DroidDeck, então as atualizações não podem ser instaladas por cima dela. Desinstale-a para trocar.</string> + <string name="upd_available">Atualização disponível</string> + <string name="upd_blocked_status">Não é possível instalar</string> + <string name="upd_switch">Pronto para trocar</string> + <string name="upd_ahead_status">À frente da Estável</string> + <string name="upd_ahead_headline">Você está à frente da Estável</string> + <string name="upd_ahead_detail">Esta versão é mais nova que a Estável mais recente. Você passará para a Estável no próximo lançamento dela.</string> + <string name="upd_gone_status">Teste encerrado</string> + <string name="upd_gone_headline">Este teste foi encerrado</string> + <string name="upd_gone_detail">A correção dele foi incorporada ou descartada. Siga a Prévia para ter as correções mais recentes.</string> + <string name="upd_current_status">Atualizado</string> + <string name="upd_current_headline">Você tem a versão mais recente: %1$s</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Atualizar%1$s</string> + <string name="upd_install_button">Instalar%1$s</string> + <string name="upd_install_anyway">Instalar a Estável %1$s mesmo assim</string> + <string name="upd_follow_preview">Seguir a Prévia</string> + <string name="upd_stop_session">Encerre a sessão em execução para atualizar.</string> + <string name="upd_less">Menos</string> + <string name="upd_more">Mais</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Nova versão Prévia</string> + <string name="upd_new_test">Versão de teste do PR #%1$d</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">CANAL DE ATUALIZAÇÃO</string> + <string name="upd_stable">Estável</string> + <string name="upd_stable_hint">Versões testadas, para a maioria das pessoas</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Prévia</string> + <string name="upd_preview_hint">As correções mais recentes, antes da Estável</string> + <string name="upd_preview_current">Atualizado</string> + <string name="upd_tests">Versões de teste</string> + <string name="upd_tests_hint">Experimente uma correção antes do lançamento</string> + <string name="upd_tests_none">Nenhuma no momento</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Não compilada para esta cópia do DroidDeck</string> + <string name="upd_updated">Atualizado %1$s</string> + <string name="upd_test_name">teste do PR #%1$d</string> + <string name="upd_while_ago">há algum tempo</string> + <string name="upd_just_now">agora mesmo</string> + <plurals name="upd_tests_count"> + <item quantity="one">%1$d para testar</item> + <item quantity="many">%1$d para testar</item> + <item quantity="other">%1$d para testar</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="one">Mudanças desde a sua versão · %1$d versão atrás</item> + <item quantity="many">Mudanças desde a sua versão · %1$d versões atrás</item> + <item quantity="other">Mudanças desde a sua versão · %1$d versões atrás</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="one">Mudanças recentes · %1$d versão</item> + <item quantity="many">Mudanças recentes · %1$d versões</item> + <item quantity="other">Mudanças recentes · %1$d versões</item> + </plurals> + <string name="gate_title">Uma configuração do Android para mudar</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">O Android interrompe apps que iniciam muitos processos em segundo plano, e a Steam inicia dezenas. Até isso mudar, os jogos fecham sem mostrar erro.</string> + <string name="gate_fix_for_me">Corrigir para mim</string> + <string name="gate_toggle_steps">Ative “Desativar restrições de processos filhos” e volte. Deixe as Opções do desenvolvedor ativadas, ou a chave volta a ser desativada.</string> + <string name="gate_open_dev">Abrir as Opções do desenvolvedor</string> + <string name="gate_no_toggle">Esta versão do Android não tem uma chave para isso, então o DroidDeck faz a mudança pela Depuração por Wi-Fi. Leva cerca de um minuto, sem precisar de computador.</string> + <string name="gate_needs_wifi">A Depuração por Wi-Fi requer Wi-Fi. Qualquer rede serve; não precisa de internet.</string> + <string name="gate_needs_notifications">O código de pareamento é digitado em uma notificação. Permita notificações do DroidDeck ou use Outras formas.</string> + <string name="gate_allow_notifications">Permitir notificações</string> + <string name="gate_step1">Abra Configurações → Opções do desenvolvedor → Depuração por Wi-Fi e ative</string> + <string name="gate_step2">Toque em “Parear o dispositivo com um código de pareamento”</string> + <string name="gate_step3">Digite o código na notificação do DroidDeck</string> + <string name="gate_waiting">Aguardando o pop-up de pareamento…</string> + <string name="gate_code_needed">Pop-up encontrado. Digite o código dele na notificação.</string> + <string name="gate_status">Status</string> + <string name="gate_polling">Verificando a cada 2 segundos. Esta página fecha quando o limite for desativado.</string> + <string name="gate_other_ways">Outras formas</string> + <string name="gate_other_toggle">Depuração por Wi-Fi, endereço manual, ADB pelo computador</string> + <string name="gate_other_no_toggle">Endereço manual, ADB pelo computador, Opções do desenvolvedor</string> + <string name="gate_use_wireless_instead">Usar a Depuração por Wi-Fi</string> + <string name="gate_some_roms">Algumas ROMs adicionam a chave mesmo assim. Procure “processos filhos” nas Opções do desenvolvedor.</string> + <string name="gate_manual_hint">Se a notificação não funcionar, digite você mesmo o endereço e o código do pop-up.</string> + <string name="gate_enter_manually">Digitar endereço manualmente</string> + <string name="gate_from_computer">De um computador com ADB:</string> + <string name="gate_copy_command">Copiar comando</string> + <string name="gate_choose_display">Escolha em qual tela abrir as Configurações do Android.</string> + <string name="gate_main_screen">Tela principal</string> + <string name="wadb_finding_port">Pareado. Procurando a porta da Depuração por Wi-Fi…</string> + <string name="wadb_enter_port">Pareado. Digite a porta mostrada nas Configurações.</string> + <string name="wadb_applying">Dispositivo encontrado. Aplicando a configuração…</string> + <string name="wadb_apply_failed">Não foi possível aplicar automaticamente. Verifique o endereço abaixo e tente de novo. %1$s</string> + <string name="wadb_title_wireless">Depuração por Wi-Fi</string> + <string name="wadb_title_done">Limite de processos filhos atualizado</string> + <string name="wadb_title">Alterar o limite de processos filhos</string> + <string name="wadb_lede_compact0">Se a opção não existir, pareie pela Depuração por Wi-Fi.</string> + <string name="wadb_lede_compact1">Pareado. Verifique o endereço IP e a porta na Depuração por Wi-Fi.</string> + <string name="wadb_lede_done">O limite de processos filhos foi atualizado.</string> + <string name="wadb_lede0">Use a etapa 2 se as Opções do desenvolvedor não tiverem a configuração de processos filhos. Abra Opções do desenvolvedor → Depuração por Wi-Fi → Parear o dispositivo com um código de pareamento.</string> + <string name="wadb_lede1">Pareado. Use o endereço abaixo ou atualize-o em Depuração por Wi-Fi → Endereço IP e porta.</string> + <string name="wadb_lede_done_long">O limite de processos filhos foi atualizado pela Depuração por Wi-Fi.</string> + <string name="wadb_confirmed_on">O Android confirmou que o limite está ativado.</string> + <string name="wadb_confirmed_off">O Android confirmou que o limite está desativado.</string> + <string name="wadb_pair_group">2 · Parear pela Depuração por Wi-Fi</string> + <string name="wadb_pair_address">Endereço IP e porta do pop-up de pareamento</string> + <string name="wadb_pin">Código de pareamento (PIN)</string> + <string name="wadb_pin_placeholder">6 dígitos</string> + <string name="wadb_pair">Parear</string> + <string name="wadb_apply_group">2 · Aplicar pela Depuração por Wi-Fi</string> + <string name="wadb_connect_address">Endereço IP e porta de conexão</string> + <string name="wadb_pair_again">Parear de novo</string> + <string name="wadb_turn_off">Desativar o limite</string> + <string name="wadb_other_ways">Outras formas de alterar</string> + <string name="wadb_dev_try_first">1 · Opções do desenvolvedor (tente primeiro)</string> + <string name="wadb_restrict_on">Ative “Restringir processos filhos”.</string> + <string name="wadb_restrict_off">Desative “Restringir processos filhos”.</string> + <string name="wadb_computer_last">3 · ADB pelo computador (último recurso)</string> + <string name="wadb_only_if">Só se a Depuração por Wi-Fi não estiver disponível.</string> + <string name="wadb_computer_group">3 · ADB pelo computador (último recurso)</string> + <string name="ctrl_osc_hint">Quando os controles de toque aparecem em uma sessão</string> + <string name="ctrl_color">Cor</string> + <string name="ctrl_color_hint">Tom dos botões na tela</string> + <string name="ctrl_custom">Personalizada</string> + <string name="ctrl_opacity">Opacidade</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">Tamanho dos botões</string> + <string name="ctrl_size_hint" formatted="false">100% é o tamanho padrão</string> + <string name="ctrl_stick_click">Clique do analógico</string> + <string name="ctrl_stick_click_hint">Toque duas vezes e segure um analógico para L3 ou R3</string> + <string name="ctrl_adaptive">Analógicos adaptáveis</string> + <string name="ctrl_rumble">Vibração do jogo</string> + <string name="ctrl_steam_button">Botão Steam</string> + <string name="ctrl_qam_button">Botão QAM</string> + <string name="ctrl_keyboard_button">Botão do teclado</string> + <string name="ctrl_select_steam">Pressione Select para abrir o menu Steam</string> + <string name="ctrl_select_steam_hint">Em vez de View/Select.</string> + <string name="setup_animations">Animações</string> + <string name="ctrl_adaptive_hint">Os analógicos aparecem onde você toca e somem ao soltar</string> + <string name="ctrl_layout">Layout</string> + <string name="ctrl_layout_custom">Posições personalizadas salvas</string> + <string name="ctrl_layout_default">Ajustado a esta tela</string> + <string name="ctrl_edit">Editar</string> + <string name="ctrl_reset">Redefinir</string> + <string name="ctrl_mapping">Mapeamento de botões</string> + <string name="ctrl_mapping_default">Cada botão envia o próprio comando</string> + <string name="ctrl_mapping_count">%1$d de %2$d botões remapeados</string> + <string name="ctrl_configure">Configurar</string> + <string name="ctrl_reset_all">Redefinir controle</string> + <string name="ctrl_reset_all_hint">Restaura todas as configurações do controle e o layout</string> + <string name="ctrl_mapping_lede">O que cada botão na tela envia ao jogo. Oculto remove o botão.</string> + <string name="ctrl_onscreen_buttons">Botões na tela</string> + <string name="ctrl_defaults">Padrões</string> + <string name="ctrl_reset_mapping">Redefinir mapeamento</string> + <string name="content_android_apps">Apps Android</string> + <string name="content_apps_count">%1$d apps</string> + <string name="content_no_android_apps">Nenhum app Android que possa ser aberto foi encontrado.</string> + <string name="content_desktop_installed">● Área de trabalho instalada</string> + <string name="content_desktop_first_open">Instalada ao abrir pela primeira vez</string> + <string name="content_available">Disponível para instalar</string> + <string name="content_not_installed">Não instalado.</string> + <string name="content_not_installed_suffix">%1$s · não instalado</string> + <string name="content_open_named">Abrir %1$s</string> + <string name="content_games">Jogos</string> + <string name="content_choose_roms">Escolha uma pasta de ROMs.</string> + <string name="content_retroarch_roms">Navegue até /root/ROMs no RetroArch.</string> + <string name="content_add_roms">Adicione jogos de %1$s em ROMs/%2$s.</string> + <string name="content_install_named">Instalar %1$s</string> + <string name="content_stop_to_install">Encerre a sessão para instalar</string> + <string name="content_runtime_required">Runtime necessário</string> + <string name="content_loading_details">Carregando detalhes da instalação…</string> + <string name="content_details_unavailable">Os detalhes da instalação não estão disponíveis. Tente novamente mais tarde.</string> + <string name="content_needs_runtime">Instale primeiro o runtime do Linux em Configuração.</string> + <string name="content_stop_first">Encerre a sessão em execução para instalar apps da área de trabalho.</string> + <string name="content_install_into">Instala %1$s na área de trabalho Linux.</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">Ocultar tira o item da Área de trabalho. Os arquivos dele continuam no runtime do Linux.</string> + <string name="content_rom_gone">Esse jogo não está mais na pasta de ROMs.</string> + <string name="content_desktop_system">Área de trabalho · %1$s</string> + <string name="content_launch_in">Abrir em %1$s</string> + <string name="content_also_in">Também em %1$s</string> + <string name="content_page_gone">Essa página não existe mais. Escolha uma seção à esquerda.</string> + <string name="content_open_desktop">Abrir área de trabalho</string> + <string name="content_install_open_desktop">Instalar e abrir área de trabalho</string> + <string name="desktop_remove_title">Desinstalar área de trabalho</string> + <string name="desktop_remove_text">Remove a área de trabalho e os apps incluídos nela. Mantém jogos, apps adicionados, configurações e bibliotecas compartilhadas.</string> + <string name="desktop_removing">Removendo a área de trabalho</string> + <string name="desktop_removed">Área de trabalho removida</string> + <string name="desktop_cleaning_legacy">Removendo arquivos antigos do LXQt</string> + <string name="desktop_removal_unknown">Atualize o DroidDeck antes de remover esta versão da área de trabalho.</string> + <string name="content_linux_desktop">Área de trabalho Linux</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox e seus emuladores</string> + <string name="games_empty">Os jogos que você instala na Steam aparecem aqui após a sessão.</string> + <string name="games_play_steam">Jogar na Steam</string> + <string name="games_launch_settings">Configurações de inicialização</string> + <string name="games_launch">Iniciar</string> + <string name="games_files">Arquivos do jogo</string> + <string name="games_prefix">Prefixo do Proton</string> + <string name="games_saves_looking">Procurando jogos salvos…</string> + <string name="games_saves_none">Nenhuma pasta de jogos salvos encontrada pelo nome. A exportação inclui toda a pasta do usuário.</string> + <string name="games_saves_folders">%1$d pastas de jogos salvos</string> + <string name="games_saves_summary">%1$s · %2$d arquivos · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">Gerenciar jogos salvos</string> + <string name="games_saves">Jogos salvos</string> + <string name="games_import_saves">Importar jogos salvos…</string> + <string name="games_export_gamehub">Exportar como zip do GameHub…</string> + <string name="games_export_winlator">Exportar como zip do Winlator…</string> + <string name="games_open_save_folder">Abrir pasta de jogos salvos</string> + <string name="games_never_played">Nunca jogado</string> + <string name="games_runtime_busy_percent">Runtime ocupado · %1$d%%</string> + <string name="games_runtime_busy">Runtime ocupado</string> + <string name="games_runtime_first_play">O runtime é instalado no primeiro Jogar</string> + <string name="games_runtime_update">Atualização do runtime disponível</string> + <string name="games_runtime_ready">● Runtime pronto</string> + <string name="games_last_played">Jogado por último: %1$s</string> + <string name="games_internal">Armazenamento interno</string> + <string name="games_added">Jogo adicionado</string> + <string name="games_custom">Jogo personalizado</string> + <string name="games_store_storage">Armazenamento de jogos</string> + <string name="gpu_auto_hint">O melhor par para esta GPU, sempre atualizado</string> + <string name="gpu_manual_hint">Escolha um par você mesmo</string> + <string name="gpu_checking">Procurando drivers…</string> + <string name="gpu_none_listed">Nenhum driver listado ainda. Atualize para procurá-los.</string> + <string name="gpu_hide_other">Ocultar drivers de outras GPUs</string> + <string name="gpu_show_other">Mostrar drivers de outras GPUs</string> + <string name="gpu_advanced">Avançado</string> + <string name="gpu_set_with_display">Definido junto com o driver de exibição: %1$s contém os dois.</string> + <string name="gpu_set_with_runtime">Definido junto com o driver do runtime: %1$s contém os dois.</string> + <string name="gpu_runtime_hint">Usado pelos jogos e pela área de trabalho.</string> + <string name="gpu_runtime_default">Padrão do runtime</string> + <string name="gpu_display_hint">Desenha os quadros na tela. Vale depois que o DroidDeck reiniciar.</string> + <string name="gpu_auto_by_gpu">Automático - escolhido pela GPU</string> + <string name="gpu_manage">Baixar, importar ou excluir…</string> + <string name="gpu_in_use_caps">EM USO</string> + <string name="gpu_builtin">Os drivers integrados ao DroidDeck</string> + <string name="gpu_matched_pair">Drivers do runtime e de exibição definidos juntos como um par compatível.</string> + <string name="gpu_recommended_for">Recomendado para esta GPU: %1$s.</string> + <string name="gpu_refresh_hint">Atualize para procurar drivers para esta GPU.</string> + <string name="gpu_recommended_caps">RECOMENDADO</string> + <string name="gpu_unavailable">Indisponível</string> + <string name="load_runtime">Runtime do Linux</string> + <string name="load_runtime_detail">Preparando o runtime do Linux</string> + <string name="load_session">Sessão</string> + <string name="load_session_detail">Iniciando a tela e o áudio</string> + <string name="load_steam">Cliente Steam</string> + <string name="load_steam_detail">Verificando a Steam e as ferramentas de compatibilidade dela</string> + <string name="load_opening_steam">Abrindo a Steam</string> + <string name="load_opening_steam_detail">Aguardando o primeiro quadro da Steam</string> + <string name="load_desktop">Área de trabalho</string> + <string name="load_desktop_detail">Preparando a área de trabalho</string> + <string name="load_opening">Abrindo</string> + <string name="load_opening_detail">Aguardando o primeiro quadro</string> + <string name="load_starting_steam">Iniciando o Steam</string> + <string name="load_ended">A sessão terminou</string> + <string name="load_step">Etapa %1$d de %2$d</string> + <string name="load_steam_paused">O Steam está pausado</string> + <string name="deck_tab_grips">Botões traseiros</string> + <string name="deck_tab_left">Lado esquerdo</string> + <string name="deck_tab_right">Lado direito</string> + <string name="deck_tab_both">Ambos</string> + <string name="deck_tab_gamepad">Controle</string> + <string name="deck_surface_stick">Analógico</string> + <string name="deck_surface_trackpad">Trackpad</string> + <string name="deck_surface_switch">Alternar entre analógico e trackpad</string> + <string name="deck_more_controls">Mais controles do Deck</string> + <string name="deck_panels">PAINÉIS</string> + <string name="deck_close">Fechar controles da segunda tela</string> + <string name="deck_open_steam_menu">Abrir o menu Steam</string> + <string name="deck_left_upper">Esquerdo · superior</string> + <string name="deck_left_lower">Esquerdo · inferior</string> + <string name="deck_right_upper">Direito · superior</string> + <string name="deck_right_lower">Direito · inferior</string> + <string name="deck_click">Clique</string> + <string name="deck_right_trackpad">Trackpad direito</string> + <string name="deck_left_trackpad">Trackpad esquerdo</string> + <string name="second_trackpad">Trackpad</string> + <string name="second_show_keyboard">Mostrar teclado</string> + <string name="second_hide_keyboard">Ocultar teclado</string> + <string name="second_terminal">Terminal Linux</string> + <string name="second_terminal_not_ready">A sessão Linux ainda não está pronta para um terminal.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Jogos</string> + <string name="rail_desktop">Desktop</string> + <string name="rail_store">Loja</string> + <string name="rail_components">Componentes</string> + <string name="rail_apps">Apps</string> + <string name="rail_setup">Configuração</string> + <string name="rail_updates">Atualizações</string> + <string name="rail_percent">%1$d%%</string> + <string name="rail_resume_named">Retomar %1$s</string> + <string name="roms_text">Aparece como /root/ROMs em cada sessão. O armazenamento interno fica em /root/Storage.</string> + <string name="roms_none">Nenhuma pasta escolhida</string> + <string name="roms_applies">Vale a partir da próxima sessão. Cartões SD funcionam.</string> + <string name="roms_choose">Escolher pasta</string> + <string name="roms_change">Trocar pasta</string> + <string name="proton_lede">Instale uma versão ARM64 do Proton e escolha-a para cada jogo em Steam → Propriedades → Compatibilidade.</string> + <string name="proton_stop_first">Pare a sessão em execução para instalar ou remover uma ferramenta de compatibilidade.</string> + <string name="proton_available">Versões disponíveis</string> + <string name="proton_installed">Instalado: %1$s</string> + <string name="proton_queued">Pendente de uma solicitação anterior</string> + <string name="proton_not_installed">Não instalado</string> + <string name="proton_update_available">Atualização disponível → %1$s</string> + <string name="proton_remove_older">Remover versões antigas do %1$s</string> + <string name="proton_remove_older_body">Remove %1$s. Jogos configurados com essas versões precisam passar para outro Proton.</string> + <string name="proton_remove_body">Jogos configurados com esta versão precisam passar para outro Proton.</string> + <string name="proton_install_now">Instalar agora</string> + <string name="proton_cancel_queued">Cancelar pendente</string> + <string name="pckb_android">Teclado do Android</string> + <string name="pckb_hide">Ocultar ✕</string> + <string name="main_launch_named">Iniciar %1$s</string> + <string name="main_choose_screen">Escolha uma tela. Secundária: %1$s.</string> + <string name="main_secondary_gone">A tela secundária não está mais disponível.</string> + <string name="main_primary">Tela principal</string> + <string name="main_secondary">Tela secundária</string> + <string name="widgets_prev_tab">Aba anterior</string> + <string name="widgets_next_tab">Próxima aba</string> + <string name="widgets_on">Ativado</string> + <string name="widgets_all_n">Todos (%1$d)</string> + <string name="widgets_none">Nenhum</string> + <string name="widgets_n_of_m">%1$d de %2$d</string> + <string name="widgets_off">Desativado</string> + <string name="screen_bottom">Tela inferior</string> + <string name="game_env_applies">Vale a partir da próxima vez que o jogo for iniciado. O perfil de um jogo tem prioridade sobre o compartilhado.</string> + <string name="game_env_other">Outro jogo pelo ID do app…</string> + <string name="game_env_done">Concluído</string> + <string name="game_env_default">Padrão</string> + <string name="game_env_off">Desativado</string> + <string name="game_env_change">Alterar…</string> + <string name="game_env_change_title">Alterar variável</string> + <string name="game_env_restore_default">Voltar ao padrão</string> + <string name="game_env_eyebrow">Jogos no Proton</string> + <string name="pip_title">Picture-in-picture</string> + <string name="pip_auto">Entrar em PiP automaticamente</string> + <string name="unfolded_controls">Controles com a tela desdobrada</string> + <string name="unfolded_controls_hint">Desdobrado, o jogo fica acima dos trackpads e dos botões traseiros do Steam Deck. Dobrado, o jogo fica em tela cheia.</string> + <string name="pip_suspended">Suspenso</string> + <string name="pip_not_ready">O picture-in-picture fica disponível quando a sessão estiver pronta.</string> + <string name="pip_permission">Permita o picture-in-picture para o DroidDeck nas configurações do Android.</string> + <string name="pip_unavailable">O Android não conseguiu abrir o picture-in-picture.</string> + <string name="game_shortcut">Atalho</string> + <string name="game_shortcut_add">Adicionar à tela inicial</string> + <string name="game_shortcut_choose">Escolher jogo</string> + <string name="game_shortcut_scanning">Verificando os jogos instalados…</string> + <string name="game_link_copy">Copiar link de inicialização</string> + <string name="game_link_copied">Link de inicialização copiado</string> + <string name="game_shortcut_unsupported">Este launcher não pode adicionar atalhos. Use Copiar link de inicialização.</string> + <string name="game_link_invalid">Link de inicialização de jogo inválido.</string> + <string name="game_link_missing">Este jogo não está mais na sua biblioteca do DroidDeck.</string> + <string name="game_link_session_busy">Pare a sessão atual antes de iniciar um jogo do Steam.</string> + <string name="game_link_failed">Não foi possível enviar o jogo ao Steam. Tente iniciá-lo novamente.</string> + <string name="setup_retry_removal">Tentar remover de novo</string> + <string name="runtime_removing">Removendo</string> + <string name="runtime_removal_incomplete">Remoção incompleta</string> + <string name="game_file_export">Exportar arquivo do jogo</string> + <string name="game_file_sync">Sincronizar todos os arquivos de jogo com a pasta</string> + <string name="game_file_stop_sync">Parar de sincronizar arquivos de jogo</string> + <string name="game_file_folder">Escolha uma pasta para os arquivos de jogo do frontend</string> + <string name="game_file_exported">Arquivos de jogo salvos em %1$s</string> + <string name="game_frontend_files">Arquivos do frontend</string> + + <string name="main_wait_runtime">Aguarde a operação do runtime terminar</string> + <string name="main_wait_proton_install">Aguarde a instalação da ferramenta de compatibilidade terminar</string> + <string name="main_wait_desktop_install">Aguarde a instalação do app de área de trabalho terminar</string> + <string name="main_wait_install_game">Aguarde a instalação terminar antes de iniciar um jogo</string> + <string name="main_saves_close_session">Feche a sessão do Steam primeiro, para que o jogo não salve por cima da importação</string> + <string name="main_saves_importing">Importando para %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="one">%1$d arquivo importado de %2$s para %3$s</item> + <item quantity="many">%1$d arquivos importados de %2$s para %3$s</item> + <item quantity="other">%1$d arquivos importados de %2$s para %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">%1$d arquivo importado de %2$s para %3$s. Backup dos saves antigos em Download/DroidDeck/Saves/backups</item> + <item quantity="many">%1$d arquivos importados de %2$s para %3$s. Backup dos saves antigos em Download/DroidDeck/Saves/backups</item> + <item quantity="other">%1$d arquivos importados de %2$s para %3$s. Backup dos saves antigos em Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Escolha um zip de saves para %1$s</string> + <string name="main_saves_exporting">Exportando %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="one">%1$d arquivo exportado como %2$s: %3$s</item> + <item quantity="many">%1$d arquivos exportados como %2$s: %3$s</item> + <item quantity="other">%1$d arquivos exportados como %2$s: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Escolha onde salvar %1$s (%2$s)</string> + <string name="main_action_failed">Falha (%1$s): %2$s</string> + <string name="main_no_logs_yet">Ainda não há logs de sessão: inicie uma sessão primeiro.</string> + <string name="main_no_logs_to_clear">Não há logs de sessão para limpar.</string> + <plurals name="main_logs_cleared"> + <item quantity="one">%1$d pasta de logs de sessão limpa.</item> + <item quantity="many">%1$d pastas de logs de sessão limpas.</item> + <item quantity="other">%1$d pastas de logs de sessão limpas.</item> + </plurals> + <string name="main_wadb_pair_failed">Falha no pareamento da depuração por Wi-Fi</string> + <string name="main_wadb_command_failed">Falha no comando de depuração por Wi-Fi</string> + <string name="main_wadb_unavailable">A depuração por Wi-Fi não está disponível</string> + <string name="main_adb_copied">Comando ADB copiado</string> + <string name="main_pick_roms">Escolha a pasta de ROMs</string> + <string name="main_non_adreno_title">Não é uma GPU Adreno</string> + <string name="main_non_adreno_text">O Turnip é compatível com GPUs Adreno. Com %1$s, o Steam pode mostrar uma tela preta. Download: %2$.0f MB.</string> + <string name="main_install_anyway">Instalar mesmo assim</string> + <string name="main_remove_runtime_title">Remover runtime Linux</string> + <string name="main_remove_runtime_text">Isso exclui o runtime, o cliente Steam dentro dele e todos os jogos instalados nele.</string> + <string name="main_applied">Aplicado: %1$s</string> + <string name="android_app_open_failed_primary">Não foi possível abrir %1$s na tela principal</string> + <string name="android_app_open_failed_on">Não foi possível abrir %1$s em %2$s</string> + <string name="android_app_open_failed_display">Não foi possível abrir %1$s na tela %2$d</string> + <string name="comp_busy_importing">Importando</string> + <string name="comp_busy_swapping">Trocando</string> + <string name="comp_busy_restoring">Restaurando</string> + <string name="comp_busy_cancelling">Cancelando</string> + <string name="comp_busy_deleting">Excluindo</string> + <string name="comp_cannot_read">não foi possível ler o arquivo</string> + <string name="comp_imported">%1$s importado</string> + <string name="comp_swap_cancelled">A troca pendente foi cancelada.</string> + <string name="comp_pick_wcp">Escolha um pacote de componente (-linux .wcp)</string> + <string name="comp_nightlies_unreachable">Não foi possível acessar as Nightlies</string> + <string name="comp_stored">%1$s armazenado</string> + <string name="comp_download_failed">Falha no download: %1$s</string> + <string name="drivers_pick_linux">Escolha um driver do runtime Linux (zip -Linux ou pacote Android + Linux)</string> + <string name="drivers_pick_android">Escolha um driver de vídeo (zip do AdrenoTools ou pacote Android + Linux)</string> + <string name="drivers_pick_any">Escolha um zip de driver ou um pacote Android + Linux</string> + <string name="main_pkg_problem">%1$s: %2$s</string> + <string name="main_removing_named">Removendo %1$s…</string> + <string name="main_forgetting_named">Esquecendo %1$s…</string> + <string name="main_pick_game_storage">Escolha a pasta de armazenamento dos jogos</string> + <string name="main_pick_decky_zip">Escolha um ZIP de plugin do Decky</string> + <string name="main_storage_not_usable">Não utilizável: %1$s</string> + <string name="main_running_program">Programa</string> + <string name="main_removing_runtime">Removendo o runtime Linux</string> + <string name="main_settings_main_screen">Não foi possível abrir as Configurações na tela inferior; elas foram abertas na tela principal.</string> + <string name="main_open_dev_options">Abra as Opções do desenvolvedor nas Configurações</string> + + <string name="upd_unknown_apps_failed">Não foi possível abrir a configuração “Instalar apps desconhecidos” do Android</string> + <string name="upd_couldnt_check">Não foi possível verificar: %1$s</string> + <string name="upd_no_download">Esta versão não tem download para esta cópia do DroidDeck</string> + <string name="upd_stop_first">Pare a sessão em execução primeiro e depois atualize</string> + <string name="upd_stage_downloading">Baixando</string> + <string name="upd_stage_installing">Instalando</string> + <string name="upd_stage_confirm">Aguardando sua confirmação</string> + <string name="upd_failed">A atualização falhou</string> + <string name="upd_not_offered">O Android não ofereceu a instalação da atualização</string> + + <string name="decky_install_failed">Falha ao instalar o Decky: %1$s</string> + <string name="decky_plugin_starting">Iniciando a importação do plugin…</string> + <string name="decky_plugin_failed">Falha ao importar o plugin: %1$s</string> + <string name="decky_plugin_installed">Plugin instalado. Reinicie o Steam para carregá-lo.</string> + + <string name="drivers_not_checked">Ainda não verificado - toque em atualizar para procurar novos drivers</string> + <string name="drivers_bundle_sets_display">Android + Linux: define também o driver de vídeo</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: define também o driver do runtime</string> + <string name="drivers_runtime_builtin">o Turnip integrado ao runtime</string> + <string name="drivers_system_vulkan">Vulkan do sistema: não há versão incluída para esta GPU</string> + <string name="drivers_bundled">%1$s (incluído)</string> + <string name="drivers_manual">Manual</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">No momento, só uma das metades está publicada</string> + <string name="drivers_pair_size">%1$.0f MB para os dois</string> + <string name="drivers_none_to_set">Nenhum driver para definir: %1$s</string> + <string name="drivers_checking_latest">Verificando os drivers mais recentes…</string> + <string name="drivers_up_to_date_check_failed">Atualizado (não foi possível verificar: %1$s)</string> + <string name="drivers_recommended_unpublished">Os drivers recomendados não estão publicados no momento</string> + <string name="drivers_downloading_pair">Baixando %1$s %2$s…</string> + <string name="drivers_updated_to">Atualizado para %1$s %2$s.</string> + <string name="drivers_updated_to_restart">Atualizado para %1$s %2$s. O driver de vídeo é aplicado depois que o DroidDeck reiniciar.</string> + <string name="drivers_switched_to">Alterado para %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Alterado para %1$s %2$s. O driver de vídeo é aplicado depois que o DroidDeck reiniciar.</string> + <string name="drivers_using">Usando %1$s %2$s.</string> + <string name="drivers_using_restart">Usando %1$s %2$s. O driver de vídeo é aplicado depois que o DroidDeck reiniciar.</string> + <string name="drivers_not_a_driver">Não é um zip de driver: não é um driver AdrenoTools, um Turnip -Linux nem um pacote Android + Linux</string> + <string name="drivers_import_failed">Falha na importação: %1$s</string> + <string name="drivers_imported_unnamed">Driver importado</string> + <string name="drivers_using_bundle">Usando %1$s como driver do runtime e de vídeo.</string> + <string name="drivers_using_bundle_restart">Usando %1$s como driver do runtime e de vídeo. O driver de vídeo é aplicado depois que o DroidDeck reiniciar.</string> + <string name="drivers_deleted_bundle">%1$s excluído (os dois drivers)</string> + <string name="drivers_deleted">%1$s excluído</string> + <string name="drivers_release_status">Mais recente: %1$s · verificado %2$s</string> + <string name="drivers_release_status_failed">Mais recente: %1$s · %2$s inacessível · verificado %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">há %1$d min</item> + <item quantity="many">há %1$d min</item> + <item quantity="other">há %1$d min</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">há %1$d h</item> + <item quantity="many">há %1$d h</item> + <item quantity="other">há %1$d h</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">há %1$d dia</item> + <item quantity="many">há %1$d dias</item> + <item quantity="other">há %1$d dias</item> + </plurals> + <string name="drivers_checking_sources">Verificando Banners-Turnip, WinNative e DroidDeck…</string> + <string name="drivers_check_failed">falha na verificação</string> + <string name="drivers_installed_pick_either">%1$s instalado - escolha-o em qualquer uma das listas para definir os dois drivers</string> + <string name="drivers_installed_pick">%1$s instalado - escolha-o no menu</string> + + <string name="session_wait_removal">Aguarde a remoção do runtime terminar</string> + <string name="session_title_game">Jogo</string> + <string name="session_touch_auto_touchpad">touchpad</string> + <string name="session_touch_auto_direct">direto</string> + <string name="session_swap_failed">Falha na troca: %1$s</string> + <string name="session_profile_save_failed">Falha ao salvar o perfil: %1$s</string> + <string name="session_desktop_paused">A área de trabalho está pausada</string> + <string name="session_named_paused">%1$s está pausado</string> + <string name="session_program_paused">O programa está pausado</string> + <string name="session_starting_desktop">Iniciando a área de trabalho</string> + <string name="session_starting_named">Iniciando %1$s</string> + <string name="session_starting_program">Iniciando o programa</string> + <string name="session_no_logs">Não há logs desta sessão.</string> + <string name="session_logs_archive_failed">Não foi possível criar o arquivo compactado dos logs da sessão.</string> + <string name="session_logs_share_failed">Não foi possível compartilhar os logs da sessão.</string> + <string name="session_starting_session">Iniciando a sessão…</string> + <string name="session_runtime_downloading">baixando o runtime Linux</string> + <string name="session_runtime_downloading_mb">baixando o runtime Linux · %1$d de %2$d MB</string> + <string name="session_runtime_checking">verificando o runtime Linux</string> + <string name="session_runtime_unpacking">descompactando o runtime Linux</string> + <string name="session_desktop_downloading">baixando a área de trabalho</string> + <string name="session_desktop_downloading_mb">baixando a área de trabalho · %1$d de %2$d MB</string> + <string name="session_desktop_checking">verificando a área de trabalho</string> + <string name="session_desktop_unpacking">descompactando a área de trabalho</string> + <string name="session_proton_downloading">baixando o Proton Experimental (ARM64)</string> + <string name="session_proton_downloading_mb">baixando o Proton Experimental (ARM64) · %1$d de %2$d MB</string> + <string name="session_proton_checking">verificando o Proton Experimental (ARM64)</string> + <string name="session_proton_unpacking">descompactando o Proton Experimental (ARM64)</string> + <string name="session_runtime_catalog_unreachable">Não foi possível acessar o catálogo de runtimes. Verifique a conexão e toque em Jogar novamente.</string> + <string name="session_runtime_install_failed">O runtime Linux não foi instalado. Verifique o runtime em Configuração e toque em Jogar novamente.</string> + <string name="session_desktop_install_failed">A área de trabalho não foi instalada (%1$s). Verifique a conexão e toque em Área de trabalho novamente.</string> + <string name="session_ended_named">%1$s parou inesperadamente. Compartilhe os logs em um relatório de bug ou tente de novo.</string> + <string name="session_ended_program">O programa parou inesperadamente. Compartilhe os logs em um relatório de bug ou tente de novo.</string> + <string name="session_ended_desktop">A área de trabalho parou inesperadamente. Compartilhe os logs em um relatório de bug ou tente de novo.</string> + <string name="session_ended_detail">Código de saída %1$d · %2$s</string> + <string name="session_ended_no_log">sem log</string> + <plurals name="session_hint_enosys"> + <item quantity="one">O log mostra %1$d erro “Function not implemented”: a aceleração seccomp do proot está falhando em um auxiliar neste dispositivo. Tente %2$s → “%3$s” (ou “%4$s”) e inicie de novo.</item> + <item quantity="many">O log mostra %1$d erros “Function not implemented”: a aceleração seccomp do proot está falhando em um auxiliar neste dispositivo. Tente %2$s → “%3$s” (ou “%4$s”) e inicie de novo.</item> + <item quantity="other">O log mostra %1$d erros “Function not implemented”: a aceleração seccomp do proot está falhando em um auxiliar neste dispositivo. Tente %2$s → “%3$s” (ou “%4$s”) e inicie de novo.</item> + </plurals> + + <string name="mode_decky_plugins">Plugins</string> + <string name="mode_decky_plugins_hint">Instale um ZIP de plugin para a próxima sessão do Steam.</string> + <string name="mode_decky_plugin_downloading">Baixando…</string> + <string name="mode_decky_plugin_install_zip">Instalar de um ZIP</string> + <string name="store_cat_games">Jogos</string> + <string name="store_cat_audio_video">Áudio e vídeo</string> + <string name="store_cat_graphics">Gráficos</string> + <string name="store_cat_internet">Internet</string> + <string name="store_cat_office">Escritório</string> + <string name="store_cat_developer">Desenvolvimento</string> + <string name="store_cat_education">Educação</string> + <string name="store_cat_science">Ciência</string> + <string name="store_cat_system">Sistema</string> + <string name="store_cat_utilities">Utilitários</string> + <string name="store_section_popular">Populares</string> + <string name="store_section_trending">Em alta</string> + <string name="store_section_new">Novidades</string> + <string name="store_section_updated">Atualizados</string> + <string name="store_section_loading">carregando…</string> + <string name="store_flatpak_setup">Configuração do Flatpak</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Não foi possível verificar atualizações no Flathub</string> + <string name="content_rom_installed">instalado</string> + <string name="content_rom_folder">pasta</string> + <string name="theme_graphite">Grafite</string> + <string name="theme_graphite_detail">Superfícies grafite, com o azul do orbe do logo em tudo que estiver selecionado, em foco ou pressionado.</string> + <string name="theme_paper">Papel sobre preto</string> + <string name="theme_paper_detail">O branco é a cor principal; o azul do ícone sinaliza linhas, anéis e pontos.</string> + <string name="theme_icon_blue">Azul do ícone</string> + <string name="theme_icon_blue_detail">Fundo preto, texto branco e o azul em tudo que estiver selecionado, em foco ou pressionado.</string> + <string name="theme_electric">Marinho elétrico</string> + <string name="theme_electric_detail">O azul mais vivo, sobre superfícies puxadas para o marinho em vez do cinza.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Espaço</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <string name="game_sync_err_runtime">Instale o runtime primeiro</string> + <string name="game_sync_err_steam_library">A biblioteca do Steam não está disponível</string> + <string name="game_sync_err_previous_sd">A biblioteca anterior no cartão SD não está disponível</string> + <string name="game_sync_err_game_storage">O armazenamento de jogos não está disponível</string> + <string name="game_sync_err_added_games">A pasta de jogos adicionados não está disponível</string> + <string name="game_sync_err_manifests">Os manifestos do Steam não estão disponíveis</string> + <string name="content_system_many">Vários sistemas</string> + <string name="fm_drive_internal">Interno</string> + <string name="fm_drive_linux">Sistema Linux</string> + <string name="fm_drive_sd">Cartão SD</string> + <string name="fm_drive_storage">Armazenamento</string> + <string name="fm_select">Selecionar</string> + <string name="fm_properties">Propriedades</string> + <string name="fm_favorite_add">Adicionar aos favoritos</string> + <string name="fm_favorite_remove">Remover dos favoritos</string> + <string name="fm_rename">Renomear</string> + <string name="fm_copy">Copiar</string> + <string name="fm_cut">Recortar</string> + <string name="fm_actions">Ações</string> + <string name="fm_favorites">Favoritos</string> + <string name="fm_pin_current">Fixar pasta atual</string> + <string name="fm_no_favorites">Nenhum favorito ainda - fixe uma pasta pelo menu ⋮ dela para voltar aqui rapidamente.</string> + <string name="fm_unpin_favorite">Remover dos favoritos</string> + <string name="fm_size_zero">0 bytes</string> + <string name="fm_unit_bytes">bytes</string> + <string name="fm_prop_location">Local</string> + <string name="fm_prop_type">Tipo</string> + <string name="fm_prop_size">Tamanho</string> + <string name="fm_prop_modified">Modificado em</string> + <string name="fm_type_folder">Pasta</string> + <string name="fm_type_file">Arquivo</string> + <string name="fm_type_ext_file">Arquivo %1$s</string> + <string name="fm_read_only">Somente leitura</string> + <string name="fm_read_only_hint">Impede que jogos (e o Wine) substituam ou excluam este arquivo.</string> + <string name="fm_read_only_failed">Não foi possível alterar Somente leitura</string> + <string name="fm_hidden">Oculto</string> + <string name="fm_hidden_unsupported">Este armazenamento não consegue guardar o atributo de oculto.</string> + <string name="fm_hidden_hint">Marca o arquivo como oculto no Windows (atributo de oculto do Wine).</string> + <string name="fm_hidden_failed">Não foi possível alterar Oculto neste armazenamento</string> + <string name="fm_done">Concluído</string> + <string name="fm_op_deleting">Excluindo...</string> + <string name="fm_delete_failed">Falha ao excluir</string> + <string name="fm_op_moving">Movendo...</string> + <string name="fm_op_copying">Copiando...</string> + <string name="fm_op_moving_item">Movendo - %1$s</string> + <string name="fm_op_copying_item">Copiando - %1$s</string> + <string name="fm_op_moving_batch">Movendo %1$d/%2$d - %3$s</string> + <string name="fm_op_copying_batch">Copiando %1$d/%2$d - %3$s</string> + <string name="fm_paste_failed">%1$d concluídos, %2$d com falha</string> + <string name="fm_paste_skipped">%1$d concluídos, %2$d ignorados</string> + <plurals name="fm_paste_moved"> + <item quantity="one">%1$d item movido</item> + <item quantity="many">%1$d itens movidos</item> + <item quantity="other">%1$d itens movidos</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">%1$d item copiado</item> + <item quantity="many">%1$d itens copiados</item> + <item quantity="other">%1$d itens copiados</item> + </plurals> + <string name="fm_already_exists">“%1$s” já existe</string> + <string name="fm_op_renaming">Renomeando...</string> + <string name="fm_rename_failed">Falha ao renomear</string> + <string name="fm_op_creating_folder">Criando pasta...</string> + <string name="fm_create_folder_failed">Não foi possível criar a pasta</string> + <string name="fm_new_folder">Nova pasta</string> + <string name="fm_folder_name">Nome da pasta</string> + <string name="fm_create">Criar</string> + <string name="fm_new_name">Novo nome</string> + <string name="fm_delete_title">Excluir?</string> + <string name="fm_delete_text">Excluir “%1$s” permanentemente?</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">Excluir %1$d item?</item> + <item quantity="many">Excluir %1$d itens?</item> + <item quantity="other">Excluir %1$d itens?</item> + </plurals> + <string name="fm_cannot_undo">Não é possível desfazer essa ação.</string> + <plurals name="fm_and_more"> + <item quantity="one">…e mais %1$d</item> + <item quantity="many">…e mais %1$d</item> + <item quantity="other">…e mais %1$d</item> + </plurals> + <string name="fm_op_deleting_batch">Excluindo %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">%1$d não pôde ser excluído</item> + <item quantity="many">%1$d não puderam ser excluídos</item> + <item quantity="other">%1$d não puderam ser excluídos</item> + </plurals> + <string name="fm_conflict_merge_hint">Mesclar adiciona e substitui arquivos dentro da pasta existente.</string> + <string name="fm_conflict_overwrite_hint">Substituir troca o arquivo existente.</string> + <string name="fm_conflict_apply_all">Aplicar a todos os conflitos</string> + <string name="fm_conflict_merge">Mesclar</string> + <string name="fm_conflict_overwrite">Substituir</string> + <string name="fm_conflict_keep_both">Manter ambos</string> + <string name="fm_conflict_skip">Pular</string> + <string name="fm_select_this_folder">Selecionar esta pasta</string> + <string name="fm_back">Voltar</string> + <string name="fm_drive_not_readable">%1$s está montado, mas não pode ser lido agora</string> + <string name="fm_list_view">Visualização em lista</string> + <string name="fm_grid_view">Visualização em grade</string> + <string name="fm_search">Pesquisar</string> + <string name="fm_sort">Ordenar</string> + <string name="fm_sort_name">Nome</string> + <string name="fm_sort_date">Data de modificação</string> + <string name="fm_rows_comfortable">Linhas confortáveis</string> + <string name="fm_rows_compact">Linhas compactas</string> + <string name="fm_hide_hidden">Ocultar arquivos ocultos</string> + <string name="fm_show_hidden">Mostrar arquivos ocultos</string> + <string name="fm_hide_favorites">Ocultar favoritos</string> + <string name="fm_show_favorites">Mostrar favoritos</string> + <string name="fm_filter_folder">Filtrar esta pasta</string> + <string name="fm_free_space">%1$s livres</string> + <string name="fm_selected_count">Selecionados: %1$d</string> + <string name="fm_select_none">Nenhum</string> + <string name="fm_select_all">Todos</string> + <string name="fm_paste">Colar</string> + <plurals name="fm_items"> + <item quantity="one">%1$d item</item> + <item quantity="many">%1$d itens</item> + <item quantity="other">%1$d itens</item> + </plurals> + <string name="fm_paste_here">Colar %1$s aqui</string> + <string name="fm_paste_move_here">Colar %1$s aqui (mover)</string> + <string name="fm_rail_title">Arquivos</string> + <string name="fm_rail_storage">ARMAZENAMENTO</string> + <string name="fm_rail_quick">ACESSO RÁPIDO</string> + <string name="fm_rail_favorites">FAVORITOS</string> + <string name="fm_rail_downloads">Downloads</string> + <string name="fm_rail_downloads_short">Downl.</string> + <string name="fm_rail_roms">ROMs</string> + <string name="fm_rail_session_logs">Logs de sessão</string> + <string name="fm_rail_pictures">Imagens</string> + <string name="fm_favorite_added">“%1$s” adicionado aos favoritos</string> + <string name="fm_favorite_removed">“%1$s” removido dos favoritos</string> + <string name="fm_empty_directory">Pasta vazia</string> + <string name="fm_up_one_level">Subir um nível</string> + <string name="fm_choose_folder">Escolha uma pasta</string> + <string name="fm_choose_file">Escolha um arquivo</string> + <string name="ctrl_tint_steam_blue">Azul Steam</string> + <string name="ctrl_tint_sky">Céu</string> + <string name="ctrl_tint_white">Branco</string> + <string name="ctrl_tint_violet">Violeta</string> + <string name="ctrl_tint_green">Verde</string> + <string name="ctrl_tint_amber">Âmbar</string> + <string name="ctrl_tint_red">Vermelho</string> + <string name="ctrl_tint_pink">Rosa</string> + <string name="ctrl_button_a">Botão A</string> + <string name="ctrl_button_b">Botão B</string> + <string name="ctrl_button_x">Botão X</string> + <string name="ctrl_button_y">Botão Y</string> + <string name="ctrl_button_lb">Botão de ombro esquerdo</string> + <string name="ctrl_button_rb">Botão de ombro direito</string> + <string name="ctrl_button_lt">Gatilho esquerdo</string> + <string name="ctrl_button_rt">Gatilho direito</string> + <string name="ctrl_button_view">Botão Exibir</string> + <string name="ctrl_button_menu">Botão Menu</string> + <string name="ctrl_target_view">Exibir</string> + <string name="ctrl_target_menu">Menu</string> + <string name="ctrl_target_up">Direcional para cima</string> + <string name="ctrl_target_down">Direcional para baixo</string> + <string name="ctrl_target_left">Direcional para a esquerda</string> + <string name="ctrl_target_right">Direcional para a direita</string> + <string name="ctrl_target_hidden">Oculto</string> + <string name="ctrl_editor_drag">Arraste para mover</string> + <string name="ctrl_editor_snap">Encaixar</string> + <string name="ctrl_editor_save">Salvar</string> + <string name="second_mode_none">Nenhum</string> + <string name="second_mode_keyboard_trackpad">Teclado + trackpad</string> + <string name="second_mode_terminal">Terminal</string> + <string name="second_mode_deck_controls">Botões traseiros + trackpads do Deck</string> + + <string name="rtinst_removing">Removendo o runtime Linux</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">Removendo o runtime Linux · %1$d entrada</item> + <item quantity="many">Removendo o runtime Linux · %1$d entradas</item> + <item quantity="other">Removendo o runtime Linux · %1$d entradas</item> + </plurals> + <string name="rtinst_removed">Runtime Linux removido</string> + <string name="rtinst_removal_failed">Não foi possível concluir a remoção do runtime Linux. Tente remover de novo em Configuração.</string> + <string name="rtinst_removing_leftovers">Removendo restos do runtime</string> + <string name="rtinst_downloading">Baixando</string> + <string name="rtinst_verifying">Verificando</string> + <string name="rtinst_extracting">Extraindo</string> + <string name="rtinst_channel_description">Aparece enquanto o runtime Linux é instalado ou removido</string> + <string name="rtinst_title_installing">Instalando o runtime Linux</string> + <string name="rtinst_title_removing">Removendo o runtime Linux</string> + + <string name="deskpkg_runtime_missing">O runtime Linux não está instalado</string> + <string name="deskpkg_no_checksum">O catálogo não tem soma de verificação para %1$s</string> + <string name="deskpkg_checksum_mismatch">A soma de verificação não confere - nada foi alterado</string> + <string name="deskpkg_installing">Instalando %1$s</string> + <string name="deskpkg_place_failed">Não foi possível colocar o AppImage no lugar</string> + <string name="deskpkg_extract_failed">Falha na extração</string> + <string name="deskpkg_install_failed">Falha na instalação</string> + + <string name="flatpakmgr_setup_failed">Falha na configuração do Flatpak (código de saída %1$d)</string> + <string name="flatpakmgr_setup_unfinished">A configuração do Flatpak não terminou</string> + <string name="flatpakmgr_busy">Outra tarefa da loja está em andamento</string> + <string name="flatpakmgr_not_ready">Configure o Flatpak primeiro</string> + <string name="flatpakmgr_starting">Iniciando</string> + <string name="flatpakmgr_exit_status">O Flatpak terminou com o código %1$d</string> + <string name="flatpakmgr_installing">Instalando %1$s</string> + <string name="flatpakmgr_updating">Atualizando %1$s</string> + <string name="flatpakmgr_removing">Removendo %1$s</string> + <string name="flatpakmgr_installing_step">Instalando %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_updating_step">Atualizando %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_removing_step">Removendo %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_ref_translations">traduções</string> + <string name="flatpakmgr_ref_graphics_drivers">drivers gráficos</string> + <string name="flatpakmgr_ref_media_codecs">codecs de mídia</string> + <string name="flatpakmgr_ref_runtime">runtime %1$s</string> + <string name="flatpakmgr_ref_runtime_branch">runtime %1$s %2$s</string> + + <string name="appimg_too_small">O arquivo é pequeno demais para ser um AppImage</string> + <string name="appimg_unreadable">Não é possível ler o arquivo: %1$s</string> + <string name="appimg_not_linux">Isto não é um AppImage (não é um programa Linux)</string> + <string name="appimg_wrong_arch">Este AppImage não foi feito para ARM64 (aarch64) nem para PCs x86</string> + <string name="appimg_not_appimage">Isto é um programa Linux, mas não um AppImage</string> + <string name="appimg_wrong_type">Só AppImages do tipo 2 podem ser importados (este é do tipo %1$d)</string> + <string name="appimg_no_space_import">Espaço livre insuficiente: a importação precisa de cerca de %1$s</string> + <string name="appimg_no_space_update">Espaço livre insuficiente: a atualização precisa de cerca de %1$s</string> + <string name="appimg_import_failed">Falha na importação</string> + <string name="appimg_gone">Esse AppImage não existe mais</string> + <string name="appimg_update_failed">Falha na atualização</string> + <string name="appimg_extracting">Extraindo %1$s</string> + <string name="appimg_extract_failed">Não foi possível extrair o AppImage</string> + <string name="appimg_extract_failed_detail">Não foi possível extrair o AppImage: %1$s</string> + + <string name="deckymgr_stop_session_loader">Pare a sessão ativa antes de instalar o Decky Loader</string> + <string name="deckymgr_download_failed">Falha no download do Decky Loader</string> + <string name="deckymgr_size_mismatch">O tamanho do Decky Loader não corresponde aos metadados da versão</string> + <string name="deckymgr_checksum_download_failed">Não foi possível baixar a soma de verificação da versão</string> + <string name="deckymgr_checksum_missing">A soma de verificação da versão está ausente</string> + <string name="deckymgr_checksum_invalid">A soma de verificação da versão é inválida</string> + <string name="deckymgr_checksum_mismatch">A soma de verificação do Decky Loader não confere</string> + <string name="deckymgr_not_elf">O arquivo da versão não é um executável Linux de 64 bits compatível</string> + <string name="deckymgr_session_started_loader">Uma sessão foi iniciada durante o download; pare-a antes de instalar o Decky Loader</string> + <string name="deckymgr_loader_not_executable">Não foi possível tornar o PluginLoader executável</string> + <string name="deckymgr_cef_failed">Não foi possível ativar a depuração remota do CEF do Steam</string> + <string name="deckymgr_stage_failed">Não foi possível preparar o PluginLoader</string> + <string name="deckymgr_loader_preserve_failed">Não foi possível preservar o PluginLoader existente</string> + <string name="deckymgr_loader_activate_failed">Não foi possível ativar o novo PluginLoader</string> + <string name="deckymgr_stop_session_plugin">Pare a sessão ativa antes de instalar um plugin</string> + <string name="deckymgr_loader_required">Instale o Decky Loader antes de importar um plugin</string> + <string name="deckymgr_zip_unreadable">Não foi possível ler o arquivo ZIP selecionado</string> + <string name="deckymgr_plugins_dir_failed">Não foi possível criar a pasta de plugins do Decky</string> + <string name="deckymgr_extracting_zip">Extraindo o ZIP do plugin</string> + <string name="deckymgr_zip_one_folder">O ZIP do plugin deve conter uma única pasta de plugin no nível superior</string> + <string name="deckymgr_zip_missing_files">Faltam plugin.json, package.json ou dist/index.js no ZIP do plugin</string> + <string name="deckymgr_plugin_name_missing">O nome do plugin está ausente</string> + <string name="deckymgr_session_started_plugin">Uma sessão foi iniciada durante a instalação; pare-a antes de instalar um plugin</string> + <string name="deckymgr_plugin_installed">Plugin instalado · reinicie o Steam para carregá-lo</string> + <string name="deckymgr_plugin_install_failed">Falha na instalação do ZIP do plugin</string> + <string name="deckymgr_zip_too_many">O ZIP do plugin contém entradas demais</string> + <string name="deckymgr_zip_invalid_path">O ZIP do plugin contém um caminho inválido</string> + <string name="deckymgr_zip_duplicate_paths">O ZIP do plugin contém caminhos duplicados</string> + <string name="deckymgr_zip_unsafe_path">O ZIP do plugin contém um caminho inseguro</string> + <string name="deckymgr_folder_failed">Não foi possível criar a pasta do plugin</string> + <string name="deckymgr_zip_too_large">O ZIP do plugin ultrapassa o limite de 1 GiB quando extraído</string> + <string name="deckymgr_bundled_not_executable">Não foi possível tornar executável o binário incluído no plugin</string> + <string name="deckymgr_remote_entry_invalid">Entrada remote_binary inválida</string> + <string name="deckymgr_remote_name_invalid">Nome de binário remoto inválido</string> + <string name="deckymgr_remote_url_https">A URL do binário remoto precisa usar HTTPS</string> + <string name="deckymgr_remote_hash_invalid">O binário remoto %1$s tem um hash SHA-256 inválido</string> + <string name="deckymgr_bin_dir_failed">Não foi possível criar a pasta bin do plugin</string> + <string name="deckymgr_downloading_binary">Baixando binário do plugin · %1$s</string> + <string name="deckymgr_binary_download_failed">Não foi possível baixar o binário do plugin %1$s</string> + <string name="deckymgr_binary_checksum_mismatch">A soma de verificação do binário do plugin não confere · %1$s</string> + <string name="deckymgr_binary_not_executable">Não foi possível tornar executável o binário do plugin · %1$s</string> + <string name="deckymgr_binary_replace_failed">Não foi possível substituir o binário do plugin · %1$s</string> + <string name="deckymgr_binary_install_failed">Não foi possível instalar o binário do plugin · %1$s</string> + <string name="deckymgr_folder_taken">A pasta do plugin pertence a outro plugin</string> + <string name="deckymgr_preserve_failed">Não foi possível preservar o plugin existente</string> + <string name="deckymgr_activate_failed">Não foi possível ativar o plugin importado</string> + <string name="deckymgr_settings_failed">Não foi possível atualizar as configurações de plugins do Decky</string> + <string name="deckymgr_restore_failed">Não foi possível restaurar o plugin existente</string> + + <string name="adbfix_save_failed">Não foi possível salvar a identidade da depuração por Wi-Fi</string> + <string name="adbfix_command_failed">Falha no comando ADB (%1$d)</string> + <string name="adbfix_not_confirmed">O Android não confirmou a alteração do limite de processos filhos: %1$s</string> + <string name="adbfix_no_pairing">Nenhum pareamento de depuração por Wi-Fi salvo</string> + <string name="adbfix_enter_address">Digite o endereço IP e a porta atuais da depuração por Wi-Fi</string> + + <string name="adbpair_timed_out">Tempo esgotado esperando o pop-up de pareamento. Tente novamente pelo DroidDeck.</string> + <string name="adbpair_discovery_failed_code">Não foi possível procurar o pop-up de pareamento (%1$d). Verifique se o Wi-Fi está ativado.</string> + <string name="adbpair_discovery_failed">Não foi possível procurar o pop-up de pareamento: %1$s</string> + <string name="adbpair_six_digits">Digite os 6 dígitos do código</string> + <string name="adbpair_pairing">Pareando…</string> + <string name="adbpair_code_rejected">Esse código não funcionou. Abra o pop-up de pareamento de novo e digite o novo código.</string> + <string name="adbpair_connecting">Pareado. Conectando…</string> + <string name="adbpair_port_not_found">Pareado, mas a porta da depuração por Wi-Fi não foi encontrada. Mantenha a depuração por Wi-Fi ativada e tente de novo.</string> + <string name="adbpair_applying">Aplicando a configuração…</string> + <string name="adbpair_command_failed">Falha no comando de depuração por Wi-Fi</string> + <string name="adbpair_channel_name">Configuração da depuração por Wi-Fi</string> + <string name="adbpair_channel_description">Pede o código de pareamento da depuração por Wi-Fi enquanto as Configurações estão abertas</string> + <string name="adbpair_title_pair">Parear depuração por Wi-Fi</string> + <string name="adbpair_waiting_text">Ative a depuração por Wi-Fi e toque em “Parear o dispositivo com um código de pareamento”.</string> + <string name="adbpair_waiting_long">Ative a depuração por Wi-Fi e toque em “Parear o dispositivo com um código de pareamento”. Fique nas Configurações: o código é digitado nesta notificação.</string> + <string name="adbpair_code_label">Código de pareamento de 6 dígitos</string> + <string name="adbpair_code_hint">Toque em Digitar código e insira os 6 dígitos mostrados nas Configurações.</string> + <string name="adbpair_title_code">Digite o código de pareamento por Wi-Fi</string> + <string name="adbpair_title_retry">Tente o código de novo</string> + <string name="adbpair_enter_code">Digitar código</string> + <string name="adbpair_title_working">Configurando o Steam</string> + <string name="adbpair_title_done">O Steam está pronto</string> + <string name="adbpair_done_text">Toque para voltar ao DroidDeck. Você já pode desativar a depuração por Wi-Fi.</string> + <string name="adbpair_title_failed">A configuração da depuração por Wi-Fi parou</string> + + <string name="selfinst_http">O download respondeu HTTP %1$d</string> + <string name="selfinst_size_mismatch">O tamanho do download não corresponde ao APK publicado, e ele foi descartado - tente de novo</string> + <string name="selfinst_checksum_mismatch">O download não corresponde à soma de verificação e foi descartado - tente de novo</string> + <string name="selfinst_wrong_package">A atualização é para o pacote Android %1$s, não %2$s</string> + <string name="selfinst_metadata_signer">Os metadados da atualização não indicam a chave de assinatura de versões do DroidDeck</string> + <string name="selfinst_unreadable">O Android não conseguiu ler o APK baixado</string> + <string name="selfinst_apk_wrong_package">O APK baixado é para %1$s, não %2$s</string> + <string name="selfinst_version_mismatch">O APK baixado informa versionCode %1$d, não o %2$d publicado</string> + <string name="selfinst_downgrade">O Android não instala o versionCode %1$d por cima do %2$d instalado</string> + <string name="selfinst_no_signing">O APK baixado não tem informações de assinatura</string> + <string name="selfinst_wrong_signer">O APK baixado não está assinado com a chave de versões do DroidDeck</string> + <string name="appupd_http">O catálogo de atualizações respondeu HTTP %1$d</string> + + <string name="loadstate_starting">Iniciando a sessão…</string> + <string name="loadstate_elapsed">%1$d:%2$02d decorridos · ainda em andamento</string> + <string name="loadstate_steam_update">Baixando a atualização do cliente Steam · %1$d%%</string> + <string name="loadstate_steam_client">Baixando o cliente Steam · %1$s</string> + + <string name="pextra_busy">Já há uma instalação de ferramenta de compatibilidade em andamento</string> + <string name="pextra_stop_session">Pare a sessão ativa antes de instalar ferramentas de compatibilidade</string> + <string name="pextra_no_release">Não foi possível encontrar uma versão ARM64 de %1$s</string> + <string name="pextra_no_space">É preciso ter pelo menos 4 GB livres, além do tamanho do download, para instalar %1$s</string> + <string name="pextra_downloading">Baixando %1$s %2$s</string> + <string name="pextra_retrying">Tentando baixar de novo · tentativa %1$d de %2$d</string> + <string name="pextra_interrupted">Download interrompido</string> + <string name="pextra_download_failed">Falha no download; o arquivo parcial foi mantido para que uma nova tentativa possa continuar</string> + <string name="pextra_too_big">O download ultrapassou o tamanho da versão e foi excluído</string> + <string name="pextra_incomplete">O download ficou incompleto; o arquivo parcial foi mantido para continuar em uma nova tentativa</string> + <string name="pextra_verifying">Verificando o download</string> + <string name="pextra_checksum_unreadable">Não foi possível ler a soma de verificação da versão; tente de novo</string> + <string name="pextra_no_checksum">%1$s %2$s não publica soma de verificação; nada foi instalado</string> + <string name="pextra_checksum_mismatch">A soma de verificação não confere; o download foi excluído</string> + <string name="pextra_session_started">Uma sessão foi iniciada durante o download; pare-a antes de instalar ferramentas de compatibilidade</string> + <string name="pextra_installing">Instalando %1$s</string> + <string name="pextra_install_failed">Falha na instalação do %1$s (código de saída %2$d)</string> + <string name="pextra_could_not_install">Não foi possível instalar o %1$s</string> + <string name="esync_fetching">Buscando o pacote droiddeck-esync</string> + + <string name="cmgr_fex_linux_helpers">com auxiliares Linux</string> + <string name="cmgr_fex_dlls_only">só DLLs</string> + <string name="cmgr_original_proton_updated">Original (Proton atualizado desde a última troca)</string> + <string name="cmgr_changed_outside">Alterado fora de Componentes</string> + <string name="cmgr_not_present">ausente</string> + <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 entra no %3$s quando o jogo que está rodando nele for fechado.</string> + <string name="cmgr_swap_done">%1$s %2$s agora está no %3$s. Vale a partir do próximo jogo iniciado.</string> + <string name="cmgr_original_missing">Esse pacote original não está armazenado</string> + <string name="cmgr_restore_queued">%1$s do %2$s volta ao original quando o jogo que está rodando nele for fechado.</string> + <string name="cmgr_restore_done">%1$s do %2$s restaurado a partir do original do %3$s.</string> + <string name="cmgr_package_in_use">%1$s está em uso no %2$s. Primeiro, troque esse Proton para outra versão.</string> + <string name="cmgr_package_not_stored">%1$s não está armazenado</string> + <string name="cmgr_deleted">%1$s excluído.</string> + <string name="cmgr_original_kept">O original da versão instalada é mantido: é o caminho de volta.</string> + <string name="cmgr_not_stored">Não armazenado</string> + <string name="cmgr_deleted_original">Original do %1$s de %2$s excluído.</string> + <string name="cmgr_nightlies_only">Os downloads vêm só das versões Nightlies</string> + <string name="cmgr_list_predates_checksums">Esta lista de pacotes é anterior às somas de verificação - atualize-a e tente de novo</string> + <string name="cmgr_checksum_mismatch">A soma de verificação não confere</string> + + <string name="sprefs_upscaler_linear">Linear</string> + <string name="sprefs_upscaler_nearest">Vizinho mais próximo</string> + <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (qualidade)</string> + <string name="sprefs_upscaler_sharpen">Só nitidez</string> + <string name="sprefs_lod_bias_auto">Automático (acompanha a escala)</string> + + <string name="look_off_desc">Nada aplicado.</string> + <string name="look_game_clarity">Clareza</string> + <string name="look_game_clarity_desc">Nitidez com um toque de contraste - o do dia a dia.</string> + <string name="look_vivid">Vívido</string> + <string name="look_vivid_desc">Cores bem fortes. Debanding ativado para o céu não ficar em faixas.</string> + <string name="look_cinematic">Cinematográfico</string> + <string name="look_cinematic_desc">Contraste mais profundo, cores mais frias, bordas suavizadas.</string> + <string name="look_competitive">Competitivo</string> + <string name="look_competitive_desc">Sombras clareadas e nitidez forte - aviste os outros primeiro.</string> + <string name="look_adaptive_sharpen">Nitidez adaptativa</string> + <string name="look_adaptive_sharpen_desc">Só nitidez, cores intactas.</string> + <string name="look_filmic">Fílmico</string> + <string name="look_filmic_desc">Curva de filme, cores levemente apagadas.</string> + <string name="look_arcade">Arcade</string> + <string name="look_arcade_desc">Intenso e vibrante.</string> + <string name="look_retro_crt">CRT retrô</string> + <string name="look_retro_crt_desc">Os passes de tubo CRT e de sinal analógico juntos.</string> + <string name="look_upscale_sharp">Upscale nítido</string> + <string name="look_upscale_sharp_desc">Para rodar abaixo da resolução da tela.</string> + <string name="look_pixel_clean">Pixels limpos</string> + <string name="look_pixel_clean_desc">Pixels nítidos para jogos 2D e antigos.</string> + <string name="look_anime_edge">Contorno anime</string> + <string name="look_anime_edge_desc">Contornos de desenho animado e nitidez.</string> + + <string name="gpuinfo_not_adreno">Não é uma GPU Adreno</string> + <string name="gpuinfo_supported">Compatível</string> + <string name="gpuinfo_experimental">Experimental: os drivers são versões de teste</string> + <string name="gpuinfo_below_tested">Abaixo do hardware testado (Adreno 725 ou mais recente): pode não funcionar</string> + <string name="gpuinfo_unsupported">Incompatível: o DroidDeck precisa de uma GPU Adreno (Snapdragon)</string> + + <string name="turnip_rate_limited">O limite de requisições do GitHub foi atingido - tente mais tarde</string> + <string name="turnip_github_http">O GitHub respondeu HTTP %1$d</string> + <string name="turnip_download_http">o download respondeu HTTP %1$d</string> + <string name="turnip_list_predates_checksums">Esta lista de drivers é anterior às somas de verificação - atualize-a e tente de novo</string> + <string name="turnip_checksum_mismatch">A soma de verificação não confere - o download foi descartado</string> + <string name="turnip_not_adrenotools">Não é um driver AdrenoTools: %1$s</string> + <string name="turnip_reject_no_meta">sem meta.json</string> + <string name="turnip_reject_meta_unreadable">não é possível ler o meta.json (%1$s)</string> + <string name="turnip_reject_linux_driver">este é um driver do runtime Linux (importe-o em “Driver do runtime Linux”)</string> + <string name="turnip_reject_wayland">este é um driver de jogos Wayland, que não tem utilidade neste app</string> + <string name="turnip_reject_no_library_name">o meta.json não indica libraryName, então nada pôde ser entregue ao AdrenoTools</string> + <string name="turnip_reject_library_missing">o meta.json indica %1$s, que não está no zip</string> + <string name="turnip_reject_not_aarch64">%1$s não é uma biblioteca compartilhada ELF AArch64 de 64 bits</string> + <string name="turnip_reject_glibc">%1$s usa glibc - é um Turnip “-Linux”, que vai em “Driver do runtime Linux”</string> + <string name="lvd_no_library">Não há libvulkan_freedreno*.so neste zip. Um zip de Turnip Android (AdrenoTools) ou -Wayland não é um driver do runtime Linux.</string> + <string name="lvd_not_aarch64">%1$s não é uma biblioteca compartilhada ELF AArch64 de 64 bits.</string> + <string name="lvd_not_glibc">%1$s não é um driver glibc - ele usa a libc do Android. O runtime Linux precisa de um zip “-Linux”; um Turnip comum ou “-Wayland” não pode ser carregado pelo cliente Steam de jeito nenhum.</string> + <string name="bundle_no_manifest">Não é um pacote de drivers: não há manifest.json indicando um driver android e um linux</string> + <string name="bundle_entry_outside">Não é um pacote de drivers: %1$s aponta para fora dele</string> + <string name="bundle_too_large">Não é um pacote de drivers: extraído, ele ocupa mais de %1$d MB</string> + + <string name="gstore_sd_card">Cartão SD</string> + <string name="gstore_folder">Pasta</string> + + <string name="logshare_chooser">Compartilhar logs da sessão</string> + + <string name="hdr_needs_android_10">requer Android 10 ou mais recente (camadas de exibição com espaço de cores)</string> + <string name="hdr_none">esta tela não informa suporte a HDR</string> + <string name="hdr_not_hdr10">esta tela é compatível com %1$s, não com HDR10</string> + + <string name="gpu_this_gpu">esta GPU</string> + <string name="phantom_title_on">O limite de processos filhos está ativado</string> + <string name="phantom_title_unset">O limite de processos filhos não está definido</string> + <string name="phantom_title_unreadable">Não foi possível verificar o limite de processos filhos</string> + <string name="phantom_title_off">O limite de processos filhos está desativado</string> + <string name="phantom_title_not_required">O limite de processos filhos não é necessário</string> + <string name="phantom_instructions_fix">%1$s Se não for possível, conecte este dispositivo a um computador com ADB e execute o comando abaixo. Depois volte aqui e verifique de novo.</string> + <string name="phantom_instructions_off">As sessões do Steam podem ser iniciadas.</string> + <string name="phantom_instructions_not_applicable">Esta versão do Android não usa esse limite.</string> + <string name="phantom_fix_toggle">Ative “Desativar restrições de processos filhos” nas Opções do desenvolvedor e mantenha as Opções do desenvolvedor ativadas depois.</string> + <string name="phantom_fix_no_toggle">Esta versão do Android não tem uma chave para isso. Use a depuração por Wi-Fi para alterar pelo próprio dispositivo.</string> + <string name="upd_block_no_apk">Esta versão não tem download para esta cópia do DroidDeck.</string> + <string name="upd_block_version_code">O Android não vai instalá-la por cima desta versão porque o versionCode %1$d é menor que o %2$d instalado.</string> + <string name="upd_fail_downgrade">O Android não instala uma versão mais antiga por cima de uma mais nova. Fique nesta versão até o canal alcançá-la.</string> + <string name="upd_fail_signature">Esta cópia do DroidDeck tem outra assinatura (uma build local?), então o Android não consegue atualizá-la diretamente.</string> + <string name="upd_fail_storage">Não há espaço livre suficiente para a atualização.</string> + <string name="upd_fail_aborted">A atualização foi cancelada.</string> + <string name="upd_fail_with_message">A atualização não foi instalada: %1$s</string> + <string name="upd_fail_generic">A atualização não foi instalada.</string> + <string name="saves_layout_zip">zip do %1$s</string> + <string name="saves_zip">zip</string> + <string name="saves_not_launched">%1$s ainda não foi iniciado</string> + <string name="saves_none_found">Nenhum arquivo de save encontrado para %1$s</string> + <string name="saves_not_save_zip">%1$s não tem a pasta drive_c, então não é um zip de saves do GameHub ou do Winlator</string> + <string name="gstore_cannot_create">não é possível criar pastas em %1$s</string> + <string name="gstore_cannot_write">não é possível gravar em %1$s</string> + <string name="gstore_cannot_write_detail">não é possível gravar em %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">A pasta não está disponível</string> + <string name="gfiles_name_taken">Outro arquivo já usa %1$s</string> + <string name="gfiles_art_unavailable">A pasta de artes não está disponível</string> + <string name="gfiles_unrelated_inventory">A pasta tem um inventário de sincronização não relacionado</string> + <string name="gfiles_remove_failed">Não foi possível remover %1$s</string> + <string name="gfiles_outside_folder">O arquivo sai da pasta de exportação</string> + <string name="cmgr_fex_unnamed">FEX (build sem nome de versão)</string> + <string name="cmgr_no_profile">sem profile.json em %1$s</string> + <string name="cmgr_not_component_package">%1$s não é um pacote de componente Linux (tipo %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s livres</string> + <string name="mode_storage_auto_short">Automático</string> + <string name="appupd_catalog_rejected">O catálogo de atualizações não passou nas verificações (%1$s)</string> + <string name="main_adb_clip_label">Configuração de processos filhos do DroidDeck</string> + <string name="setup_signed_in_offline_unnamed">Conectado · modo offline</string> + <string name="setup_signed_in_unnamed">Conectado</string> + <string name="driver_tag_bundled">INCLUÍDO</string> + <string name="driver_tag_downloaded">BAIXADO</string> + <string name="driver_tag_imported">IMPORTADO</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Tela da sessão</string> + <string name="display_resolution">Resolução</string> + <string name="display_game_fit">Ajuste de jogos 16:9</string> + <string name="display_game_fit_hint">Como os jogos 16:9 se ajustam a esta tela. Na próxima sessão.</string> + <string name="display_game_fit_unavailable">Escolha Igual à tela ou um tamanho personalizado mais estreito que 16:9.</string> + <string name="display_preserve">Manter proporções</string> + <string name="display_stretch">Esticar para a tela</string> + <string name="display_image_scaling">Escala de imagem</string> + <string name="display_filter">Filtro de escala</string> + <string name="display_filter_hint">Compartilhado entre Steam e Área de trabalho.</string> + <string name="display_filter_live">Aplicado na hora. Compartilhado entre Steam e Área de trabalho.</string> + <string name="display_filter_note">FSR, GSR e NIS são aplicados ao ampliar a imagem da sessão. Só nitidez também funciona sem ampliação.</string> + <string name="display_sharpness">Nitidez do filtro</string> + <string name="display_frame_rate">Taxa de quadros</string> + <string name="display_look_filter">%1$s Define a escala como %2$s.</string> + <string name="display_match_screen">Igual à tela (%1$d×%2$d)</string> + <string name="display_follow_screen">Seguir a tela (redimensiona ao dobrar ou desdobrar)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Mostrar detalhes do dispositivo</string> + <string name="device_info_hide">Ocultar detalhes do dispositivo</string> + <string name="device_info_loading">Lendo o dispositivo…</string> + <string name="device_info_copy">Copiar</string> + <string name="device_info_copy_hint">Copie estes detalhes para um relatório de bug.</string> + <string name="device_info_copied">Copiado.</string> + <string name="force_ssbs_title">Forçar SSBS</string> + <string name="force_ssbs_hint">Mantém ativada uma configuração de velocidade da CPU que o Wine fica desativando. Pode deixar jogos do Windows mais rápidos no Snapdragon 8 Elite; pouca mudança em chips mais antigos. Vale a partir da próxima vez que um jogo for iniciado.</string> + + <string name="rail_stores">Lojas</string> + <string name="source_steam">Steam</string> + <string name="source_added">Personalizado</string> + <string name="setup_stores">Lojas</string> + <string name="setup_stores_open_on">Abrir uma loja em</string> + <string name="setup_stores_speed">Nível de velocidade de download</string> + <string name="setup_stores_engine">Mecanismo das lojas</string> + <string name="setup_stores_engine_chip_ready">● Mecanismo pronto</string> + <string name="setup_stores_engine_chip_missing">Não compilado</string> + <string name="stores_tier_balanced">Equilibrado</string> + <string name="stores_tier_fast">Rápido</string> + <string name="stores_tier_max">Máximo</string> + <string name="stores_title">Lojas</string> + <string name="stores_settings">Configurações das lojas</string> + <string name="stores_settings_eyebrow">Configurações</string> + <string name="stores_chip_signed_out">(sem login)</string> + <string name="stores_tab_store">Loja</string> + <string name="stores_tab_installed">Instalados</string> + <string name="stores_tab_library">Biblioteca</string> + <string name="stores_tab_all">Todos</string> + <string name="stores_tab_downloads">Downloads</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Fazer login</string> + <string name="stores_signout_named">%1$s · sair</string> + <string name="stores_signout_title">Sair de %1$s?</string> + <string name="stores_signout_note">Os jogos instalados continuam. A biblioteca é buscada de novo quando você fizer login outra vez.</string> + <string name="stores_signout_stay">Continuar conectado</string> + <string name="stores_signout_go">Sair</string> + <string name="stores_search_catalog">Pesquisar no catálogo de %1$s…</string> + <string name="stores_search_installed">Pesquisar jogos instalados…</string> + <string name="stores_search_library">Pesquisar na sua biblioteca…</string> + <string name="stores_search_open">Pesquisar</string> + <string name="stores_installed_empty">Nada instalado</string> + <string name="stores_library_loading">Carregando</string> + <string name="stores_nothing_matches">Nenhum jogo</string> + <string name="stores_shelves_loading">Carregando a loja…</string> + <string name="stores_amazon_no_catalog">Sem catálogo</string> + <string name="stores_shelf_new">Novidades</string> + <string name="stores_shelf_deals">Ofertas</string> + <string name="stores_shelf_free">Jogos grátis</string> + <string name="stores_shelf_trending">Em alta</string> + <string name="stores_shelf_library">Sua biblioteca</string> + <string name="stores_play">Jogar</string> + <string name="stores_install">Instalar</string> + <string name="stores_resume_install">Retomar instalação</string> + <string name="stores_dl_interrupted">Interrompido</string> + <string name="stores_install_size">Instalar · %1$s</string> + <string name="stores_get_free">Obter grátis</string> + <string name="stores_view_on">Ver em %1$s</string> + <string name="stores_buy_price">Comprar · %1$s</string> + <string name="stores_installed_chip">Instalado</string> + <string name="stores_in_library">Na biblioteca</string> + <string name="stores_free">Grátis</string> + <string name="stores_resume">Retomar</string> + <string name="stores_refresh">Atualizar</string> + <string name="stores_show_mature">Mostrar conteúdo adulto</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Offline</string> + <string name="epic_card_none">Desativado</string> + <string name="epic_eos">Login da Epic (EOS)</string> + <string name="epic_offline">Iniciar offline</string> + <string name="epic_resolve">Resolver login da Epic</string> + <string name="store_cloud_saves">Saves na nuvem</string> + <string name="cloud_upload">Enviar para a nuvem</string> + <string name="cloud_download">Baixar da nuvem</string> + <string name="cloud_none">Sem saves na nuvem</string> + <string name="cloud_conflict">Conflito na nuvem</string> + <string name="cloud_conflict_files">Conflito na nuvem · %1$d arquivos</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Manter da nuvem</string> + <string name="cloud_keep_local">Manter local</string> + <string name="cloud_checking">Verificando a nuvem…</string> + <string name="cloud_last_sync">Última sincronização: %1$s</string> + <string name="cloud_never">Ainda não sincronizado</string> + <string name="cloud_done">%1$d arquivos</string> + <string name="stores_eyebrow_installed">instalado</string> + <string name="stores_eyebrow_library">na sua biblioteca</string> + <string name="stores_eyebrow_store">loja</string> + <string name="achievements_title">Conquistas</string> + <string name="achievements_count">%1$d de %2$d desbloqueadas</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Desbloqueadas · %1$d</string> + <string name="achievements_group_locked">Bloqueadas · %1$d</string> + <string name="achievements_group_hidden">Ocultas · %1$d</string> + <string name="achievements_hidden">Conquista oculta</string> + <string name="achievements_hidden_detail">Os detalhes aparecem quando ela é desbloqueada.</string> + <string name="achievements_all">Todas as conquistas desbloqueadas</string> + <string name="achievements_reading">Carregando…</string> + <string name="achievements_signed_out">Entre no %1$s para ver suas conquistas.</string> + <string name="achievements_unreachable">Não foi possível acessar o %1$s. Verifique novamente quando estiver online.</string> + <string name="achievements_none">O %1$s não lista conquistas para este jogo.</string> + <string name="achievements_show_all">Mostrar todas as %1$d</string> + <string name="achievements_dialog_title">Conquistas · %1$s</string> + <string name="stores_in_steam">✓ No Steam</string> + <string name="stores_uninstall">Desinstalar</string> + <string name="stores_uninstalling">Desinstalando…</string> + <string name="stores_uninstall_title">Desinstalar %1$s?</string> + <string name="stores_uninstall_body_size">Libera %1$s e remove o jogo do Steam.</string> + <string name="stores_uninstall_body">Remove os arquivos e o atalho no Steam.</string> + <string name="stores_launch_not_registered">O Steam detecta o jogo na próxima vez que for iniciado; abra o jogo por lá ou inicie o Steam agora.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Escolha do Steam para cada jogo</string> + <string name="stores_card_audio">Áudio</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Componentes</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Mecanismo</string> + <string name="stores_card_engine_value">%1$s · nativo</string> + <string name="stores_card_engine_java">%1$s · integrado</string> + <string name="stores_how_it_runs">Como ele roda</string> + <string name="stores_kv_fetched">Obtido de</string> + <string name="stores_kv_fetched_value">%1$s, com sua conta</string> + <string name="stores_kv_installed_to">Instalado em</string> + <string name="stores_kv_installed_to_value">Armazenamento de jogos (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Iniciado por</string> + <string name="stores_kv_launched_by_value">Steam e Proton, o mesmo caminho dos jogos do Steam</string> + <string name="stores_kv_launched_by_epic">Steam e Proton · login da Epic a cada início, overlay do EOS desativado</string> + <string name="stores_kv_launched_by_amazon">Steam e Proton · variáveis do Amazon Games Services definidas</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">adicionado ao Steam na instalação (interface do Deck + Steam Input)</string> + <string name="stores_downloads_empty">Nenhum download</string> + <string name="stores_dl_paused">Pausado</string> + <string name="stores_dl_queued">Na fila</string> + <string name="stores_dl_queued_at">Na fila · #%1$d</string> + <string name="stores_dl_cancelled">Cancelado</string> + <string name="stores_dl_failed">Falhou</string> + <string name="stores_dl_failed_reason">Falhou · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Pausar</string> + <string name="stores_dl_resume">Retomar</string> + <string name="stores_dl_cancel">Cancelar</string> + <string name="stores_dl_retry">Tentar de novo</string> + <string name="stores_dl_clear">Limpar</string> + <string name="stores_dl_clear_all">Limpar concluídos</string> + <string name="stores_stage_manifest">Manifesto</string> + <string name="stores_stage_download">Download</string> + <string name="stores_stage_verify">Verificação</string> + <string name="stores_stage_install">Instalação</string> + <string name="stores_stage_done">Concluído</string> + <string name="stores_stage_checking">Verificando</string> + <string name="stores_stage_downloading">Baixando</string> + <string name="stores_dl_cancel_confirm">Excluir download?</string> + <string name="stores_dl_failed_kept">Falhou · %1$s mantido</string> + <string name="stores_stage_verifying">Verificando</string> + <string name="stores_stage_installing">Instalando</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d arquivos</string> + <string name="stores_download_manager">Gerenciador de downloads</string> + <string name="stores_parallel">Downloads simultâneos</string> + <string name="stores_install_where">Instalar em</string> + <string name="stores_install_title">Instalar %1$s:</string> + <string name="stores_target_internal">Interno</string> + <string name="stores_target_free">%1$s livres</string> + <string name="stores_notification_channel">Downloads da loja</string> + <string name="stores_notification_channel_hint">Mostra o progresso dos downloads de jogos e mantém os downloads rodando em segundo plano</string> + <string name="stores_notification_one">Baixando</string> + <string name="stores_notification_many">%1$d downloads</string> + <string name="stores_notification_waiting">Preparando…</string> + <string name="stores_notification_done_channel">Downloads da loja concluídos</string> + <string name="stores_notification_installed">%1$s instalado</string> + <string name="stores_notification_failed">Falha no download de %1$s</string> + <string name="stores_notification_delete">Excluir</string> + <string name="stores_notification_keep">Manter</string> + <string name="stores_login_title">Fazer login em %1$s</string> + <string name="stores_login_provider_title">Fazer login com seu provedor</string> + <string name="stores_login_reload">Recarregar</string> + <string name="stores_login_close">Fechar</string> + <string name="stores_login_retry">Tentar de novo</string> + <string name="stores_login_finishing">Concluindo o login…</string> + <string name="stores_login_error_network">Não foi possível carregar a página de login: %1$s</string> + <string name="stores_login_error_idp_blocked">Este provedor não permite login a partir de uma página incorporada. Faça login com a senha da loja.</string> + <string name="stores_login_error_http">A página de login respondeu com um erro (HTTP %1$d).</string> + <string name="stores_login_error_ssl">A conexão com a página de login não é segura.</string> + <string name="stores_login_error_verification">Não foi possível verificar o login. Tente de novo.</string> + <string name="stores_login_error_generic">Falha no login. Tente de novo.</string> + <string name="common_close">Fechar</string> + <string name="common_back">Voltar</string> + <string name="common_edit">Editar</string> + <string name="games_add_all">Adicionar todos os jogos desta pasta</string> + <string name="games_added_none">Nada de novo nesta pasta</string> + <string name="edit_name">Nome</string> + <string name="edit_target">Destino</string> + <string name="edit_start_in">Iniciar em</string> + <string name="edit_launch_options">Opções de inicialização</string> + <string name="edit_artwork">Artes</string> + <string name="edit_other_exe">Outro exe…</string> + <string name="edit_remove">Remover</string> + <string name="art_cover">Capa</string> + <string name="art_background">Fundo</string> + <string name="art_logo">Logo</string> + <string name="art_icon">Ícone</string> + <string name="art_src_folder">Pasta do jogo</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">Seu arquivo</string> + <string name="art_src_exe">Do exe</string> + <string name="art_src_auto">Automático</string> + <string name="art_pick_image">Escolher imagem…</string> + <string name="art_pick_title">Escolha uma imagem: %1$s</string> + <string name="art_reset">Voltar ao automático</string> + <plurals name="games_added_n"> + <item quantity="one">%1$d jogo adicionado</item> + <item quantity="many">%1$d jogos adicionados</item> + <item quantity="other">%1$d jogos adicionados</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="one">%1$d já está em Jogos</item> + <item quantity="many">%1$d já estão em Jogos</item> + <item quantity="other">%1$d já estão em Jogos</item> + </plurals> + <string name="hop_back">B · Voltar para %1$s</string> + <string name="hop_from_games">o jogo</string> + <string name="hop_from_play">Configurações de jogo</string> + <string name="sheet_graphics_drivers">Drivers gráficos</string> + <string name="comp_this_proton">Este Proton</string> + <string name="comp_every_proton">Todos os Protons</string> + <string name="comp_for_every_proton">Para todos os Protons</string> + <string name="comp_available_count">Disponíveis · %1$d</string> + <string name="comp_checked">Verificado %1$s</string> + <string name="comp_check_all">Procurar novos componentes e drivers</string> + <string name="comp_get">Obter</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton e FEX abaixo são deste Proton</string> + <string name="comp_device_tested">%1$s · testado</string> + <string name="comp_device_untested">%1$s · não testado</string> + <string name="comp_device_unsupported">%1$s · incompatível</string> + <string name="gpu_auto_keeps">Mantém o par recomendado atualizado</string> + <string name="gpu_bar_auto">Automático · %1$s</string> + <string name="gpu_separately">Drivers do runtime e de vídeo separados</string> + <string name="gpu_use_title">Usar %1$s?</string> + <string name="proton_remove_title">Remover %1$s?</string> + <string name="setup_tab_diagnostics">Diagnóstico</string> + <string name="setup_ready">Pronto para jogar</string> + <string name="setup_not_ready">Ainda não está pronto para jogar</string> + <string name="setup_ready_runtime">runtime %1$s</string> + <string name="setup_ready_limit_off">limite de processos desativado</string> + <string name="setup_ready_signed_in">conectado como %1$s</string> + <string name="setup_needs_you">Quando algo precisar de você</string> + <string name="setup_group_look">Aparência</string> + <string name="setup_group_device">Este dispositivo</string> + <string name="setup_group_art">Artes da biblioteca</string> + <string name="flathub_eyebrow">APPS LINUX</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Apps para a área de trabalho Linux, abertos pela Área de trabalho.</string> + <string name="flathub_setup">Configurar o Flathub</string> + <string name="flathub_remove_title">Remover %1$s?</string> + <string name="flathub_remove_note">As configurações e os arquivos dele são mantidos.</string> + <string name="flathub_keep">Manter</string> + <string name="desktop_size">Tamanho em disco</string> + <string name="desktop_uninstall_title">Desinstalar a área de trabalho?</string> + <string name="desktop_all_systems">Todos</string> + <string name="gog_achievements">Conquistas da GOG</string> + <string name="gog_achievements_hint">Sincroniza conquistas dos jogos compatíveis da GOG. Vale a partir da próxima abertura.</string> + <string name="gog_achievements_off">Conquistas desativadas</string> + <string name="gog_achievements_on">Conquistas ativadas</string> + <string name="gog_achievements_signin">Entre na GOG novamente</string> + <string name="gog_achievements_uploaded">Conquistas enviadas</string> + <string name="gog_achievements_pending">Envio de conquista pendente</string> + <string name="gog_achievements_enable_store">Ative as conquistas da GOG nas configurações de Lojas</string> + <string name="gog_achievements_unavailable">Conquistas indisponíveis; veja os registros da sessão</string> + <string name="achievement_unlocked">Conquista desbloqueada</string> + <string name="touch_paste">Colar</string> + <string name="touch_dpad">Direcional</string> + <string name="touch_left_stick">Analógico esquerdo</string> + <string name="touch_right_stick">Analógico direito</string> + <string name="touch_menu">Menu</string> + <string name="touch_keyboard">Teclado</string> + <string name="touch_dpad_up">Direcional para cima</string> + <string name="touch_dpad_down">Direcional para baixo</string> + <string name="touch_dpad_left">Direcional para a esquerda</string> + <string name="touch_dpad_right">Direcional para a direita</string> + <string name="touch_title">Controles por toque</string> + <string name="touch_controller">Controle por toque</string> + <string name="touch_mouse">Mouse</string> + <string name="touch_on">Ativado</string> + <string name="touch_off">Desativado</string> + <string name="touch_speed_slow">Lenta</string> + <string name="touch_speed_normal">Normal</string> + <string name="touch_speed_fast">Rápida</string> + <string name="touch_edit_layout">Editar layout</string> + <string name="touch_droiddeck_menu">Menu do DroidDeck</string> + <string name="touch_drag_hint">Arraste os controles para a tela</string> + <string name="touch_arrange">Organizar</string> + <string name="touch_discard">Descartar</string> + <string name="touch_saving">Salvando…</string> + <string name="touch_icon">Ícone</string> + <string name="touch_narrower">L−</string> + <string name="touch_wider">L+</string> + <string name="touch_shorter">A−</string> + <string name="touch_taller">A+</string> + <string name="touch_hide">Ocultar</string> + <string name="touch_color_opacity">Cor e opacidade…</string> + <string name="touch_copy_layout">Copiar este layout</string> + <string name="touch_layout_copied">Layout copiado; cole no layout de toque de qualquer jogo</string> + <string name="touch_paste_layout">Colar layout copiado</string> + <string name="touch_reset_layout">Restaurar o padrão da Steam</string> + <string name="touch_no_binding">Este controle não tem uma ação vinculada na Steam para receber um ícone</string> + <string name="touch_icon_for">Ícone para %1$s</string> + <string name="touch_layout_color">Cor do layout de toque</string> + <string name="touch_done">Concluído</string> + <string name="touch_save_failed">Não foi possível salvar o layout de toque</string> + <string name="touch_saved">Layout de toque salvo na Steam</string> + <string name="touch_app_id">App %1$d</string> + <string name="touch_label_hint">Rótulo (opcional)</string> + <string name="touch_icons_missing">Os ícones da Steam não foram encontrados</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d ícone da Steam</item> + <item quantity="many">%1$d ícones da Steam</item> + <item quantity="other">%1$d ícones da Steam</item> + </plurals> + <string name="touch_icon_color">Cor do ícone</string> + <string name="touch_button_color">Cor do botão</string> + <string name="touch_no_icon">Sem ícone</string> + + <string name="stores_sync_sign_in">Verificando login…</string> + <string name="stores_sync_library">Buscando sua biblioteca…</string> + <string name="stores_sync_game_list">Buscando lista de jogos…</string> + <string name="stores_sync_details">Carregando detalhes dos jogos… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">Sincronizando %1$d jogo…</item> + <item quantity="many">Sincronizando %1$d jogos…</item> + <item quantity="other">Sincronizando %1$d jogos…</item> + </plurals> + <string name="stores_sync_progress">Sincronizando… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Buscando capas dos jogos…</string> + <string name="stores_sync_failed">Falha na sincronização</string> + <string name="stores_sync_no_games">Nenhum jogo encontrado na sua biblioteca da %1$s</string> + <string name="stores_sync_amazon_no_answer">A Amazon Games não respondeu sobre sua biblioteca: atualize, ou faça login novamente</string> + <string name="stores_sync_gog_unreachable">Não foi possível conectar à GOG</string> + <string name="stores_sync_gog_unreadable">Não foi possível ler sua biblioteca da GOG</string> + <string name="stores_registering">Registrando na Steam…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Modo Cardboard</string> + <string name="cardboard_mode_hint">Imagens para o olho esquerdo e o direito, para um óculos de RV de celular.</string> + <string name="cardboard_source">Imagem</string> + <string name="cardboard_source_screen">Tela virtual</string> + <string name="cardboard_source_sbs">3D lado a lado</string> + <string name="cardboard_viewer">Óculos</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Sem lente</string> + <string name="cardboard_ipd">Distância entre os olhos</string> + <string name="cardboard_screen_width">Largura da tela</string> + <string name="cardboard_screen_distance">Distância da tela</string> + <string name="cardboard_head_tracking">Rastreamento da cabeça</string> + <string name="cardboard_no_sensor">Este dispositivo não tem sensor de rotação.</string> + <string name="cardboard_recenter">Recentralizar visão</string> + <string name="cardboard_recenter_button">Recentralizar</string> + <string name="cardboard_steamvr">SteamVR (experimental)</string> + <string name="cardboard_steamvr_install">Instalar</string> + <string name="cardboard_steamvr_install_hint">Baixa o SteamVR ARM64 da Valve (571 MB).</string> + <string name="cardboard_steamvr_installing">Baixando e descompactando o SteamVR…</string> + <string name="cardboard_steamvr_start">Iniciar</string> + <string name="cardboard_steamvr_ready_hint">Executa o SteamVR usando sua cabeça como headset; a imagem muda para 3D lado a lado.</string> + <string name="cardboard_steamvr_stop">Parar</string> + <string name="cardboard_steamvr_running_hint">O SteamVR está em execução.</string> + <string name="cardboard_steamvr_unavailable">Indisponível nesta sessão.</string> + <string name="cardboard_steamvr_failed">O SteamVR não respondeu; veja o registro da sessão.</string> + <string name="drawer_component_downloads">Baixar componentes</string> + <string name="drawer_component_downloads_hint">Os downloads são compartilhados com a página Componentes. Escolha acima uma versão baixada para usá-la na próxima vez que um jogo for aberto.</string> + <string name="drawer_component_choose_download">Escolha uma versão para baixar</string> + <string name="stores_fact_developer">Desenvolvedora</string> + <string name="stores_fact_publisher">Publicadora</string> + <string name="stores_fact_released">Lançamento</string> + <string name="stores_hltb_main">Principal</string> + <string name="stores_hltb_extras">+ Extras</string> + <string name="stores_hltb_completionist">Completista</string> + <string name="stores_hltb_hours">%1$s h</string> + <string name="stores_hltb_minutes">%1$d min</string> +</resources> diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml new file mode 100644 index 000000000..8c01376ce --- /dev/null +++ b/app/src/main/res/values-ro/strings.xml @@ -0,0 +1,2260 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>Prima rulare descarcă Steam. Poate dura câteva minute.</item> + <item>În mod implicit, la părăsirea aplicației, Steam este pus pe pauză; continuă de unde ai rămas când te întorci.</item> + <item>Controllerul tău apare ca un gamepad Xbox 360. Altfel, folosește comenzile de pe ecran.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>Prima rulare configurează desktopul. Poate dura câteva minute.</item> + <item>În mod implicit, la părăsirea aplicației, sesiunea este pusă pe pauză; continuă de unde ai rămas când te întorci.</item> + <item>Atingerea funcționează ca un touchpad: trage pentru a mișca cursorul, apasă scurt pentru a face clic.</item> + <item>Controllerul tău apare ca un gamepad Xbox 360. Altfel, folosește comenzile de pe ecran.</item> + </string-array> + <string name="session_channel">Sesiune</string> + <string name="session_channel_description">Apare cât timp o sesiune Steam sau desktop este activă</string> + <string name="session_notification">Steam este în execuție</string> + <string name="session_notification_downloads">Descărcare Steam activă</string> + <string name="session_notification_paused">Sesiune pusă pe pauză - apasă Reluare când ești pregătit</string> + <string name="resume_session">Reluare</string> + <string name="suspend_now">Suspendă acum</string> + <string name="stop_session">Oprește sesiunea</string> + <string name="frame_gen_title">Generare de cadre</string> + <string name="frame_gen_hint">Selectează modul de generare a cadrelor</string> + <string name="frame_gen_off">Dezactivat</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG Adaptiv %1$d</string> + <string name="lsfg_needs_steam">Necesită Lossless Scaling pe Steam</string> + <string name="lsfg_needs_install">Instalează Lossless Scaling în Steam sau importă Lossless.dll</string> + <string name="lsfg_import">Importă Lossless.dll…</string> + <string name="lsfg_using_steam">Se folosește copia din Steam</string> + <string name="lsfg_using_import">Se folosește o copie importată</string> + <string name="lsfg_pick_title">Alege Lossless.dll</string> + <string name="lsfg_imported">Lossless.dll importat</string> + <string name="lsfg_import_unchanged">Acest Lossless.dll este deja în uz</string> + <string name="lsfg_import_not_owned">Lossless Scaling nu este deținut pe acest cont Steam</string> + <string name="lsfg_import_invalid">Acest fișier nu este Lossless.dll al Lossless Scaling</string> + <string name="lsfg_import_failed">Nu s-a putut pregăti LSFG din acest Lossless.dll</string> + <string name="lsfg_problem_missing">LSFG are nevoie de Lossless Scaling din Steam sau de un Lossless.dll importat</string> + <string name="lsfg_problem_failed">LSFG nu a putut pregăti shaderele din Lossless.dll</string> + <string name="hud_fg_unsupported">%1$s: neacceptat</string> + <string name="hud_fg_failed">%1$s: motor eșuat</string> + <string name="hud_fg_needs_lossless">%1$s: necesită Lossless Scaling</string> + <string name="hud_fg_starting">%1$s %2$s · se pornește</string> + <string name="second_library_import_hint">Adaugă o bibliotecă Steam și o scanează pentru jocuri Windows. Jocurile deținute folosesc intrările lor Steam; restul devin comenzi rapide non-Steam.</string> + <string name="game_env_title">Mediu de joc</string> + <string name="game_env_hint">Variabile de mediu pentru jocurile Proton.</string> + <string name="game_env_edit">Editează</string> + <string name="game_env_shared">Toate jocurile Proton</string> + <string name="game_env_scope">Profil</string> + <string name="game_env_app_id">ID-ul aplicației Steam</string> + <string name="game_env_open">Deschide</string> + <string name="game_env_profile">Jocul %1$s</string> + <string name="game_env_add">Adaugă variabilă</string> + <string name="game_env_name">Nume variabilă</string> + <string name="game_env_value">Valoare</string> + <string name="game_env_save">Salvează</string> + <string name="game_env_remove">Elimină</string> + <string name="game_env_restore">Moștenește din nou</string> + <string name="game_env_reset">Șterge toate suprascrierile de mediu</string> + <string name="game_env_unset">Dezactivează pentru acest profil</string> + <string name="game_env_empty">Nicio variabilă definită.</string> + <string name="game_env_inherited">Moștenită</string> + <string name="game_env_custom">Variabilă personalizată</string> + <string name="game_env_error">Nu s-a putut încărca sau salva. Verifică spațiul de stocare liber și încearcă din nou.</string> + <string name="game_env_invalid">Folosește un nume valid și o valoare de cel mult 8192 de caractere.</string> + <string name="game_env_unused">Mediul de execuție Proton standard nu citește această variabilă.</string> + <string name="game_env_loading">Se încarcă…</string> + <string name="game_env_feature">Suprascrierea nivelului de funcții D3D12. 12_2 raportează capabilitățile DirectX 12 Ultimate; funcțiile neacceptate pot eșua. Lasă nesetat pentru detectare automată.</string> + <string name="game_env_shader">Suprascrierea modelului de shadere D3D12. 6_9 necesită un VKD3D suficient de recent și funcții GPU corespunzătoare. Lasă nesetat pentru detectare automată.</string> + <string name="game_env_cache">Păstrează memorarea în cache a shaderelor Mesa activată pentru a reduce compilarea repetată. false activează memorarea în cache.</string> + <string name="game_env_cache_size">Dimensiunea maximă a cache-ului de shadere Mesa, de exemplu 1G. Cache-urile mai mari folosesc mai mult spațiu de stocare.</string> + <string name="game_env_vkd3d_config">Opțiuni VKD3D pentru D3D12. nodxr dezactivează ray tracing-ul; folosește doar pentru un joc care are nevoie de el.</string> + <string name="game_env_proton_log">Setează la 1 pentru un jurnal de diagnosticare Proton. Înregistrarea poate reduce performanța și folosește spațiu de stocare.</string> + <string name="game_env_wined3d">Setează la 1 pentru a folosi WineD3D bazat pe OpenGL în locul DXVK, pentru testarea compatibilității D3D9–11.</string> + <string name="game_env_no_ntsync">Setează la 1 pentru a păstra acest joc în afara ntsync (droiddeck-ntsync sau un driver de kernel); folosește droiddeck-esync unde există un pachet, altfel droiddeck-fsync.</string> + <string name="game_env_no_esync">Setează la 1 pentru a dezactiva droiddeck-esync pentru acest joc; folosește în schimb droiddeck-fsync, sau droiddeck-ntsync unde fsync este dezactivat și el.</string> + <string name="game_env_no_fsync">Setează la 1 pentru a păstra acest joc în afara droiddeck-fsync; folosește droiddeck-esync unde există un pachet, altfel droiddeck-ntsync.</string> + <string name="game_env_fsync_first">Setează la 1 pentru a rula acest joc pe droiddeck-fsync, sau la 0 pentru a pune droiddeck-esync primul unde un pachet se potrivește cu Proton-ul său.</string> + <string name="game_env_xalia">Setează la 0 pentru a dezactiva asistentul de navigare cu gamepad al Proton pentru acest joc.</string> + <string name="game_env_hud">Suprapunere DXVK pentru D3D9–11. fps afișează rata de cadre; nesetat o ascunde.</string> + <string name="game_env_dxvk_config">Setări DXVK separate prin punct și virgulă. Versiunile recente folosesc dxvk.maxFrameRate pentru limitarea cadrelor; preferă limitatorul de cadre al sesiunii pentru joc obișnuit.</string> + <string name="game_env_vkd3d_limit">Limita ratei de cadre D3D12. 0 dezactivează limita.</string> + <string name="game_env_glthread">Procesarea pe fire a Mesa OpenGL. true poate ajuta jocurile OpenGL limitate de CPU; testează pentru fiecare joc.</string> + <string name="sync_backend_title">Sincronizare Wine</string> + <string name="sync_backend_hint">Modul în care jocurile își sincronizează firele. Se aplică la sesiunea următoare.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">Preset FEX</string> + <string name="fex_next_launch">Se aplică la următoarea lansare a jocului.</string> + <string name="game_settings_title">Jocuri</string> + <string name="fex_default_label">Valori implicite FEX</string> + <string name="fex_default_detail">Folosește valorile implicite FEX.</string> + <string name="fex_stability_label">Stabilitate</string> + <string name="fex_stability_detail">Setările cele mai sigure, dar cele mai lente.</string> + <string name="fex_compatibility_label">Compatibilitate</string> + <string name="fex_compatibility_detail">TSO complet cu multiblock.</string> + <string name="fex_intermediate_label">Intermediar</string> + <string name="fex_intermediate_detail">TSO echilibrat cu precizie x87 redusă.</string> + <string name="fex_performance_label">Performanță</string> + <string name="fex_performance_detail">TSO complet dezactivat; multiblock.</string> + <string name="fex_performance_tso_label">Performanță + TSO</string> + <string name="fex_performance_tso_detail">Setările de performanță cu TSO principal activat.</string> + <string name="fex_extreme_label">Extrem</string> + <string name="fex_extreme_detail">Dezactivează verificările de cod auto-modificabil. Jocurile JIT și DRM-ul pot să nu mai funcționeze.</string> + <string name="fex_extreme_tso_label">Extrem + TSO</string> + <string name="fex_extreme_tso_detail">Setările extreme cu TSO principal activat.</string> + <string name="fex_extreme_gn_label">Extreme-gn</string> + <string name="fex_extreme_gn_detail">Performanță cu indicii TSC și metadate. Încearcă înainte de Extrem.</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">Performanță cu verificări complete de cod și bitul de hipervizor ascuns.</string> + <string name="game_env_custom_value">Valoare personalizată</string> + <string name="game_env_select_value">Selectează valoarea</string> + <string name="user_apps_section">Emulatoare & aplicații</string> + <string name="user_apps_add">Adaugă</string> + <string name="user_apps_add_detail">Propria ta aplicație sau joc</string> + <string name="user_apps_kind_script">Script</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d%%</string> + <string name="user_apps_starting">Se pornește…</string> + <string name="user_apps_added">%1$s adăugat</string> + <string name="user_apps_removed">%1$s eliminat</string> + <string name="user_apps_back">Desktop</string> + <string name="user_apps_gone">Aplicația nu mai există. Alege-o din nou din Desktop.</string> + <string name="user_apps_open">Deschide %1$s</string> + <string name="user_apps_remove">Elimină</string> + <string name="user_apps_remove_confirm">Apasă din nou pentru a elimina</string> + <string name="user_apps_stop_session">Oprește sesiunea pentru a elimina</string> + <string name="user_apps_script_linked">Rulează %1$s din folderul său, conectat în sesiune acolo unde se află. Eliminarea aplicației nu afectează acel folder.</string> + <string name="user_apps_script_copied">Rulează %1$s dintr-o copie a folderului său din DroidDeck. Eliminarea aplicației șterge copia, nu originalul.</string> + <string name="user_apps_runtime_required">Instalează mai întâi mediul de execuție Linux</string> + <string name="user_apps_file_gone">Fișierul nu mai există</string> + <string name="user_apps_no_space">Spațiu liber insuficient: copierea folderului necesită %1$s</string> + <string name="user_apps_copying">Se copiază %1$s</string> + <string name="user_apps_failed">Adăugarea aplicației a eșuat</string> + <string name="user_apps_github_checking">Se caută o versiune a %1$s</string> + <string name="user_apps_github_unreachable">Nu s-au putut citi versiunile %1$s</string> + <string name="user_apps_github_no_appimage">Niciuna dintre versiunile %1$s nu are un AppImage ARM64 sau x86_64</string> + <string name="user_apps_downloading">Se descarcă %1$s</string> + <string name="user_apps_download_failed">Descărcarea a eșuat</string> + <string name="user_apps_update_failed">Actualizarea aplicației a eșuat</string> + <string name="user_apps_icon_failed">Acea iconiță nu a putut fi citită</string> + <string name="user_apps_saved">%1$s salvat</string> + <string name="user_apps_edit">Editează</string> + <string name="user_apps_check_updates">Verifică actualizările</string> + <string name="user_apps_checking_updates">Se verifică actualizările…</string> + <string name="user_apps_update_to">Actualizează la %1$s</string> + <string name="user_apps_up_to_date">Actualizat · %1$s</string> + <string name="user_apps_updated">%1$s actualizat</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">Adaugă o aplicație</string> + <string name="add_app_tab_script">Script</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">Un fișier .sh. Rulează din propriul folder, pe tot ecranul, sub gamescope.</string> + <string name="add_app_appimage_hint">Un AppImage ARM64 (aarch64), sau unul x86_64 care rulează prin FEX. Este despachetat o singură dată în mediul de execuție Linux.</string> + <string name="add_app_github_hint">Instalează AppImage-ul ARM64 din cea mai recentă versiune a depozitului, sau pe cel x86_64 prin FEX.</string> + <string name="app_fex_title">Folosește FEX</string> + <string name="app_fex_auto">Automat</string> + <string name="app_fex_on">Activat</string> + <string name="app_fex_off">Dezactivat</string> + <string name="app_fex_hint">FEX rulează programe Linux x86 pe acest dispozitiv ARM64. Automat îl folosește atunci când programul este compilat pentru x86.</string> + <string name="app_fex_x86">%1$s prin FEX</string> + <string name="app_fex_not_ready">Acest program este compilat pentru PC-uri %1$s și rulează prin FEX, care nu este încă configurat. Deschide Steam o dată, fiind autentificat: DroidDeck îl pune să descarce FEX și Steam Linux Runtime de care are nevoie, apoi acesta se deschide.</string> + <string name="session_end_program">Acest program</string> + <string name="session_end_fex_missing">%1$s este compilat pentru PC-uri x86 și rulează prin FEX, care nu este încă instalat. Pornește Steam o dată, fiind autentificat, și lasă-l să descarce FEX și Steam Linux Runtime, apoi pornește din nou acesta.</string> + <string name="add_app_flatpak_hint">Un ID de aplicație Flathub sau pagina sa Flathub. Flatpak este configurat mai întâi, dacă nu este deja.</string> + <string name="add_app_file">Fișier</string> + <string name="add_app_no_file">Niciun fișier ales</string> + <string name="add_app_choose_file">Alege fișierul</string> + <string name="add_app_change_file">Schimbă</string> + <string name="add_app_pick_script">Alege un script</string> + <string name="add_app_pick_appimage">Alege un AppImage</string> + <string name="add_app_pick_icon">Alege o iconiță</string> + <string name="add_app_repository">Depozit</string> + <string name="add_app_repository_placeholder">proprietar/depozit sau un link github.com</string> + <string name="add_app_repository_invalid">Nu este un depozit GitHub</string> + <string name="add_app_flatpak_id">ID aplicație</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">Nu este un ID de aplicație Flathub</string> + <string name="add_app_flatpak_bundle_hint">Sau un fișier .flatpak, pentru o aplicație care nu este pe Flathub.</string> + <string name="add_app_pick_flatpak">Alege un fișier .flatpak</string> + <string name="add_app_source">Sursă</string> + <string name="add_app_appearance">Pe Desktopul tău</string> + <string name="add_app_preview_name">Aplicație nouă</string> + <string name="add_app_name">Nume</string> + <string name="add_app_name_placeholder">Din aplicație</string> + <string name="add_app_icon">Iconiță</string> + <string name="add_app_icon_default">Iconiță implicită</string> + <string name="add_app_choose_icon">Alege iconița</string> + <string name="add_app_clear_icon">Șterge</string> + <string name="add_app_icon_suggestions">Din depozit</string> + <string name="add_app_icon_searching">Se caută iconițe…</string> + <string name="add_app_icon_reset">Implicit</string> + <string name="edit_app_title">Editează %1$s</string> + <string name="edit_app_save">Salvează</string> + <string name="add_app_checking_folder">Se verifică folderul…</string> + <string name="add_app_folder_linked">Folderul său este conectat în sesiune: %1$s</string> + <string name="add_app_folder_copied">Folderul său conține programe Linux, pe care stocarea partajată nu le poate rula, așa că este copiat în DroidDeck (%1$s).</string> + <string name="add_app_confirm">Adaugă</string> + <string name="add_app_cancel">Anulează</string> + <string name="add_app_runtime_required">Instalează mai întâi mediul de execuție Linux din Configurare.</string> + <string name="add_app_busy">Așteaptă până se termină %1$s.</string> + <string name="steam_compat_label">Compatibil</string> + <string name="proton_compatible_note">Versiunile marcate „%1$s” din meniul Compatibilitate al Steam au remedierile de prefix ale DroidDeck și pornesc jocuri aici. Cea implicită este Proton Experimental ARM64 (%1$s), sau Proton 11.0 ARM64 (%1$s) după ce acea versiune mai nouă este instalată. Intrările Proton proprii ale Valve, fără marcaj, nu pot porni un joc pe acest dispozitiv.</string> + <string name="proton_installed_compatible">Instalat %1$s · %2$s</string> + <string name="common_cancel">Anulează</string> + <string name="common_auto">Automat</string> + <string name="common_never">Niciodată</string> + <string name="common_always">Întotdeauna</string> + <string name="common_uninstall">Dezinstalează</string> + <string name="common_check">Verifică</string> + <string name="common_update">Actualizează</string> + <string name="common_checking">Se verifică…</string> + <string name="common_working">Se procesează…</string> + <string name="common_forget">Uită</string> + <string name="common_add_ellipsis">Adaugă…</string> + <string name="common_use">Folosește</string> + <string name="common_applies_next_session">Se aplică la sesiunea următoare.</string> + <string name="mode_steam_title">Sesiune Steam</string> + <string name="mode_desktop_title">Sesiune Desktop</string> + <string name="mode_display">Afișaj</string> + <string name="mode_resolution">Rezoluție</string> + <string name="mode_res_720">Până la 720p</string> + <string name="mode_res_900">Până la 900p</string> + <string name="mode_res_1080">Până la 1080p</string> + <string name="mode_res_native">Nativă</string> + <string name="mode_res_default">%1$s (implicit)</string> + <string name="mode_res_custom_value">Personalizat · %1$d×%2$d</string> + <string name="mode_res_custom">Personalizat…</string> + <string name="mode_res_note">720p poate face meniurile mai receptive.</string> + <string name="mode_ratio">Raport de ecran</string> + <string name="mode_ratio_custom">Stabilit de rezoluția personalizată.</string> + <string name="mode_ratio_auto">Automat folosește cel puțin 16:9.</string> + <string name="mode_fps">Limită de cadre</string> + <string name="mode_fps_note">Limitează întreaga sesiune. Afișajul trece la o rată de reîmprospătare pe care limita o împarte exact.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">Ieșire HDR10</string> + <string name="mode_hdr_unavailable">Indisponibil: %1$s.</string> + <string name="mode_hdr_restart">Repornește aplicația pentru a aplica.</string> + <string name="mode_drivers">Drivere</string> + <string name="mode_gpu_drivers">Drivere GPU</string> + <string name="mode_gpu_drivers_hint">Partajate de Steam și Desktop. Se stabilesc pe pagina Componente.</string> + <string name="mode_touch_controls">Atingere & comenzi</string> + <string name="mode_touch">Atingere</string> + <string name="mode_touch_touchpad">Touchpad</string> + <string name="mode_touch_direct">Direct</string> + <string name="mode_touch_note">Automat: touchpad pe Desktop, direct în Steam.</string> + <string name="mode_osc">Comenzi pe ecran</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">Automat afișează comenzile când niciun controller nu este conectat.</string> + <string name="osc_steam_touch">Atingere Steam (beta)</string> + <string name="osc_style_next_session">Comenzile tactile Steam pornesc odată cu următoarea sesiune Steam</string> + <string name="osc_steam_layout">Aranjament tactil Steam</string> + <string name="osc_steam_layout_hint">Mută, redimensionează sau ascunde comenzile tactile ale acestui joc; salvate în Steam</string> + <string name="mode_controller">Controller</string> + <string name="mode_controller_hint">Cum vede Steam controllerul tău. Se aplică la sesiunea următoare.</string> + <string name="mode_controller_deck">Controller Steam Deck</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Controller Xbox 360</string> + <string name="mode_controller_note">Deck: butonul Acces rapid și giroscopul. Steam Controller: la fel, cu senzori de strângere pe butoanele din spate. DualShock 4 și DualSense: giroscop, și trackpadurile ca touchpad propriu. Xbox 360: deschide Acces rapid cu Guide+A.</string> + <string name="mode_back">Înapoi</string> + <string name="back_menu_then_qam">Meniu → QAM</string> + <string name="back_qam_then_menu">QAM → Meniu</string> + <string name="mode_session">Sesiune</string> + <string name="mode_suspend">Comportament în fundal</string> + <string name="mode_suspend_hint">Când aplicația este ascunsă sau ecranul se stinge.</string> + <string name="mode_suspend_manual">Manual</string> + <string name="mode_suspend_native">Nativ</string> + <string name="mode_suspend_steam_note">Automat pune pe pauză și reia atunci când este afișată. Nativ pregătește Steam pentru repaus mai întâi. Manual așteaptă Reluare. Niciodată continuă să ruleze.</string> + <string name="mode_background_downloads">Continuă descărcările</string> + <string name="mode_background_downloads_hint">Întârzie suspendarea în fundal cât timp Steam descarcă activ.</string> + <string name="mode_suspend_note">Automat pune pe pauză în fundal și reia atunci când este afișată. Manual așteaptă Reluare. Niciodată continuă să ruleze.</string> + <string name="mode_startup">Pornire</string> + <string name="mode_steam_startup">Pornește Steam odată cu DroidDeck</string> + <string name="mode_steam_startup_hint">Deschide sesiunea Steam la lansarea DroidDeck.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">Se verifică versiunile…</string> + <string name="mode_decky_ready">Pregătit pentru instalarea %1$s.</string> + <string name="mode_decky_none">Nu s-a găsit o versiune compatibilă. Încearcă din nou mai târziu.</string> + <string name="mode_decky_installed">Instalat · %1$s</string> + <string name="mode_decky_update">Actualizare disponibilă · %1$s</string> + <string name="mode_decky_current">Actualizat · %1$s</string> + <string name="mode_decky_loader">Loader</string> + <string name="mode_decky_install_latest">Instalează cea mai recentă versiune</string> + <string name="mode_decky_on">Pornește odată cu Steam. Cât timp este activ, alte aplicații de pe acest dispozitiv pot fi capabile să controleze Steam.</string> + <string name="mode_decky_off">Dezactivat. Portul de depanare la distanță al Steam rămâne închis.</string> + <string name="mode_decky_remove_title">Dezinstalezi Decky Loader?</string> + <string name="mode_decky_remove_text">Elimină loader-ul. Pluginurile și setările tale sunt păstrate.</string> + <string name="mode_client">Client</string> + <string name="mode_mangoapp">Suprapunere de performanță</string> + <string name="mode_mangoapp_hint">Dezactivează dacă jocurile se blochează sau ecranul devine negru în modul Deck. Se aplică la sesiunea următoare.</string> + <string name="mode_steam_repair">Repară clientul Steam</string> + <string name="mode_steam_repair_hint">Descarcă din nou clientul Steam la următoarea pornire a Steam. Folosește-l dacă Steam rămâne blocat pe ecranul de încărcare. Jocurile, autentificarea și setările sunt păstrate.</string> + <string name="mode_steam_repair_queued">Clientul Steam va fi descărcat din nou la următoarea pornire a Steam.</string> + <string name="mode_steam_repair_button">Repară</string> + <string name="mode_added_art">Grafică</string> + <string name="mode_sgdb_key">Cheie API SteamGridDB</string> + <string name="mode_sgdb_key_yours">Cheia ta</string> + <string name="mode_sgdb_edit">Editează…</string> + <string name="mode_added_art_hint">Completează grafica lipsă din Steam, apoi din SteamGridDB. Propriile tale cover.jpg, hero.jpg, logo.png sau icon.png din folderul jocului au prioritate.</string> + <string name="summary_remove_game">Elimină %1$s</string> + <string name="mode_added_remove_confirm">Elimini?</string> + <string name="games_add">Adaugă joc</string> + <string name="games_add_title">Adaugă un joc:</string> + <string name="games_pick_exe_title">Alege fișierul exe:</string> + <string name="games_add_unreachable">Această locație nu este disponibilă în sesiune</string> + <string name="mode_added_exe_check">Verifică acest exe</string> + <string name="wincomp_title">Componente Windows</string> + <string name="wincomp_pad_hint">LB / RB trec între secțiuni.</string> + <string name="wincomp_card_none">Niciuna</string> + <string name="wincomp_applies">DLL-uri Windows pe care Proton nu le include, din lista de componente. Activează-le pe cele de care are nevoie acest joc: se adaugă în folderul Windows propriu al jocului, niciodată în fișierele sale, și rămân indiferent pe ce Proton rulează.</string> + <string name="wincomp_offline">Lista de componente nu a putut fi accesată. Componentele deja descărcate pot fi în continuare comutate.</string> + <string name="wincomp_recommended">Recomandate pentru acest joc</string> + <string name="wincomp_checking">Se verifică de ce are nevoie acest joc…</string> + <string name="wincomp_when_online">Disponibil când ești online</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">Se activează %1$d componentă…</item> + <item quantity="few">Se activează %1$d componente…</item> + <item quantity="other">Se activează %1$d de componente…</item> + </plurals> + <string name="wincomp_auto_section">Activ pentru acest joc</string> + <string name="wincomp_auto_steam">Steam instalează %1$s cu %2$s (aplicația %3$s)</string> + <string name="wincomp_auto_gog">Din lista GOG pentru acest joc</string> + <string name="wincomp_auto_epic">Din cerințele preliminare Epic pentru acest joc</string> + <string name="wincomp_auto_amazon">Din pașii de instalare Amazon pentru acest joc</string> + <string name="wincomp_steam_by_name">Steam instalează %1$s cu aplicația %2$s, potrivită după nume</string> + <string name="wincomp_suggestions">De asemenea, listate</string> + <string name="wincomp_suggestion_reason">Listată de %1$s; nu este o componentă aici</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">Jocul include %1$s</string> + <string name="wincomp_found_shipped">Jocul are %1$s (funcționează de obicei ca atare)</string> + <string name="wincomp_already_here">Deja prezentă în acest joc</string> + <string name="wincomp_no_recommendation">Nimic din folderul jocului nu indică o componentă.</string> + <string name="wincomp_active">Instalate pentru acest joc (%1$d)</string> + <string name="wincomp_all">Toate componentele (%1$d)</string> + <string name="wincomp_installers">Necesită un program de instalare Windows (%1$d)</string> + <string name="wincomp_installers_note">Acestea sunt propriile lor programe de instalare, fără pachet în interior de despachetat, și fără o înregistrare a ceea ce instalează încă. Majoritatea sunt medii de execuție vechi pe care Mono din Proton le înlocuiește deja; înregistrările sunt adăugate pe măsură ce sunt create.</string> + <string name="wincomp_show">Arată</string> + <string name="wincomp_hide">Ascunde</string> + <string name="wincomp_needs_installer">Rulează propriul program de instalare (fără înregistrare încă)</string> + <string name="wincomp_unsupported">Neacceptat încă aici</string> + <string name="wincomp_loading">Se încarcă componentele…</string> + <string name="wincomp_failed">Nu s-a putut instala: %1$s</string> + <string name="wincomp_downloaded">Descărcat</string> + <string name="wincomp_downloading">Se descarcă %1$s</string> + <string name="wincomp_installing">Se instalează: %1$s</string> + <string name="wincomp_reverting">Se dezactivează…</string> + <string name="wincomp_download_size">Descărcare de %1$s</string> + <string name="wincomp_next_launch">Are efect la următoarea pornire a jocului.</string> + <string name="mode_stretch169">Întinde jocurile 16:9</string> + <string name="mode_stretch169_hint">Pe ecrane mai pătrate decât 16:9, întinde jocurile pentru a umple ecranul, în loc să arate benzi. Se aplică la sesiunea următoare.</string> + <string name="mode_audio">Sunet</string> + <string name="mode_directaudio">DirectAudio pentru jocuri</string> + <string name="mode_directaudio_hint">Evită PulseAudio pentru o latență mai mică.</string> + <string name="mode_mic">Microfon</string> + <string name="mode_mic_hint">Pentru chat vocal.</string> + <string name="mode_network">Rețea</string> + <string name="mode_wifi_discovery">Descoperire Wi-Fi</string> + <string name="mode_wifi_discovery_hint">Arată numele rețelelor Wi-Fi și rețelele din apropiere în Steam.</string> + <string name="mode_wifi_location_off">Serviciile de localizare sunt dezactivate.</string> + <string name="mode_wifi_permission_denied">Permisiunea de localizare nu a fost acordată.</string> + <string name="mode_wifi_open_settings">Deschide setările</string> + <string name="mode_wifi_continue">Continuă</string> + <string name="mode_wifi_explain_title">Activezi descoperirea Wi-Fi?</string> + <string name="mode_wifi_explain_text">Android necesită permisiunea de localizare pentru a citi numele rețelelor Wi-Fi și a descoperi rețelele din apropiere. DroidDeck folosește acest acces pentru a arăta acele rețele în Steam. Nu îți urmărim locația. Accesul la internet funcționează și fără această permisiune.</string> + <string name="mode_storage">Stocarea jocurilor</string> + <string name="mode_storage_auto">Automat - cardul SD când este prezent, plus folderele tale Games</string> + <string name="mode_storage_auto_none">Automat - folderele tale Games, plus cardul SD când este prezent (niciunul acum)</string> + <string name="mode_storage_internal">Doar internă</string> + <string name="mode_storage_folder">Folder: %1$s</string> + <string name="mode_second_library">A doua bibliotecă</string> + <string name="mode_storage_note">Jocurile care transmit fișiere de pe SD sau din stocarea partajată pot avea sacadări.</string> + <string name="mode_storage_diagnostics">Diagnosticare stocare</string> + <string name="mode_storage_diagnostics_hint">Înregistrează rezultatele alocărilor și timpii de stocare eșantionați în Partajare jurnale. Poate încetini instalările cât este activă. Se aplică la sesiunea următoare.</string> + <string name="mode_choose_folder">Alege un folder…</string> + <string name="mode_custom_res_title">Rezoluție personalizată</string> + <string name="mode_custom_res_text">Înlocuiește limita și raportul. O dimensiune care nu se potrivește cu ecranul primește benzi.</string> + <string name="mode_width">Lățime</string> + <string name="mode_height">Înălțime</string> + <string name="mode_custom_res_range">Între 320×240 și 3840×2160.</string> + <plurals name="mode_added_count"> + <item quantity="one">%1$d joc</item> + <item quantity="few">%1$d jocuri</item> + <item quantity="other">%1$d de jocuri</item> + </plurals> + <string name="common_ok">OK</string> + <string name="common_delete">Șterge</string> + <string name="common_restore">Restaurează</string> + <string name="common_download">Descarcă</string> + <string name="common_delete_named">Șterge %1$s</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Driver pentru mediul de execuție</string> + <string name="comp_runtime_driver_hint">Folosit de Steam, jocuri și desktop. Se aplică la sesiunea următoare.</string> + <string name="comp_import_turnip">Importă arhiva Turnip…</string> + <string name="comp_display_driver">Driver de afișaj</string> + <string name="comp_display_driver_hint">Desenează cadrele pe ecran în ambele moduri. Repornește aplicația pentru a aplica.</string> + <string name="comp_import_adrenotools">Importă o arhivă AdrenoTools…</string> + <string name="comp_never">niciodată</string> + <string name="comp_nightlies_checked">Versiunile nightly verificate %1$s</string> + <string name="comp_title">Componente</string> + <string name="comp_about">Despre Componente</string> + <string name="comp_check_drivers">Verifică drivere noi</string> + <string name="comp_check_nightlies">Verifică versiunile nightly pentru pachete noi</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">Proton-ul ales aici rulează orice joc care nu și-a ales propriul în Steam, și o modificare făcută în Steam apare și aici. Steam înlocuiește Proton-ul Valve la actualizare; originalele sunt păstrate pentru fiecare versiune.</string> + <string name="comp_scope">Configurare</string> + <string name="comp_profile_configure">Configurează profilul</string> + <string name="comp_profile">Profil</string> + <string name="comp_scope_general">Profil implicit</string> + <string name="comp_scope_general_detail">Folosit automat de jocurile fără suprascrieri.</string> + <string name="comp_profile_game_detail">Setări salvate, folosite automat la lansarea %1$s.</string> + <string name="comp_game_hint">Alege doar setările de care are nevoie acest joc. Tot restul continuă să folosească profilul implicit.</string> + <string name="comp_game_fex">Preset FEX</string> + <string name="comp_game_fex_inherited">Se folosește presetul profilului implicit.</string> + <string name="comp_game_fex_override">Preset personalizat salvat pentru acest joc.</string> + <string name="comp_game_fex_custom">Variabile FEX personalizate</string> + <string name="comp_game_use_general">Folosește implicit</string> + <string name="comp_game_compatibility">Compatibilitatea jocului</string> + <string name="comp_game_environment_hint">Opțiuni de mediu Proton, DXVK, VKD3D și sincronizare pentru acest joc.</string> + <string name="comp_game_proton_inherited">Se folosește Proton-ul profilului implicit.</string> + <string name="comp_game_proton_override">Proton personalizat salvat pentru acest joc prin maparea de compatibilitate Steam.</string> + <string name="comp_no_proton_short">Fără Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Folosește pachetul profilului implicit din %1$s, dacă acest joc nu selectează alt pachet.</string> + <string name="comp_game_component_general">Folosește pachetul de componente al profilului implicit.</string> + <string name="comp_game_component_override">Folosește %1$s doar pentru acest joc.</string> + <string name="comp_game_runtime_options">Opțiuni pentru mediul de execuție</string> + <string name="comp_game_environment_other_hint">Alte opțiuni de mediu Proton, DXVK, VKD3D-Proton și sincronizare, per joc.</string> + <string name="comp_game_saved_next">Profilul %1$s este salvat pentru următoarea lansare a jocului.</string> + <string name="game_no_steam_title">Pornește fără Steam</string> + <string name="game_no_steam_detail">Pentru un joc care se închide imediat: îl pornește fără configurarea Steam din Proton. Funcțiile Steam, precum overlay-ul, sunt dezactivate pentru el.</string> + <string name="game_no_steam_on">Activat</string> + <string name="comp_game_proton_live">Alegerea Proton pentru %1$s este salvată și disponibilă în Steam acum.</string> + <string name="comp_game_proton_next">Alegerea Proton pentru %1$s este salvată. Repornește Steam înainte de a lansa jocul.</string> + <string name="comp_game_proton_not_runnable">%1$s nu poate folosi Proton-ul selectat pe acest dispozitiv.</string> + <string name="comp_game_load_failed">Profilul %1$s nu a putut fi încărcat: %2$s</string> + <string name="comp_game_save_failed">Profilul jocului nu a putut fi salvat: %1$s</string> + <string name="comp_game_reset">Resetează profilul jocului</string> + <string name="comp_game_reset_title">Resetezi profilul %1$s?</string> + <string name="comp_game_reset_body">Elimini suprascrierile Proton, FEX, pachet de componente, mediu, filtrare de texturi și componente Windows ale acestui joc? Va folosi profilul implicit.</string> + <string name="comp_game_reset_done">%1$s folosește acum profilul implicit.</string> + <string name="comp_game_reset_failed">Profilul jocului nu a putut fi resetat complet: %1$s</string> + <string name="comp_default_live">%1$s rulează acum orice joc care nu și-a ales propriul Proton în Steam.</string> + <string name="comp_default_next">%1$s va rula orice joc care nu și-a ales propriul Proton, de la următoarea pornire a Steam.</string> + <string name="comp_default_not_runnable">%1$s nu poate porni jocuri aici, așa că nu poate fi implicit.</string> + <string name="comp_default_failed">Proton-ul implicit nu a putut fi salvat: %1$s</string> + <string name="comp_swapped">%1$s · %2$d schimbate</string> + <string name="comp_all_original">%1$s · toate originale</string> + <string name="comp_game_running_suffix">%1$s · joc în execuție</string> + <string name="comp_esync_pack">%1$s · pachet droiddeck-esync</string> + <string name="comp_esync_wanted">%1$s · niciun pachet droiddeck-esync încă</string> + <string name="comp_esync_builtin">%1$s · esync integrat, nu necesită pachet</string> + <string name="comp_gpu_drivers">Drivere GPU</string> + <string name="comp_reapplied">%1$s · reaplicat la pornirea %2$s</string> + <string name="comp_next_queued">Următorul: %1$s · după ce jocul se închide</string> + <string name="comp_in_use">%1$s în uz: %2$s</string> + <string name="comp_waits">Așteaptă până se închide jocul</string> + <string name="comp_applies_next_game">Se aplică la următoarea pornire a unui joc</string> + <string name="comp_import_zip">Importă .zip</string> + <string name="comp_reading">Se citesc versiunile Proton…</string> + <string name="comp_no_proton">Niciun Proton instalat încă. Pornește Steam o dată pentru a descărca Proton-ul său ARM64, sau adaugă GE-Proton, CachyOS sau DroidDeck-Proton din Configurare.</string> + <string name="comp_original">Original · %1$s</string> + <string name="comp_this_build">%1$s · această versiune</string> + <string name="comp_earlier_build">%1$s · versiune anterioară</string> + <string name="comp_installed_count">Instalate · %1$d</string> + <string name="comp_nothing_stored">Nimic salvat pentru %1$s încă. Fișierele proprii ale acestui Proton sunt salvate ca original al lui.</string> + <string name="comp_waits_long">Un joc rulează pe acest Proton, așa că aceasta așteaptă până se închide.</string> + <string name="comp_applies_long">Se aplică la următoarea pornire a unui joc.</string> + <string name="comp_swap_title">Schimbi %1$s?</string> + <string name="comp_swap_body">Pui %1$s în %2$s? Fișierele sale originale rămân salvate. %3$s</string> + <string name="comp_restore_title">Restaurezi %1$s?</string> + <string name="comp_restore_body">Pui originalul %1$s înapoi în %2$s? %3$s</string> + <string name="comp_delete_package_body">Elimină pachetul din aplicație. Îl poți descărca din nou.</string> + <string name="comp_delete_original_title">Ștergi acest original?</string> + <string name="comp_delete_original_body">Acest original %1$s este dintr-o versiune anterioară a %2$s. Originalul versiunii curente este păstrat mereu.</string> + <string name="comp_nightlies_count">Pe versiunile nightly · %1$d</string> + <string name="comp_refresh_to_list">Reîmprospătează pentru a lista pachetele de pe versiunile nightly.</string> + <string name="comp_nothing_new">Nimic nou de la ultima verificare.</string> + <string name="comp_import_wcp">Importă .wcp</string> + <string name="comp_restore_original">Restaurează originalul acestui Proton</string> + <string name="comp_restore_original_body">Pui originalul %1$s al %2$s înapoi?</string> + <string name="comp_swap">Schimbă</string> + <string name="comp_about_text">FEX, DXVK și VKD3D-Proton pentru fiecare Proton cu care Steam rulează jocuri.\n\nSelectează un rând instalat pentru a-l pune în uz. Se aplică la următoarea pornire a unui joc, sau după ce jocul în execuție se închide.\n\nAlegerile tale sunt reverificate înainte de fiecare lansare și puse înapoi dacă ceva le-a schimbat. Fișierele originale ale fiecărui Proton sunt păstrate, așa că actualizările Steam nu le pierd niciodată.</string> + <string name="comp_tag_original">Original</string> + <string name="comp_tag_in_use">În uz</string> + <string name="comp_tag_new">Nou</string> + <string name="comp_tag_stored">Salvat</string> + <string name="perf_title">Performanță</string> + <string name="perf_lede">Nuclee CPU, remedieri de sesiune și numele hostului. Se aplică la sesiunea următoare.</string> + <string name="perf_client_cores_group">Nuclee pentru clientul Steam</string> + <string name="perf_override">Suprascrie alegerea de nuclee a Steam</string> + <string name="perf_override_hint">Fixează Steam, asistentul său de interfață și gamescope pe nucleele de mai jos.</string> + <string name="perf_client_cores">Nuclee pentru client</string> + <string name="perf_client_cores_on">Nuclee pentru Steam.</string> + <string name="perf_client_cores_off">Activează suprascrierea pentru a alege.</string> + <string name="perf_game_cores_group">Nuclee pentru joc</string> + <string name="perf_game_cores">Nuclee pentru joc</string> + <string name="perf_game_cores_hint">Nuclee pentru jocuri. Cu toate selectate, Android le programează liber.</string> + <string name="perf_client_ui">Interfața clientului</string> + <string name="perf_glthread">GL pe fire</string> + <string name="perf_glthread_hint">Poate face meniurile Steam mai receptive.</string> + <string name="perf_zink">Zink: descriptori leneși</string> + <string name="perf_zink_hint">Recomandat pentru drivere fără buffere de descriptori.</string> + <string name="perf_noglerror">Omite verificările de erori GL</string> + <string name="perf_noglerror_hint">Dezactivează validarea GL pentru fiecare apel.</string> + <string name="perf_gsrealtime">gamescope: coadă GPU în timp real</string> + <string name="perf_gsrealtime_hint">Prioritizează lucrul GPU al compositorului. Poate netezi ritmul cadrelor, cu un cost pentru jocuri.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">Fixează GPU-ul la frecvența maximă</string> + <string name="perf_gpuclock_hint">Doar Adreno. Mai puține întreruperi, consum mai mare de baterie și căldură.</string> + <string name="perf_fixes">Remedieri de sesiune</string> + <string name="perf_sysmem">Turnip: randare sysmem</string> + <string name="perf_sysmem_hint">Mai rapidă pentru Steam și majoritatea jocurilor. Necesară pe Adreno 710/720/722.</string> + <string name="perf_xalia">Omite asistentul xalia al Steam</string> + <string name="perf_fastsync">droiddeck-ntsync (experimental)</string> + <string name="perf_fastsync_hint">Un substitut la nivel de utilizator pentru driverul Linux ntsync, pentru testare. Cât timp este activ, Proton îl folosește în locul droiddeck-esync și droiddeck-fsync. Funcționează cu Proton Experimental ARM64, GE-Proton și proton-cachyos. Se aplică la sesiunea următoare.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync primul</string> + <string name="perf_fsync_hint">Rulează propriul fsync al Proton pe fișierele Proton standard, cu futex_waitv furnizat la nivel de utilizator, deci nu are nevoie de pachet și continuă să funcționeze imediat ce Steam actualizează Proton. Cât timp este activ, jocurile îl folosesc înaintea droiddeck-esync, cu excepția celor puține pe care Proton le ține în afara fsync. Se aplică la sesiunea următoare.</string> + <string name="perf_esync_hint">Firele Windows ale jocurilor se așteaptă unele pe altele prin eventfd și memorie partajată, în loc de drumuri dus-întors prin wineserver. Folosește un pachet droiddeck-esync creat pentru exact acea versiune de Proton; pachetele sunt obținute automat. Până când există unul pentru o versiune nouă de Proton, droiddeck-fsync îl înlocuiește. Se aplică la sesiunea următoare.</string> + <string name="perf_xalia_hint">Dezactivează asistentul de navigare cu gamepad al Proton, care costă CPU în fiecare joc.</string> + <string name="perf_seccomp">Rulează proot fără seccomp</string> + <string name="perf_seccomp_hint">Poate remedia erori de apel de sistem lipsă pe unele kernel-uri. Poate reduce performanța.</string> + <string name="perf_fastpath">Cale rapidă proot</string> + <string name="perf_fastpath_needs_seccomp">Necesită filtrul seccomp al proot.</string> + <string name="perf_fastpath_hint">Căutări de fișiere mai rapide și mai puține sacadări. Dezactivează dacă un program nu își găsește fișierele.</string> + <string name="perf_identity">Identitatea sesiunii</string> + <string name="perf_hostname">Nume de host</string> + <string name="perf_hostname_hint">Numele pe care Steam îl vede pentru acest dispozitiv. Gol restaurează %1$s.</string> + <string name="perf_hostname_invalid">Doar litere, cifre și liniuțe interioare, până la 63 de caractere.</string> + <string name="perf_phantom_warning">Android este setat să închidă această sesiune</string> + <string name="driver_installed">Instalat</string> + <string name="driver_available">Disponibil pentru descărcare</string> + <string name="driver_nothing_new">Nimic nou de la ultima verificare.</string> + <string name="driver_restore_builtin">Restaurează driverele integrate</string> + <string name="driver_delete_builtin">Integrat în aplicație: aceasta elimină fișierele sale despachetate și îl ascunde. Restaurează driverele integrate îl readuce.</string> + <string name="driver_delete_bundle">Jumătate dintr-un pachet Android + Linux: ambele jumătăți sunt eliminate. Un driver în uz revine la cel implicit.</string> + <string name="driver_delete_plain">Fișierele sale sunt eliminate din aplicație.</string> + <string name="driver_delete_in_use">Fișierele sale sunt eliminate. Este în uz, așa că cel implicit îi ia locul.</string> + <string name="drawer_page_display">Afișaj</string> + <string name="drawer_page_effects">Efecte</string> + <string name="drawer_page_controls">Comenzi</string> + <string name="drawer_page_components">Componente</string> + <string name="drawer_page_controller">Controller</string> + <string name="drawer_page_settings">Setări</string> + <string name="drawer_page_desc">Pagina %1$s</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Desktop</string> + <string name="drawer_steam_menu">Meniu Steam</string> + <string name="drawer_quick_access">Acces rapid</string> + <string name="drawer_open_qam">Deschide meniul de acces rapid</string> + <string name="drawer_prev_page">Pagina anterioară</string> + <string name="drawer_next_page">Pagina următoare</string> + <string name="drawer_hud">HUD de performanță</string> + <string name="drawer_scaling">Mod de scalare</string> + <string name="drawer_scaling_sharpness">Claritatea scalării</string> + <string name="drawer_effects">Efecte de ecran</string> + <string name="drawer_effects_hint">Live. Costă puțin timp GPU cât timp un efect este activ.</string> + <string name="drawer_look">Presetări</string> + <string name="drawer_look_custom">Personalizat</string> + <string name="drawer_effects_advanced">Avansat</string> + <string name="widgets_expanded">Expandat</string> + <string name="widgets_collapsed">Restrâns</string> + <string name="drawer_cas">Claritate (CAS)</string> + <string name="drawer_cas_level">Nivel de claritate</string> + <string name="drawer_fake_hdr">HDR fals</string> + <string name="drawer_deband">Elimină benzile</string> + <string name="drawer_deband_strength">Intensitatea eliminării benzilor</string> + <string name="drawer_brightness">Luminozitate</string> + <string name="drawer_contrast">Contrast</string> + <string name="drawer_gamma">Gamma</string> + <string name="drawer_saturation">Saturație</string> + <string name="drawer_fxaa">FXAA (netezirea marginilor)</string> + <string name="drawer_toon">Contururi toon</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Filtrare de texturi</string> + <string name="drawer_texture_hint">Jocuri DirectX 9–11, de la următoarea lor lansare.</string> + <string name="drawer_anisotropy">Filtrare anizotropă</string> + <string name="drawer_texture_sharpness">Claritatea texturilor</string> + <string name="drawer_texture_sharpness_note">O deviere mip negativă: texturi mai clare, cu puțin tremur. Automat se potrivește cu cât de mult este mărită sesiunea pentru ecran.</string> + <string name="drawer_touch_auto">Automat (%1$s)</string> + <string name="drawer_keyboard">Tastatură</string> + <string name="drawer_pc_keyboard">Tastatură de PC</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">Al doilea ecran</string> + <string name="drawer_second_screen_mode">Mod</string> + <string name="drawer_second_screen_shows">Arată</string> + <string name="drawer_android_apps">Aplicații Android</string> + <string name="drawer_launch_app">Lansează o aplicație</string> + <string name="common_hide">Ascunde</string> + <string name="common_show">Arată</string> + <string name="drawer_no_apps">Nicio aplicație care poate fi lansată</string> + <string name="drawer_session_behavior">Comportamentul sesiunii</string> + <string name="drawer_suspend_hint">Se aplică acum și sesiunilor viitoare din acest mod.</string> + <string name="drawer_next_session">Sesiunea următoare</string> + <string name="drawer_support">Asistență</string> + <string name="drawer_logs">Jurnalele sesiunii</string> + <string name="drawer_logs_hint">Atașează la un raport de eroare</string> + <string name="drawer_share_logs">Partajează jurnalele</string> + <string name="drawer_background">Fundal</string> + <string name="drawer_stop">Oprește</string> + <string name="drawer_stop_title">Oprești sesiunea?</string> + <string name="drawer_running_now">În execuție acum</string> + <string name="drawer_no_game">Niciun joc nu rulează pe un Proton</string> + <string name="drawer_selected_profile">Profilul jocului</string> + <string name="drawer_selected_profile_none">Detaliile profilului nu sunt încă disponibile.</string> + <string name="drawer_profile_refresh_failed_initial">Profilurile jocurilor nu au putut fi încărcate. Se reîncearcă automat.</string> + <string name="drawer_profile_refresh_failed_stale">Profilurile jocurilor nu au putut fi reîmprospătate. Se arată ultimele date disponibile; se reîncearcă automat.</string> + <string name="drawer_profile_inspect_hint">Vezi și editează setările salvate folosite la următoarea lansare.</string> + <string name="drawer_profile_default">Se folosește profilul implicit</string> + <string name="drawer_profile_custom">Profil personalizat</string> + <string name="drawer_component_inherited">Se folosește %1$s din profilul implicit.</string> + <string name="drawer_game_components_note">Modificările sunt salvate în profilul %1$s și se aplică la următoarea lansare a jocului.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d suprascriere de joc</item> + <item quantity="few">%1$d suprascrieri de joc</item> + <item quantity="other">%1$d de suprascrieri de joc</item> + </plurals> + <string name="drawer_no_proton">Niciun Proton nu este instalat încă.</string> + <string name="drawer_game_running">joc în execuție</string> + <string name="drawer_reapplied">reaplicat la pornirea %1$s</string> + <string name="drawer_original">Original</string> + <string name="drawer_components_note">Importă și șterge pe pagina Componente.</string> + <string name="setup_check_done">Finalizat</string> + <string name="setup_check_warn">Necesită atenție</string> + <string name="setup_check_busy">Se procesează</string> + <string name="setup_limit_adb_failed">Verifică adresa IP și portul Depanării wireless, sau asociază din nou dacă Android a eliminat acest dispozitiv.</string> + <string name="setup_install">Instalează</string> + <string name="setup_manage">Gestionează</string> + <string name="setup_tab_overview">Prezentare generală</string> + <string name="setup_tab_controller">Controller</string> + <string name="setup_tab_session">Sesiune</string> + <string name="setup_tab_launcher">Launcher</string> + <string name="setup_title">Configurare</string> + <string name="setup_all_set">● Totul este pregătit</string> + <string name="setup_n_ready">%1$d din %2$d pregătite</string> + <string name="setup_system_check">Verificarea sistemului</string> + <string name="setup_gpu_supported">Dispozitiv acceptat</string> + <string name="setup_gpu_untested">GPU netestat</string> + <string name="setup_gpu_unsupported">GPU neacceptat</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">Steam are nevoie de un driver Adreno; %1$s poate arăta un ecran negru</string> + <string name="setup_runtime">Mediul de execuție Linux</string> + <string name="setup_runtime_progress">%1$s · %2$d%%</string> + <string name="setup_runtime_missing">Neinstalat · circa 3 GB, instalat la prima apăsare pe Play</string> + <string name="setup_installed">Instalat</string> + <string name="setup_runtime_update">%1$s · actualizare disponibilă</string> + <string name="setup_runtime_current">%1$s · actualizat</string> + <string name="setup_limit">Limita proceselor-copil</string> + <string name="setup_limit_on">Activă · Android poate închide Steam. Dezactivarea durează un minut.</string> + <string name="setup_limit_unset">Nesetată · Android poate închide Steam. Dezactivarea durează un minut.</string> + <string name="setup_limit_unknown">Nu a putut fi verificată · dezactivarea durează un minut.</string> + <string name="setup_fix_it">Remediază</string> + <string name="setup_details">Detalii</string> + <string name="setup_dev_options">Opțiuni pentru dezvoltatori</string> + <string name="setup_use_wireless">Folosește Depanarea wireless</string> + <string name="setup_check_again">Verifică din nou</string> + <string name="setup_limit_turn_on">Activează limita</string> + <string name="setup_account">Cont Steam</string> + <string name="setup_signed_in_offline">Autentificat ca %1$s · mod offline</string> + <string name="setup_signed_in">Autentificat ca %1$s</string> + <string name="setup_sign_in">Apasă Play și autentifică-te în Steam</string> + <string name="setup_tools">Instrumente</string> + <string name="setup_controller">Controller</string> + <string name="setup_controller_unavailable">Setările controllerului nu sunt disponibile.</string> + <string name="setup_session">Sesiune</string> + <string name="setup_logs">Jurnalele sesiunii</string> + <string name="setup_logs_hint">Salvate după fiecare sesiune</string> + <string name="setup_agent_commands">Instrumente de depanare</string> + <string name="setup_latest_logs">Cele mai recente jurnale de sesiune</string> + <string name="setup_saved_logs">Jurnale de sesiune salvate</string> + <string name="setup_saved_logs_hint">Păstrează cele mai recente %1$d</string> + <string name="setup_clear_logs">Șterge jurnalele</string> + <string name="setup_offline">Mod offline</string> + <string name="setup_sign_in_first">Autentifică-te mai întâi în Steam</string> + <string name="setup_launcher">Launcher</string> + <string name="setup_language">Limbă</string> + <string name="setup_language_hint">Steam o folosește și el, de la următoarea pornire.</string> + <string name="setup_language_system">Cea implicită a sistemului (%1$s)</string> + <string name="setup_language_system_short">Cea implicită a sistemului</string> + <string name="setup_app_scale">Scala aplicației</string> + <string name="setup_app_scale_hint">Textul și comenzile DroidDeck</string> + <string name="setup_app_scale_default">%1$d%% (Implicit)</string> + <string name="setup_theme">Temă</string> + <string name="setup_theme_hint">Aspectul launcherului</string> + <string name="setup_home">Folosește ca ecran Acasă</string> + <string name="setup_home_on">DroidDeck poate fi aplicația Acasă</string> + <string name="setup_home_off">DroidDeck nu este niciodată oferit ca aplicație Acasă</string> + <string name="setup_orientation">Orientare</string> + <string name="setup_orientation_auto">Peisaj automat</string> + <string name="setup_orientation_landscape">Peisaj</string> + <string name="setup_orientation_reverse">Peisaj inversat</string> + <string name="setup_fullscreen">Ecran complet</string> + <string name="setup_fullscreen_on">Ascunde barele de stare și de navigare Android</string> + <string name="setup_fullscreen_off">Arată barele de stare și de navigare Android</string> + <string name="setup_default_home">Aplicația Acasă implicită</string> + <string name="setup_choose_home">Alege o aplicație Acasă</string> + <string name="setup_choose">Alege</string> + <string name="setup_linux_apps">Aplicații Linux (beta)</string> + <string name="setup_tool_files">Fișiere</string> + <string name="setup_tool_files_hint">Răsfoiește și gestionează fișiere</string> + <string name="setup_tool_protons">Versiuni Proton</string> + <string name="setup_tool_protons_hint">Instalează versiuni Proton ARM64</string> + <string name="setup_tool_performance">Performanță</string> + <string name="setup_tool_performance_hint">Alocarea nucleelor CPU</string> + <string name="setup_tool_roms">Folderul ROMs</string> + <string name="setup_tool_roms_hint">Unde sunt stocate jocurile emulatorului</string> + <string name="setup_card_components">Componente</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Comenzi</string> + <string name="setup_card_controls_hint">Maparea butoanelor</string> + <string name="store_tab_discover">Descoperă</string> + <string name="store_tab_search">Căutare</string> + <string name="store_tab_installed">Instalate</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Magazin</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak pregătit</string> + <string name="store_needs_runtime">Instalează mai întâi mediul de execuție Linux din Configurare.</string> + <string name="store_setup_title">Configurează Flatpak</string> + <string name="store_setup_text">Adaugă Flatpak și îl conectează la Flathub (circa 20 MB). Prima aplicație descarcă și mediul său de execuție.</string> + <string name="store_setting_up">Se configurează…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Actualizări</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d%%</string> + <string name="store_starting">Se pornește…</string> + <string name="store_categories">Categorii</string> + <string name="store_unreachable">Flathub nu a putut fi accesat. Verifică conexiunea și încearcă din nou.</string> + <string name="store_try_again">Încearcă din nou</string> + <string name="store_search_label">Caută pe Flathub</string> + <string name="store_search_placeholder">Firefox, jocuri, emulatoare…</string> + <string name="store_search">Caută</string> + <string name="store_all">Toate</string> + <string name="store_searching">Se caută…</string> + <string name="store_search_hint">Caută după nume sau alege o categorie. Sunt listate doar aplicații ARM64.</string> + <string name="store_no_results">Nicio aplicație ARM64 de pe Flathub nu se potrivește.</string> + <string name="store_results">Rezultate</string> + <string name="store_check_updates">Verifică actualizările</string> + <string name="store_update_all">Actualizează tot (%1$d)</string> + <string name="store_all_current">● Totul este actualizat</string> + <string name="store_no_apps">Nicio aplicație încă. Găsește câteva la Descoperă sau Căutare.</string> + <string name="store_setup_first_note">Configurează Flatpak pentru a instala aplicații.</string> + <string name="store_update_available_prefix">Actualizare disponibilă · %1$s</string> + <string name="store_open">Deschide</string> + <string name="store_details">Detalii</string> + <string name="store_remove">Elimină</string> + <string name="store_press_again">Apasă din nou pentru a elimina</string> + <string name="store_installing">Se instalează…</string> + <string name="store_install">Instalează</string> + <string name="store_setup_first">Configurează mai întâi Flatpak</string> + <string name="store_download_size">Descărcare de %1$s</string> + <string name="store_installed_chip">● Instalat</string> + <string name="store_details_failed">Pagina Flathub a acestei aplicații nu a putut fi încărcată.</string> + <string name="store_loading">Se încarcă de pe Flathub…</string> + <string name="store_no_arm">Nicio versiune ARM64 pe Flathub, deci nu poate fi instalată aici.</string> + <string name="store_screenshots">Capturi de ecran</string> + <string name="store_about">Despre</string> + <string name="store_app_id">ID aplicație</string> + <string name="store_license">Licență</string> + <string name="store_runtime">Mediu de execuție</string> + <string name="store_installed_size">Dimensiune instalată</string> + <string name="store_installed_size_value">%1$s (plus mediul de execuție partajat)</string> + <string name="store_website">Site web</string> + <string name="store_verified">Verificat</string> + <string name="upd_title">Actualizări</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Verificat %1$s</string> + <string name="upd_not_checked">Neverificat încă</string> + <string name="upd_perm_title">Permite DroidDeck să se actualizeze singur</string> + <string name="upd_perm_text">Pe ecranul următor, activează Permite din această sursă, apoi revino. Actualizarea continuă de la sine.</string> + <string name="upd_open_settings">Deschide setările</string> + <string name="upd_not_now">Nu acum</string> + <string name="upd_signed_status">Semnătură diferită</string> + <string name="upd_signed_headline">Nu se poate actualiza pe loc</string> + <string name="upd_signed_detail">Această copie nu este semnată cu cheia de lansare a DroidDeck, așa că actualizările nu se pot instala peste ea. Dezinstaleaz-o pentru a comuta.</string> + <string name="upd_available">Actualizare disponibilă</string> + <string name="upd_blocked_status">Nu se poate instala</string> + <string name="upd_switch">Pregătit pentru comutare</string> + <string name="upd_ahead_status">Înaintea versiunii Stabile</string> + <string name="upd_ahead_headline">Ești înaintea versiunii Stabile</string> + <string name="upd_ahead_detail">Această versiune este mai nouă decât cea mai recentă Stabilă. Vei trece la Stabilă odată cu următoarea sa lansare.</string> + <string name="upd_gone_status">Testul s-a terminat</string> + <string name="upd_gone_headline">Acest test s-a terminat</string> + <string name="upd_gone_detail">Remedierea sa a fost integrată sau abandonată. Urmărește Preview pentru cele mai noi remedieri.</string> + <string name="upd_current_status">Actualizat</string> + <string name="upd_current_headline">Ai cea mai recentă versiune %1$s</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Actualizează%1$s</string> + <string name="upd_install_button">Instalează%1$s</string> + <string name="upd_install_anyway">Instalează Stabilă %1$s oricum</string> + <string name="upd_follow_preview">Urmărește Preview</string> + <string name="upd_stop_session">Oprește sesiunea în curs pentru a actualiza.</string> + <string name="upd_less">Mai puțin</string> + <string name="upd_more">Mai mult</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Versiune nouă Preview</string> + <string name="upd_new_test">Versiune de test PR #%1$d</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">CANAL DE ACTUALIZARE</string> + <string name="upd_stable">Stabilă</string> + <string name="upd_stable_hint">Versiuni testate, pentru majoritatea oamenilor</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Preview</string> + <string name="upd_preview_hint">Cele mai noi remedieri, înainte de Stabilă</string> + <string name="upd_preview_current">Actualizat</string> + <string name="upd_tests">Versiuni de test</string> + <string name="upd_tests_hint">Încearcă o remediere înainte de lansare</string> + <string name="upd_tests_none">Niciuna acum</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Nu este compilată pentru această copie de DroidDeck</string> + <string name="upd_updated">Actualizat %1$s</string> + <string name="upd_test_name">testul PR #%1$d</string> + <string name="upd_while_ago">cu un timp în urmă</string> + <string name="upd_just_now">chiar acum</string> + <plurals name="upd_tests_count"> + <item quantity="one">%1$d de încercat</item> + <item quantity="few">%1$d de încercat</item> + <item quantity="other">%1$d de încercat</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="one">Modificări de la versiunea ta · %1$d compilare în urmă</item> + <item quantity="few">Modificări de la versiunea ta · %1$d compilări în urmă</item> + <item quantity="other">Modificări de la versiunea ta · %1$d de compilări în urmă</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="one">Modificări recente · %1$d compilare</item> + <item quantity="few">Modificări recente · %1$d compilări</item> + <item quantity="other">Modificări recente · %1$d de compilări</item> + </plurals> + <string name="gate_title">O setare Android de schimbat</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Android oprește aplicațiile care pornesc multe procese în fundal, iar Steam pornește zeci. Până se schimbă asta, jocurile se închid fără o eroare.</string> + <string name="gate_fix_for_me">Repară pentru mine</string> + <string name="gate_toggle_steps">Activează „Dezactivează restricțiile pentru procesele-copil”, apoi revino. Lasă Opțiunile pentru dezvoltatori activate, altfel comutatorul se dezactivează din nou.</string> + <string name="gate_open_dev">Deschide Opțiunile pentru dezvoltatori</string> + <string name="gate_no_toggle">Această versiune de Android nu are un comutator pentru asta, așa că DroidDeck o schimbă prin Depanarea wireless. Durează circa un minut, fără a avea nevoie de un computer.</string> + <string name="gate_needs_wifi">Depanarea wireless are nevoie de Wi-Fi. Orice rețea funcționează; nu are nevoie de internet.</string> + <string name="gate_needs_notifications">Codul de asociere apare într-o notificare. Permite notificările pentru DroidDeck, sau folosește Alte moduri.</string> + <string name="gate_allow_notifications">Permite notificările</string> + <string name="gate_step1">Deschide Setări → Opțiuni pentru dezvoltatori → Depanare wireless și activează-o</string> + <string name="gate_step2">Apasă „Asociază dispozitivul cu un cod de asociere”</string> + <string name="gate_step3">Introdu codul în notificarea DroidDeck</string> + <string name="gate_waiting">Se așteaptă fereastra de asociere…</string> + <string name="gate_code_needed">Fereastra a fost găsită. Introdu codul său în notificare.</string> + <string name="gate_status">Stare</string> + <string name="gate_polling">Se verifică la fiecare 2 secunde. Această pagină se închide când este dezactivată.</string> + <string name="gate_other_ways">Alte moduri</string> + <string name="gate_other_toggle">Depanare wireless, adresă manuală, ADB pe computer</string> + <string name="gate_other_no_toggle">Adresă manuală, ADB pe computer, Opțiuni pentru dezvoltatori</string> + <string name="gate_use_wireless_instead">Folosește Depanarea wireless în schimb</string> + <string name="gate_some_roms">Unele ROM-uri adaugă comutatorul oricum. Caută „procese-copil” în Opțiuni pentru dezvoltatori.</string> + <string name="gate_manual_hint">Dacă notificarea nu funcționează, introdu tu adresa și codul din fereastră.</string> + <string name="gate_enter_manually">Introdu adresa manual</string> + <string name="gate_from_computer">De pe un computer cu ADB:</string> + <string name="gate_copy_command">Copiază comanda</string> + <string name="gate_choose_display">Alege pe care afișaj se deschid Setările Android.</string> + <string name="gate_main_screen">Ecranul principal</string> + <string name="wadb_finding_port">Asociat. Se caută portul de Depanare wireless…</string> + <string name="wadb_enter_port">Asociat. Introdu portul din Setări.</string> + <string name="wadb_applying">Dispozitiv găsit. Se aplică setarea…</string> + <string name="wadb_apply_failed">Nu s-a putut aplica automat. Verifică adresa de mai jos și încearcă din nou. %1$s</string> + <string name="wadb_title_wireless">Depanare wireless</string> + <string name="wadb_title_done">Limita proceselor-copil a fost actualizată</string> + <string name="wadb_title">Schimbă limita proceselor-copil</string> + <string name="wadb_lede_compact0">Dacă opțiunea lipsește, asociază-te prin Depanare wireless.</string> + <string name="wadb_lede_compact1">Asociat. Verifică adresa IP și portul în Depanare wireless.</string> + <string name="wadb_lede_done">Limita proceselor-copil a fost actualizată.</string> + <string name="wadb_lede0">Folosește pasul 2 dacă Opțiunile pentru dezvoltatori nu au o setare pentru procese-copil. Deschide Opțiuni pentru dezvoltatori → Depanare wireless → Asociază dispozitivul cu un cod de asociere.</string> + <string name="wadb_lede1">Asociat. Folosește adresa de mai jos, sau actualizeaz-o din Depanare wireless → Adresă IP și port.</string> + <string name="wadb_lede_done_long">Limita proceselor-copil a fost actualizată prin Depanarea wireless.</string> + <string name="wadb_confirmed_on">Android a confirmat că limita este activă.</string> + <string name="wadb_confirmed_off">Android a confirmat că limita este dezactivată.</string> + <string name="wadb_pair_group">2 · Asociază Depanarea wireless</string> + <string name="wadb_pair_address">Adresa IP și portul ferestrei de asociere</string> + <string name="wadb_pin">Cod de asociere (PIN)</string> + <string name="wadb_pin_placeholder">6 cifre</string> + <string name="wadb_pair">Asociază</string> + <string name="wadb_apply_group">2 · Aplică prin Depanarea wireless</string> + <string name="wadb_connect_address">Adresa IP și portul de conexiune</string> + <string name="wadb_pair_again">Asociază din nou</string> + <string name="wadb_turn_off">Dezactivează limita</string> + <string name="wadb_other_ways">Alte moduri de a o schimba</string> + <string name="wadb_dev_try_first">1 · Opțiuni pentru dezvoltatori (încearcă mai întâi)</string> + <string name="wadb_restrict_on">Activează „Restricționează procesele-copil”.</string> + <string name="wadb_restrict_off">Dezactivează „Restricționează procesele-copil”.</string> + <string name="wadb_computer_last">3 · ADB pe computer (ultima soluție)</string> + <string name="wadb_only_if">Doar dacă Depanarea wireless nu este disponibilă.</string> + <string name="wadb_computer_group">3 · ADB pe computer (ultima soluție)</string> + <string name="ctrl_osc_hint">Când apar comenzile tactile într-o sesiune</string> + <string name="ctrl_color">Culoare</string> + <string name="ctrl_color_hint">Tenta butoanelor de pe ecran</string> + <string name="ctrl_custom">Personalizat</string> + <string name="ctrl_opacity">Opacitate</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">Dimensiunea butoanelor</string> + <string name="ctrl_size_hint" formatted="false">100% este dimensiunea standard</string> + <string name="ctrl_stick_click">Apăsarea manșei</string> + <string name="ctrl_stick_click_hint">Apasă de două ori și ține apăsată o manșă pentru L3 sau R3</string> + <string name="ctrl_adaptive">Manșe adaptive</string> + <string name="ctrl_rumble">Vibrații de joc</string> + <string name="ctrl_steam_button">Butonul Steam</string> + <string name="ctrl_qam_button">Butonul QAM</string> + <string name="ctrl_keyboard_button">Butonul tastaturii</string> + <string name="ctrl_select_steam">Apasă Select pentru a deschide meniul Steam</string> + <string name="ctrl_select_steam_hint">În loc de View/Select.</string> + <string name="setup_animations">Animații</string> + <string name="ctrl_adaptive_hint">Manșele apar unde atingi ecranul și se ascund la eliberare</string> + <string name="ctrl_layout">Aranjament</string> + <string name="ctrl_layout_custom">Poziții personalizate salvate</string> + <string name="ctrl_layout_default">Adaptat pentru acest ecran</string> + <string name="ctrl_edit">Editează</string> + <string name="ctrl_reset">Resetează</string> + <string name="ctrl_mapping">Maparea butoanelor</string> + <string name="ctrl_mapping_default">Fiecare buton trimite propria intrare</string> + <string name="ctrl_mapping_count">%1$d din %2$d butoane remapate</string> + <string name="ctrl_configure">Configurează</string> + <string name="ctrl_reset_all">Resetează controllerul</string> + <string name="ctrl_reset_all_hint">Restaurează toate setările controllerului și aranjamentul</string> + <string name="ctrl_mapping_lede">Ce trimite jocului fiecare buton de pe ecran. Ascuns elimină butonul.</string> + <string name="ctrl_onscreen_buttons">Butoane pe ecran</string> + <string name="ctrl_defaults">Valori implicite</string> + <string name="ctrl_reset_mapping">Resetează maparea</string> + <string name="content_android_apps">Aplicații Android</string> + <string name="content_apps_count">%1$d aplicații</string> + <string name="content_no_android_apps">Nu s-a găsit nicio aplicație Android care poate fi lansată.</string> + <string name="content_desktop_installed">● Desktop instalat</string> + <string name="content_desktop_first_open">Se instalează la prima deschidere</string> + <string name="content_available">Disponibil pentru instalare</string> + <string name="content_not_installed">Neinstalat.</string> + <string name="content_not_installed_suffix">%1$s · neinstalat</string> + <string name="content_open_named">Deschide %1$s</string> + <string name="content_games">Jocuri</string> + <string name="content_choose_roms">Alege un folder ROMs.</string> + <string name="content_retroarch_roms">Navighează la /root/ROMs în RetroArch.</string> + <string name="content_add_roms">Adaugă jocuri %1$s în ROMs/%2$s.</string> + <string name="content_install_named">Instalează %1$s</string> + <string name="content_stop_to_install">Oprește sesiunea pentru a instala</string> + <string name="content_runtime_required">Mediu de execuție necesar</string> + <string name="content_loading_details">Se încarcă detaliile instalării…</string> + <string name="content_details_unavailable">Detaliile instalării nu sunt disponibile. Încearcă din nou mai târziu.</string> + <string name="content_needs_runtime">Instalează mai întâi mediul de execuție Linux din Configurare.</string> + <string name="content_stop_first">Oprește sesiunea în curs pentru a instala aplicații desktop.</string> + <string name="content_install_into">Instalează %1$s în desktopul Linux.</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">Ascunderea o elimină din Desktop. Fișierele sale rămân în mediul de execuție Linux.</string> + <string name="content_rom_gone">Acel joc nu mai este în folderul ROMs.</string> + <string name="content_desktop_system">Desktop · %1$s</string> + <string name="content_launch_in">Lansează în %1$s</string> + <string name="content_also_in">De asemenea în %1$s</string> + <string name="content_page_gone">Acea pagină nu mai există. Alege o secțiune din stânga.</string> + <string name="content_open_desktop">Deschide desktopul</string> + <string name="content_install_open_desktop">Instalează și deschide desktopul</string> + <string name="desktop_remove_title">Dezinstalează desktopul</string> + <string name="desktop_remove_text">Elimină desktopul și aplicațiile sale incluse. Păstrează jocurile, aplicațiile adăugate, setările și bibliotecile partajate.</string> + <string name="desktop_removing">Se elimină desktopul</string> + <string name="desktop_removed">Desktop eliminat</string> + <string name="desktop_cleaning_legacy">Se elimină fișierele LXQt vechi</string> + <string name="desktop_removal_unknown">Actualizează DroidDeck înainte de a elimina această versiune de desktop.</string> + <string name="content_linux_desktop">Desktop Linux</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox și emulatoarele tale</string> + <string name="games_empty">Jocurile pe care le instalezi în Steam apar aici după sesiune.</string> + <string name="games_play_steam">Joacă Steam</string> + <string name="games_launch_settings">Setări de lansare</string> + <string name="games_launch">Lansează</string> + <string name="games_files">Fișierele jocului</string> + <string name="games_prefix">Prefix Proton</string> + <string name="games_saves_looking">Se caută salvările…</string> + <string name="games_saves_none">Niciun folder de salvare găsit după nume. Exportul include tot folderul de utilizator.</string> + <string name="games_saves_folders">%1$d foldere de salvare</string> + <string name="games_saves_summary">%1$s · %2$d fișiere · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">Gestionează salvările</string> + <string name="games_saves">Salvările jocului</string> + <string name="games_import_saves">Importă salvări…</string> + <string name="games_export_gamehub">Exportă ca arhivă GameHub…</string> + <string name="games_export_winlator">Exportă ca arhivă Winlator…</string> + <string name="games_open_save_folder">Deschide folderul de salvare</string> + <string name="games_never_played">Niciodată jucat</string> + <string name="games_runtime_busy_percent">Mediu de execuție ocupat · %1$d%%</string> + <string name="games_runtime_busy">Mediu de execuție ocupat</string> + <string name="games_runtime_first_play">Mediul de execuție se instalează la prima apăsare pe Play</string> + <string name="games_runtime_update">Actualizare disponibilă pentru mediul de execuție</string> + <string name="games_runtime_ready">● Mediu de execuție pregătit</string> + <string name="games_last_played">Jucat ultima dată %1$s</string> + <string name="games_internal">Stocare internă</string> + <string name="games_added">Joc adăugat</string> + <string name="games_custom">Joc personalizat</string> + <string name="games_store_storage">Stocarea jocurilor</string> + <string name="gpu_auto_hint">Cea mai bună pereche pentru acest GPU, păstrată actualizată</string> + <string name="gpu_manual_hint">Alege o pereche tu însuți</string> + <string name="gpu_checking">Se verifică driverele…</string> + <string name="gpu_none_listed">Niciun driver listat încă. Reîmprospătează pentru a le căuta.</string> + <string name="gpu_hide_other">Ascunde driverele pentru alte GPU-uri</string> + <string name="gpu_show_other">Arată driverele pentru alte GPU-uri</string> + <string name="gpu_advanced">Avansat</string> + <string name="gpu_set_with_display">Setat împreună cu driverul de afișaj: %1$s include ambele.</string> + <string name="gpu_set_with_runtime">Setat împreună cu driverul mediului de execuție: %1$s include ambele.</string> + <string name="gpu_runtime_hint">Folosit de jocuri și de desktop.</string> + <string name="gpu_runtime_default">Implicit pentru mediul de execuție</string> + <string name="gpu_display_hint">Desenează cadrele pe ecran. Se aplică după repornirea DroidDeck.</string> + <string name="gpu_auto_by_gpu">Automat - ales în funcție de GPU</string> + <string name="gpu_manage">Descarcă, importă sau șterge…</string> + <string name="gpu_in_use_caps">ÎN UZ</string> + <string name="gpu_builtin">Driverele integrate în DroidDeck</string> + <string name="gpu_matched_pair">Driverele mediului de execuție și de afișaj setate împreună ca pereche asortată.</string> + <string name="gpu_recommended_for">Recomandat pentru acest GPU: %1$s.</string> + <string name="gpu_refresh_hint">Reîmprospătează pentru a căuta drivere pentru acest GPU.</string> + <string name="gpu_recommended_caps">RECOMANDAT</string> + <string name="gpu_unavailable">Indisponibil</string> + <string name="load_runtime">Mediul de execuție Linux</string> + <string name="load_runtime_detail">Se pregătește mediul de execuție Linux</string> + <string name="load_session">Sesiune</string> + <string name="load_session_detail">Se pornesc afișajul și sunetul</string> + <string name="load_steam">Clientul Steam</string> + <string name="load_steam_detail">Se verifică Steam și instrumentele sale de compatibilitate</string> + <string name="load_opening_steam">Se deschide Steam</string> + <string name="load_opening_steam_detail">Se așteaptă primul cadru al Steam</string> + <string name="load_desktop">Desktop</string> + <string name="load_desktop_detail">Se pregătește desktopul</string> + <string name="load_opening">Se deschide</string> + <string name="load_opening_detail">Se așteaptă primul cadru</string> + <string name="load_starting_steam">Se pornește Steam</string> + <string name="load_ended">Sesiunea s-a terminat</string> + <string name="load_step">Pasul %1$d din %2$d</string> + <string name="load_steam_paused">Steam este pus pe pauză</string> + <string name="deck_tab_grips">Mânere</string> + <string name="deck_tab_left">Padul stâng</string> + <string name="deck_tab_right">Padul drept</string> + <string name="deck_tab_both">Ambele</string> + <string name="deck_tab_gamepad">Gamepad</string> + <string name="deck_surface_stick">Manșă</string> + <string name="deck_surface_trackpad">Trackpad</string> + <string name="deck_surface_switch">Comută între manșă și trackpad</string> + <string name="deck_more_controls">Mai multe comenzi Deck</string> + <string name="deck_panels">PANOURI</string> + <string name="deck_close">Închide comenzile celui de-al doilea ecran</string> + <string name="deck_open_steam_menu">Deschide meniul Steam</string> + <string name="deck_left_upper">Stânga · sus</string> + <string name="deck_left_lower">Stânga · jos</string> + <string name="deck_right_upper">Dreapta · sus</string> + <string name="deck_right_lower">Dreapta · jos</string> + <string name="deck_click">Clic</string> + <string name="deck_right_trackpad">Trackpad-ul drept</string> + <string name="deck_left_trackpad">Trackpad-ul stâng</string> + <string name="second_trackpad">Trackpad</string> + <string name="second_show_keyboard">Arată tastatura</string> + <string name="second_hide_keyboard">Ascunde tastatura</string> + <string name="second_terminal">Terminal Linux</string> + <string name="second_terminal_not_ready">Sesiunea Linux nu este încă pregătită pentru un terminal.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Jocuri</string> + <string name="rail_desktop">Desktop</string> + <string name="rail_store">Magazin</string> + <string name="rail_components">Componente</string> + <string name="rail_apps">Aplicații</string> + <string name="rail_setup">Configurare</string> + <string name="rail_updates">Actualizări</string> + <string name="rail_percent">%1$d%%</string> + <string name="rail_resume_named">Reluare %1$s</string> + <string name="roms_text">Apare ca /root/ROMs în fiecare sesiune. Stocarea internă este la /root/Storage.</string> + <string name="roms_none">Niciun folder ales</string> + <string name="roms_applies">Se aplică la sesiunea următoare. Cardurile SD funcționează.</string> + <string name="roms_choose">Alege folderul</string> + <string name="roms_change">Schimbă folderul</string> + <string name="proton_lede">Instalează o versiune Proton ARM64, apoi alege-o per joc în Steam → Proprietăți → Compatibilitate.</string> + <string name="proton_stop_first">Oprește sesiunea în curs pentru a instala sau elimina un instrument de compatibilitate.</string> + <string name="proton_available">Versiuni disponibile</string> + <string name="proton_installed">Instalat %1$s</string> + <string name="proton_queued">În așteptare de la o cerere anterioară</string> + <string name="proton_not_installed">Neinstalat</string> + <string name="proton_update_available">Actualizare disponibilă → %1$s</string> + <string name="proton_remove_older">Elimină versiunile vechi de %1$s</string> + <string name="proton_remove_older_body">Elimină %1$s. Jocurile setate pe aceste versiuni trebuie comutate pe alt Proton.</string> + <string name="proton_remove_body">Jocurile setate pe această versiune trebuie comutate pe alt Proton.</string> + <string name="proton_install_now">Instalează acum</string> + <string name="proton_cancel_queued">Anulează cererea în așteptare</string> + <string name="pckb_android">Tastatura Android</string> + <string name="pckb_hide">Ascunde ✕</string> + <string name="main_launch_named">Lansează %1$s</string> + <string name="main_choose_screen">Alege un ecran. Secundar: %1$s.</string> + <string name="main_secondary_gone">Ecranul secundar nu mai este disponibil.</string> + <string name="main_primary">Ecranul principal</string> + <string name="main_secondary">Ecranul secundar</string> + <string name="widgets_prev_tab">Fila anterioară</string> + <string name="widgets_next_tab">Fila următoare</string> + <string name="widgets_on">Activat</string> + <string name="widgets_all_n">Toate %1$d</string> + <string name="widgets_none">Niciuna</string> + <string name="widgets_n_of_m">%1$d din %2$d</string> + <string name="widgets_off">Dezactivat</string> + <string name="screen_bottom">Ecranul de jos</string> + <string name="game_env_applies">Se aplică la următoarea lansare a jocului. Profilul unui joc are prioritate față de cel comun.</string> + <string name="game_env_other">Alt joc după ID-ul aplicației…</string> + <string name="game_env_done">Terminat</string> + <string name="game_env_default">Implicit</string> + <string name="game_env_off">Dezactivat</string> + <string name="game_env_change">Schimbă…</string> + <string name="game_env_change_title">Schimbă variabila</string> + <string name="game_env_restore_default">Înapoi la implicit</string> + <string name="game_env_eyebrow">Jocuri Proton</string> + <string name="pip_title">Imagine în imagine</string> + <string name="pip_auto">Intră automat în PiP</string> + <string name="unfolded_controls">Comenzi pe ecranul desfăcut</string> + <string name="unfolded_controls_hint">Desfăcut, jocul se află peste trackpadurile și mânerele din spate ale Steam Deck. Împăturit, jocul este pe tot ecranul.</string> + <string name="pip_suspended">Suspendat</string> + <string name="pip_not_ready">Imaginea în imagine este disponibilă odată ce sesiunea este pregătită.</string> + <string name="pip_permission">Permite imaginea în imagine pentru DroidDeck în setările Android.</string> + <string name="pip_unavailable">Android nu a putut deschide imaginea în imagine.</string> + <string name="game_shortcut">Comandă rapidă</string> + <string name="game_shortcut_add">Adaugă pe ecranul principal</string> + <string name="game_shortcut_choose">Alege jocul</string> + <string name="game_shortcut_scanning">Se scanează jocurile instalate…</string> + <string name="game_link_copy">Copiază linkul de lansare</string> + <string name="game_link_copied">Link de lansare copiat</string> + <string name="game_shortcut_unsupported">Acest launcher nu poate adăuga comenzi rapide. Folosește Copiază linkul de lansare în schimb.</string> + <string name="game_link_invalid">Link de lansare a jocului invalid.</string> + <string name="game_link_missing">Acest joc nu mai este în biblioteca ta DroidDeck.</string> + <string name="game_link_session_busy">Oprește sesiunea curentă înainte de a lansa un joc Steam.</string> + <string name="game_link_failed">Jocul nu a putut fi trimis către Steam. Încearcă să îl lansezi din nou.</string> + <string name="setup_retry_removal">Reîncearcă eliminarea</string> + <string name="runtime_removing">Se elimină</string> + <string name="runtime_removal_incomplete">Eliminare incompletă</string> + <string name="game_file_export">Exportă fișierul jocului</string> + <string name="game_file_sync">Sincronizează toate fișierele jocurilor în folder</string> + <string name="game_file_stop_sync">Oprește sincronizarea fișierelor jocurilor</string> + <string name="game_file_folder">Alege un folder pentru fișierele frontend ale jocurilor</string> + <string name="game_file_exported">Fișierele jocului salvate în %1$s</string> + <string name="game_frontend_files">Fișiere frontend</string> + + <string name="main_wait_runtime">Așteaptă să se termine operația asupra mediului de execuție</string> + <string name="main_wait_proton_install">Așteaptă să se termine instalarea instrumentului de compatibilitate</string> + <string name="main_wait_desktop_install">Așteaptă să se termine instalarea aplicației desktop</string> + <string name="main_wait_install_game">Așteaptă să se termine instalarea înainte de a lansa un joc</string> + <string name="main_saves_close_session">Închide mai întâi sesiunea Steam, ca jocul să nu poată salva peste import</string> + <string name="main_saves_importing">Se importă în %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="one">S-a importat %1$d fișier din %2$s în %3$s</item> + <item quantity="few">S-au importat %1$d fișiere din %2$s în %3$s</item> + <item quantity="other">S-au importat %1$d de fișiere din %2$s în %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">S-a importat %1$d fișier din %2$s în %3$s. Salvările vechi au fost copiate de rezervă în Download/DroidDeck/Saves/backups</item> + <item quantity="few">S-au importat %1$d fișiere din %2$s în %3$s. Salvările vechi au fost copiate de rezervă în Download/DroidDeck/Saves/backups</item> + <item quantity="other">S-au importat %1$d de fișiere din %2$s în %3$s. Salvările vechi au fost copiate de rezervă în Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Alege o arhivă de salvare pentru %1$s</string> + <string name="main_saves_exporting">Se exportă %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="one">S-a exportat %1$d fișier ca %2$s: %3$s</item> + <item quantity="few">S-au exportat %1$d fișiere ca %2$s: %3$s</item> + <item quantity="other">S-au exportat %1$d de fișiere ca %2$s: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Alege unde se salvează %1$s (%2$s)</string> + <string name="main_action_failed">%1$s a eșuat: %2$s</string> + <string name="main_no_logs_yet">Niciun jurnal de sesiune încă: rulează mai întâi o sesiune.</string> + <string name="main_no_logs_to_clear">Niciun jurnal de sesiune de șters.</string> + <plurals name="main_logs_cleared"> + <item quantity="one">S-a șters %1$d folder de jurnal de sesiune.</item> + <item quantity="few">S-au șters %1$d foldere de jurnal de sesiune.</item> + <item quantity="other">S-au șters %1$d de foldere de jurnal de sesiune.</item> + </plurals> + <string name="main_wadb_pair_failed">Asocierea Depanării wireless a eșuat</string> + <string name="main_wadb_command_failed">Comanda Depanării wireless a eșuat</string> + <string name="main_wadb_unavailable">Depanarea wireless nu este disponibilă</string> + <string name="main_adb_copied">Comanda ADB copiată</string> + <string name="main_pick_roms">Alege folderul ROMs</string> + <string name="main_non_adreno_title">Nu este un GPU Adreno</string> + <string name="main_non_adreno_text">Turnip acceptă GPU-uri Adreno. Pe %1$s, Steam poate arăta un ecran negru. Descărcare: %2$.0f MB.</string> + <string name="main_install_anyway">Instalează oricum</string> + <string name="main_remove_runtime_title">Elimină mediul de execuție Linux</string> + <string name="main_remove_runtime_text">Aceasta șterge mediul de execuție, clientul Steam din interiorul lui și fiecare joc instalat acolo.</string> + <string name="main_applied">Aplicat: %1$s</string> + <string name="android_app_open_failed_primary">%1$s nu a putut fi deschis pe ecranul principal</string> + <string name="android_app_open_failed_on">%1$s nu a putut fi deschis pe %2$s</string> + <string name="android_app_open_failed_display">%1$s nu a putut fi deschis pe afișajul %2$d</string> + <string name="comp_busy_importing">Se importă</string> + <string name="comp_busy_swapping">Se comută</string> + <string name="comp_busy_restoring">Se restaurează</string> + <string name="comp_busy_cancelling">Se anulează</string> + <string name="comp_busy_deleting">Se șterge</string> + <string name="comp_cannot_read">fișierul nu poate fi citit</string> + <string name="comp_imported">Importat %1$s</string> + <string name="comp_swap_cancelled">Comutarea în așteptare a fost anulată.</string> + <string name="comp_pick_wcp">Alege un pachet de componente (-linux .wcp)</string> + <string name="comp_nightlies_unreachable">Versiunile nightly nu au putut fi accesate</string> + <string name="comp_stored">Salvat %1$s</string> + <string name="comp_download_failed">Descărcarea a eșuat: %1$s</string> + <string name="drivers_pick_linux">Alege un driver pentru mediul de execuție Linux (arhivă -Linux sau pachet Android + Linux)</string> + <string name="drivers_pick_android">Alege un driver de afișaj (arhivă AdrenoTools sau pachet Android + Linux)</string> + <string name="drivers_pick_any">Alege o arhivă de driver sau un pachet Android + Linux</string> + <string name="main_pkg_problem">%1$s: %2$s</string> + <string name="main_removing_named">Se elimină %1$s…</string> + <string name="main_forgetting_named">Se elimină din memorie %1$s…</string> + <string name="main_pick_game_storage">Alege folderul de stocare a jocurilor</string> + <string name="main_pick_decky_zip">Alege o arhivă ZIP cu un plugin Decky</string> + <string name="main_storage_not_usable">Nu poate fi folosit: %1$s</string> + <string name="main_running_program">Program</string> + <string name="main_removing_runtime">Se elimină mediul de execuție Linux</string> + <string name="main_settings_main_screen">Setările nu au putut fi deschise pe ecranul de jos; au fost deschise pe ecranul principal.</string> + <string name="main_open_dev_options">Deschide Opțiunile pentru dezvoltatori din Setări</string> + + <string name="upd_unknown_apps_failed">Setarea Android „Instalează aplicații necunoscute” nu a putut fi deschisă</string> + <string name="upd_couldnt_check">Nu s-a putut verifica: %1$s</string> + <string name="upd_no_download">Această versiune nu are o descărcare pentru această copie de DroidDeck</string> + <string name="upd_stop_first">Oprește mai întâi sesiunea în curs, apoi actualizează</string> + <string name="upd_stage_downloading">Se descarcă</string> + <string name="upd_stage_installing">Se instalează</string> + <string name="upd_stage_confirm">Se așteaptă confirmarea ta</string> + <string name="upd_failed">Actualizarea a eșuat</string> + <string name="upd_not_offered">Android nu a oferit instalarea actualizării</string> + + <string name="decky_install_failed">Instalarea Decky a eșuat: %1$s</string> + <string name="decky_plugin_starting">Se pornește importul pluginului…</string> + <string name="decky_plugin_failed">Importul pluginului a eșuat: %1$s</string> + <string name="decky_plugin_installed">Plugin instalat. Repornește Steam pentru a-l încărca.</string> + + <string name="drivers_not_checked">Neverificat încă - apasă reîmprospătare pentru a căuta drivere noi</string> + <string name="drivers_bundle_sets_display">Android + Linux: setează driverul de afișaj odată cu el</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: setează driverul mediului de execuție odată cu el</string> + <string name="drivers_runtime_builtin">Turnip-ul integrat în mediul de execuție</string> + <string name="drivers_system_vulkan">Vulkan de sistem: nicio versiune inclusă pentru acest GPU</string> + <string name="drivers_bundled">%1$s (inclus)</string> + <string name="drivers_manual">Manual</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">Doar o jumătate este publicată acum</string> + <string name="drivers_pair_size">%1$.0f MB pentru ambele</string> + <string name="drivers_none_to_set">Niciun driver de setat: %1$s</string> + <string name="drivers_checking_latest">Se verifică cele mai noi drivere…</string> + <string name="drivers_up_to_date_check_failed">Actualizat (nu s-a putut verifica: %1$s)</string> + <string name="drivers_recommended_unpublished">Driverele recomandate nu sunt publicate acum</string> + <string name="drivers_downloading_pair">Se descarcă %1$s %2$s…</string> + <string name="drivers_updated_to">Actualizat la %1$s %2$s.</string> + <string name="drivers_updated_to_restart">Actualizat la %1$s %2$s. Driverul de afișaj se aplică după repornirea DroidDeck.</string> + <string name="drivers_switched_to">Comutat la %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Comutat la %1$s %2$s. Driverul de afișaj se aplică după repornirea DroidDeck.</string> + <string name="drivers_using">Se folosește %1$s %2$s.</string> + <string name="drivers_using_restart">Se folosește %1$s %2$s. Driverul de afișaj se aplică după repornirea DroidDeck.</string> + <string name="drivers_not_a_driver">Nu este o arhivă de driver: nici driver AdrenoTools, nici Turnip -Linux, nici pachet Android + Linux</string> + <string name="drivers_import_failed">Importul a eșuat: %1$s</string> + <string name="drivers_imported_unnamed">Driver importat</string> + <string name="drivers_using_bundle">Se folosește %1$s ca driver pentru mediul de execuție și pentru afișaj, deopotrivă.</string> + <string name="drivers_using_bundle_restart">Se folosește %1$s ca driver pentru mediul de execuție și pentru afișaj, deopotrivă. Driverul de afișaj se aplică după repornirea DroidDeck.</string> + <string name="drivers_deleted_bundle">Șters %1$s (ambele drivere)</string> + <string name="drivers_deleted">Șters %1$s</string> + <string name="drivers_release_status">Cele mai recente: %1$s · verificat %2$s</string> + <string name="drivers_release_status_failed">Cele mai recente: %1$s · %2$s inaccesibil · verificat %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">acum %1$d min</item> + <item quantity="few">acum %1$d min</item> + <item quantity="other">acum %1$d min</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">acum %1$d h</item> + <item quantity="few">acum %1$d h</item> + <item quantity="other">acum %1$d h</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">acum %1$d zi</item> + <item quantity="few">acum %1$d zile</item> + <item quantity="other">acum %1$d de zile</item> + </plurals> + <string name="drivers_checking_sources">Se verifică Banners-Turnip, WinNative și DroidDeck…</string> + <string name="drivers_check_failed">verificare eșuată</string> + <string name="drivers_installed_pick_either">Instalat %1$s - alege-l în orice listă pentru a seta ambele drivere</string> + <string name="drivers_installed_pick">Instalat %1$s - alege-l în meniu</string> + + <string name="session_wait_removal">Așteaptă să se termine eliminarea mediului de execuție</string> + <string name="session_title_game">Joc</string> + <string name="session_touch_auto_touchpad">touchpad</string> + <string name="session_touch_auto_direct">direct</string> + <string name="session_swap_failed">Comutarea a eșuat: %1$s</string> + <string name="session_profile_save_failed">Salvarea profilului a eșuat: %1$s</string> + <string name="session_desktop_paused">Desktopul este pus pe pauză</string> + <string name="session_named_paused">%1$s este pus pe pauză</string> + <string name="session_program_paused">Programul este pus pe pauză</string> + <string name="session_starting_desktop">Se pornește desktopul</string> + <string name="session_starting_named">Se pornește %1$s</string> + <string name="session_starting_program">Se pornește programul</string> + <string name="session_no_logs">Niciun jurnal pentru această sesiune.</string> + <string name="session_logs_archive_failed">Arhiva jurnalelor sesiunii nu a putut fi creată.</string> + <string name="session_logs_share_failed">Jurnalele sesiunii nu au putut fi partajate.</string> + <string name="session_starting_session">Se pornește sesiunea…</string> + <string name="session_runtime_downloading">se descarcă mediul de execuție Linux</string> + <string name="session_runtime_downloading_mb">se descarcă mediul de execuție Linux · %1$d din %2$d MB</string> + <string name="session_runtime_checking">se verifică mediul de execuție Linux</string> + <string name="session_runtime_unpacking">se despachetează mediul de execuție Linux</string> + <string name="session_desktop_downloading">se descarcă desktopul</string> + <string name="session_desktop_downloading_mb">se descarcă desktopul · %1$d din %2$d MB</string> + <string name="session_desktop_checking">se verifică desktopul</string> + <string name="session_desktop_unpacking">se despachetează desktopul</string> + <string name="session_proton_downloading">se descarcă Proton Experimental (ARM64)</string> + <string name="session_proton_downloading_mb">se descarcă Proton Experimental (ARM64) · %1$d din %2$d MB</string> + <string name="session_proton_checking">se verifică Proton Experimental (ARM64)</string> + <string name="session_proton_unpacking">se despachetează Proton Experimental (ARM64)</string> + <string name="session_runtime_catalog_unreachable">Catalogul mediului de execuție nu a putut fi accesat. Verifică conexiunea și apasă din nou Play.</string> + <string name="session_runtime_install_failed">Mediul de execuție Linux nu s-a instalat. Verifică mediul de execuție în Configurare și apasă din nou Play.</string> + <string name="session_desktop_install_failed">Desktopul nu s-a instalat (%1$s). Verifică conexiunea și apasă din nou Desktop.</string> + <string name="session_ended_named">%1$s s-a oprit neașteptat. Partajează jurnalele cu un raport de eroare, sau încearcă din nou.</string> + <string name="session_ended_program">Programul s-a oprit neașteptat. Partajează jurnalele cu un raport de eroare, sau încearcă din nou.</string> + <string name="session_ended_desktop">Desktopul s-a oprit neașteptat. Partajează jurnalele cu un raport de eroare, sau încearcă din nou.</string> + <string name="session_ended_detail">Stare de ieșire %1$d · %2$s</string> + <string name="session_ended_no_log">niciun jurnal</string> + <plurals name="session_hint_enosys"> + <item quantity="one">Jurnalul arată %1$d eroare „Function not implemented”: accelerarea seccomp a proot nu reușește să ruleze un auxiliar pe acest dispozitiv. Încearcă %2$s → „%3$s” (sau „%4$s”) și pornește din nou.</item> + <item quantity="few">Jurnalul arată %1$d erori „Function not implemented”: accelerarea seccomp a proot nu reușește să ruleze un auxiliar pe acest dispozitiv. Încearcă %2$s → „%3$s” (sau „%4$s”) și pornește din nou.</item> + <item quantity="other">Jurnalul arată %1$d de erori „Function not implemented”: accelerarea seccomp a proot nu reușește să ruleze un auxiliar pe acest dispozitiv. Încearcă %2$s → „%3$s” (sau „%4$s”) și pornește din nou.</item> + </plurals> + + <string name="mode_decky_plugins">Pluginuri</string> + <string name="mode_decky_plugins_hint">Instalează o arhivă ZIP cu un plugin pentru sesiunea Steam următoare.</string> + <string name="mode_decky_plugin_downloading">Se descarcă…</string> + <string name="mode_decky_plugin_install_zip">Instalează din ZIP</string> + <string name="store_cat_games">Jocuri</string> + <string name="store_cat_audio_video">Audio & video</string> + <string name="store_cat_graphics">Grafică</string> + <string name="store_cat_internet">Internet</string> + <string name="store_cat_office">Birou</string> + <string name="store_cat_developer">Dezvoltare</string> + <string name="store_cat_education">Educație</string> + <string name="store_cat_science">Știință</string> + <string name="store_cat_system">Sistem</string> + <string name="store_cat_utilities">Utilitare</string> + <string name="store_section_popular">Populare</string> + <string name="store_section_trending">În tendințe</string> + <string name="store_section_new">Noi</string> + <string name="store_section_updated">Actualizate</string> + <string name="store_section_loading">se încarcă…</string> + <string name="store_flatpak_setup">Configurare Flatpak</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Nu s-au putut verifica actualizările pe Flathub</string> + <string name="content_rom_installed">instalat</string> + <string name="content_rom_folder">folder</string> + <string name="theme_graphite">Grafit</string> + <string name="theme_graphite_detail">Suprafețe grafit, cu albastrul propriu al orbului din logo pentru tot ce este selectat, focalizat sau apăsat.</string> + <string name="theme_paper">Hârtie pe negru</string> + <string name="theme_paper_detail">Albul este principal, albastrul iconiței este semnalul pentru linii, cercuri și puncte.</string> + <string name="theme_icon_blue">Albastru iconiță</string> + <string name="theme_icon_blue_detail">Fundal negru, text alb, albastrul pentru tot ce este selectat, focalizat sau apăsat.</string> + <string name="theme_electric">Bleumarin electric</string> + <string name="theme_electric_detail">Albastrul mai strălucitor, pe suprafețe care tind spre bleumarin în loc de gri.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Space</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <string name="game_sync_err_runtime">Instalează mai întâi mediul de execuție</string> + <string name="game_sync_err_steam_library">Biblioteca Steam nu este disponibilă</string> + <string name="game_sync_err_previous_sd">Biblioteca SD anterioară nu este disponibilă</string> + <string name="game_sync_err_game_storage">Stocarea jocurilor nu este disponibilă</string> + <string name="game_sync_err_added_games">Folderul de jocuri adăugate nu este disponibil</string> + <string name="game_sync_err_manifests">Manifestele Steam nu sunt disponibile</string> + <string name="content_system_many">Multe sisteme</string> + <string name="fm_drive_internal">Internă</string> + <string name="fm_drive_linux">Sistem Linux</string> + <string name="fm_drive_sd">Card SD</string> + <string name="fm_drive_storage">Stocare</string> + <string name="fm_select">Selectează</string> + <string name="fm_properties">Proprietăți</string> + <string name="fm_favorite_add">Adaugă la Favorite</string> + <string name="fm_favorite_remove">Elimină din Favorite</string> + <string name="fm_rename">Redenumește</string> + <string name="fm_copy">Copiază</string> + <string name="fm_cut">Taie</string> + <string name="fm_actions">Acțiuni</string> + <string name="fm_favorites">Favorite</string> + <string name="fm_pin_current">Fixează folderul curent</string> + <string name="fm_no_favorites">Niciun favorit încă - fixează un folder din meniul său ⋮ pentru a reveni rapid aici.</string> + <string name="fm_unpin_favorite">Elimină din favorite</string> + <string name="fm_size_zero">0 bytes</string> + <string name="fm_unit_bytes">bytes</string> + <string name="fm_prop_location">Locație</string> + <string name="fm_prop_type">Tip</string> + <string name="fm_prop_size">Dimensiune</string> + <string name="fm_prop_modified">Modificat</string> + <string name="fm_type_folder">Folder</string> + <string name="fm_type_file">Fișier</string> + <string name="fm_type_ext_file">Fișier %1$s</string> + <string name="fm_read_only">Doar pentru citire</string> + <string name="fm_read_only_hint">Oprește jocurile (și Wine) de la a suprascrie sau șterge acest fișier.</string> + <string name="fm_read_only_failed">Nu s-a putut schimba Doar pentru citire</string> + <string name="fm_hidden">Ascuns</string> + <string name="fm_hidden_unsupported">Această stocare nu poate salva indicatorul ascuns.</string> + <string name="fm_hidden_hint">Marchează fișierul ca ascuns în Windows (atributul hidden al Wine).</string> + <string name="fm_hidden_failed">Nu s-a putut schimba Ascuns pe această stocare</string> + <string name="fm_done">Terminat</string> + <string name="fm_op_deleting">Se șterge...</string> + <string name="fm_delete_failed">Ștergerea a eșuat</string> + <string name="fm_op_moving">Se mută...</string> + <string name="fm_op_copying">Se copiază...</string> + <string name="fm_op_moving_item">Se mută - %1$s</string> + <string name="fm_op_copying_item">Se copiază - %1$s</string> + <string name="fm_op_moving_batch">Se mută %1$d/%2$d - %3$s</string> + <string name="fm_op_copying_batch">Se copiază %1$d/%2$d - %3$s</string> + <string name="fm_paste_failed">%1$d finalizate, %2$d eșuate</string> + <string name="fm_paste_skipped">%1$d finalizate, %2$d omise</string> + <plurals name="fm_paste_moved"> + <item quantity="one">%1$d element mutat</item> + <item quantity="few">%1$d elemente mutate</item> + <item quantity="other">%1$d de elemente mutate</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">%1$d element copiat</item> + <item quantity="few">%1$d elemente copiate</item> + <item quantity="other">%1$d de elemente copiate</item> + </plurals> + <string name="fm_already_exists">„%1$s” deja există</string> + <string name="fm_op_renaming">Se redenumește...</string> + <string name="fm_rename_failed">Redenumirea a eșuat</string> + <string name="fm_op_creating_folder">Se creează folderul...</string> + <string name="fm_create_folder_failed">Folderul nu a putut fi creat</string> + <string name="fm_new_folder">Folder nou</string> + <string name="fm_folder_name">Nume folder</string> + <string name="fm_create">Creează</string> + <string name="fm_new_name">Nume nou</string> + <string name="fm_delete_title">Ștergi?</string> + <string name="fm_delete_text">Ștergi definitiv „%1$s”?</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">Ștergi %1$d element?</item> + <item quantity="few">Ștergi %1$d elemente?</item> + <item quantity="other">Ștergi %1$d de elemente?</item> + </plurals> + <string name="fm_cannot_undo">Această acțiune nu poate fi anulată.</string> + <plurals name="fm_and_more"> + <item quantity="one">…și încă %1$d</item> + <item quantity="few">…și încă %1$d</item> + <item quantity="other">…și încă %1$d</item> + </plurals> + <string name="fm_op_deleting_batch">Se șterge %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">%1$d nu a putut fi șters</item> + <item quantity="few">%1$d nu au putut fi șterse</item> + <item quantity="other">%1$d nu au putut fi șterse</item> + </plurals> + <string name="fm_conflict_merge_hint">Îmbinare adaugă și înlocuiește fișiere în folderul existent.</string> + <string name="fm_conflict_overwrite_hint">Suprascrie înlocuiește fișierul existent.</string> + <string name="fm_conflict_apply_all">Aplică la toate conflictele</string> + <string name="fm_conflict_merge">Îmbinare</string> + <string name="fm_conflict_overwrite">Suprascrie</string> + <string name="fm_conflict_keep_both">Păstrează ambele</string> + <string name="fm_conflict_skip">Omite</string> + <string name="fm_select_this_folder">Selectează acest folder</string> + <string name="fm_back">Înapoi</string> + <string name="fm_drive_not_readable">%1$s este montat, dar nu poate fi citit acum</string> + <string name="fm_list_view">Vizualizare listă</string> + <string name="fm_grid_view">Vizualizare grilă</string> + <string name="fm_search">Căutare</string> + <string name="fm_sort">Sortează</string> + <string name="fm_sort_name">Nume</string> + <string name="fm_sort_date">Data modificării</string> + <string name="fm_rows_comfortable">Rânduri confortabile</string> + <string name="fm_rows_compact">Rânduri compacte</string> + <string name="fm_hide_hidden">Ascunde fișierele ascunse</string> + <string name="fm_show_hidden">Arată fișierele ascunse</string> + <string name="fm_hide_favorites">Ascunde favoritele</string> + <string name="fm_show_favorites">Arată favoritele</string> + <string name="fm_filter_folder">Filtrează acest folder</string> + <string name="fm_free_space">%1$s liber</string> + <string name="fm_selected_count">%1$d selectate</string> + <string name="fm_select_none">Niciunul</string> + <string name="fm_select_all">Toate</string> + <string name="fm_paste">Lipește</string> + <plurals name="fm_items"> + <item quantity="one">%1$d element</item> + <item quantity="few">%1$d elemente</item> + <item quantity="other">%1$d de elemente</item> + </plurals> + <string name="fm_paste_here">Lipește %1$s aici</string> + <string name="fm_paste_move_here">Lipește %1$s (mutare) aici</string> + <string name="fm_rail_title">Fișiere</string> + <string name="fm_rail_storage">STOCARE</string> + <string name="fm_rail_quick">RAPID</string> + <string name="fm_rail_favorites">FAVORITE</string> + <string name="fm_rail_downloads">Descărcări</string> + <string name="fm_rail_downloads_short">Descărc</string> + <string name="fm_rail_roms">ROMs</string> + <string name="fm_rail_session_logs">Jurnale de sesiune</string> + <string name="fm_rail_pictures">Imagini</string> + <string name="fm_favorite_added">„%1$s” a fost adăugat la Favorite</string> + <string name="fm_favorite_removed">„%1$s” a fost eliminat din Favorite</string> + <string name="fm_empty_directory">Director gol</string> + <string name="fm_up_one_level">Un nivel mai sus</string> + <string name="fm_choose_folder">Alege un folder</string> + <string name="fm_choose_file">Alege un fișier</string> + <string name="ctrl_tint_steam_blue">Albastru Steam</string> + <string name="ctrl_tint_sky">Cer</string> + <string name="ctrl_tint_white">Alb</string> + <string name="ctrl_tint_violet">Violet</string> + <string name="ctrl_tint_green">Verde</string> + <string name="ctrl_tint_amber">Amber</string> + <string name="ctrl_tint_red">Roșu</string> + <string name="ctrl_tint_pink">Roz</string> + <string name="ctrl_button_a">Butonul A</string> + <string name="ctrl_button_b">Butonul B</string> + <string name="ctrl_button_x">Butonul X</string> + <string name="ctrl_button_y">Butonul Y</string> + <string name="ctrl_button_lb">Bumperul stâng</string> + <string name="ctrl_button_rb">Bumperul drept</string> + <string name="ctrl_button_lt">Trigger-ul stâng</string> + <string name="ctrl_button_rt">Trigger-ul drept</string> + <string name="ctrl_button_view">Butonul View</string> + <string name="ctrl_button_menu">Butonul Menu</string> + <string name="ctrl_target_view">View</string> + <string name="ctrl_target_menu">Menu</string> + <string name="ctrl_target_up">D-pad sus</string> + <string name="ctrl_target_down">D-pad jos</string> + <string name="ctrl_target_left">D-pad stânga</string> + <string name="ctrl_target_right">D-pad dreapta</string> + <string name="ctrl_target_hidden">Ascuns</string> + <string name="ctrl_editor_drag">Trage pentru a mișca</string> + <string name="ctrl_editor_snap">Aliniere</string> + <string name="ctrl_editor_save">Salvează</string> + <string name="second_mode_none">Niciunul</string> + <string name="second_mode_keyboard_trackpad">Tastatură + trackpad</string> + <string name="second_mode_terminal">Terminal</string> + <string name="second_mode_deck_controls">Mânere + trackpaduri Deck</string> + + <string name="rtinst_removing">Se elimină mediul de execuție Linux</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">Se elimină mediul de execuție Linux · %1$d intrare</item> + <item quantity="few">Se elimină mediul de execuție Linux · %1$d intrări</item> + <item quantity="other">Se elimină mediul de execuție Linux · %1$d de intrări</item> + </plurals> + <string name="rtinst_removed">Mediul de execuție Linux eliminat</string> + <string name="rtinst_removal_failed">Eliminarea mediului de execuție Linux nu a putut fi terminată. Reîncearcă eliminarea din Configurare.</string> + <string name="rtinst_removing_leftovers">Se elimină resturile mediului de execuție</string> + <string name="rtinst_downloading">Se descarcă</string> + <string name="rtinst_verifying">Se verifică</string> + <string name="rtinst_extracting">Se extrage</string> + <string name="rtinst_channel_description">Apare cât timp mediul de execuție Linux este instalat sau eliminat</string> + <string name="rtinst_title_installing">Se instalează mediul de execuție Linux</string> + <string name="rtinst_title_removing">Se elimină mediul de execuție Linux</string> + + <string name="deskpkg_runtime_missing">Mediul de execuție Linux nu este instalat</string> + <string name="deskpkg_no_checksum">Catalogul nu are o sumă de control pentru %1$s</string> + <string name="deskpkg_checksum_mismatch">Sumă de control incorectă - nimic nu a fost modificat</string> + <string name="deskpkg_installing">Se instalează %1$s</string> + <string name="deskpkg_place_failed">AppImage-ul nu a putut fi plasat</string> + <string name="deskpkg_extract_failed">Extragerea a eșuat</string> + <string name="deskpkg_install_failed">Instalarea a eșuat</string> + + <string name="flatpakmgr_setup_failed">Configurarea Flatpak a eșuat (cod de ieșire %1$d)</string> + <string name="flatpakmgr_setup_unfinished">Configurarea Flatpak nu s-a terminat</string> + <string name="flatpakmgr_busy">O altă sarcină a magazinului este în curs</string> + <string name="flatpakmgr_not_ready">Configurează mai întâi Flatpak</string> + <string name="flatpakmgr_starting">Se pornește</string> + <string name="flatpakmgr_exit_status">Flatpak s-a închis cu starea %1$d</string> + <string name="flatpakmgr_installing">Se instalează %1$s</string> + <string name="flatpakmgr_updating">Se actualizează %1$s</string> + <string name="flatpakmgr_removing">Se elimină %1$s</string> + <string name="flatpakmgr_installing_step">Se instalează %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_updating_step">Se actualizează %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_removing_step">Se elimină %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_ref_translations">traduceri</string> + <string name="flatpakmgr_ref_graphics_drivers">drivere grafice</string> + <string name="flatpakmgr_ref_media_codecs">codecuri media</string> + <string name="flatpakmgr_ref_runtime">mediul de execuție %1$s</string> + <string name="flatpakmgr_ref_runtime_branch">mediul de execuție %1$s %2$s</string> + + <string name="appimg_too_small">Fișierul este prea mic pentru a fi un AppImage</string> + <string name="appimg_unreadable">Fișierul nu poate fi citit: %1$s</string> + <string name="appimg_not_linux">Acesta nu este un AppImage (nu este un program Linux)</string> + <string name="appimg_wrong_arch">Acest AppImage nu este compilat nici pentru ARM64 (aarch64), nici pentru PC-uri x86</string> + <string name="appimg_not_appimage">Acesta este un program Linux, dar nu este un AppImage</string> + <string name="appimg_wrong_type">Doar AppImage-urile de tip 2 pot fi importate (acesta este de tip %1$d)</string> + <string name="appimg_no_space_import">Spațiu liber insuficient: importul necesită circa %1$s</string> + <string name="appimg_no_space_update">Spațiu liber insuficient: actualizarea necesită circa %1$s</string> + <string name="appimg_import_failed">Importul a eșuat</string> + <string name="appimg_gone">Acel AppImage nu mai există</string> + <string name="appimg_update_failed">Actualizarea a eșuat</string> + <string name="appimg_extracting">Se extrage %1$s</string> + <string name="appimg_extract_failed">AppImage-ul nu a putut fi extras</string> + <string name="appimg_extract_failed_detail">AppImage-ul nu a putut fi extras: %1$s</string> + + <string name="deckymgr_stop_session_loader">Oprește sesiunea activă înainte de a instala Decky Loader</string> + <string name="deckymgr_download_failed">Descărcarea Decky Loader a eșuat</string> + <string name="deckymgr_size_mismatch">Dimensiunea Decky Loader nu s-a potrivit cu metadatele versiunii</string> + <string name="deckymgr_checksum_download_failed">Suma de control a versiunii nu a putut fi descărcată</string> + <string name="deckymgr_checksum_missing">Suma de control a versiunii lipsește</string> + <string name="deckymgr_checksum_invalid">Suma de control a versiunii este invalidă</string> + <string name="deckymgr_checksum_mismatch">Sumă de control incorectă pentru Decky Loader</string> + <string name="deckymgr_not_elf">Fișierul versiunii nu este un executabil Linux pe 64 de biți compatibil</string> + <string name="deckymgr_session_started_loader">O sesiune a pornit în timpul descărcării; oprește-o înainte de a instala Decky Loader</string> + <string name="deckymgr_loader_not_executable">PluginLoader nu a putut fi marcat executabil</string> + <string name="deckymgr_cef_failed">Depanarea la distanță Steam CEF nu a putut fi activată</string> + <string name="deckymgr_stage_failed">PluginLoader nu a putut fi pregătit</string> + <string name="deckymgr_loader_preserve_failed">PluginLoader-ul existent nu a putut fi păstrat</string> + <string name="deckymgr_loader_activate_failed">Noul PluginLoader nu a putut fi activat</string> + <string name="deckymgr_stop_session_plugin">Oprește sesiunea activă înainte de a instala un plugin</string> + <string name="deckymgr_loader_required">Instalează Decky Loader înainte de a importa un plugin</string> + <string name="deckymgr_zip_unreadable">Arhiva ZIP selectată nu a putut fi citită</string> + <string name="deckymgr_plugins_dir_failed">Directorul de pluginuri Decky nu a putut fi creat</string> + <string name="deckymgr_extracting_zip">Se extrage arhiva ZIP a pluginului</string> + <string name="deckymgr_zip_one_folder">Arhiva ZIP a pluginului trebuie să conțină un singur folder de plugin la nivel superior</string> + <string name="deckymgr_zip_missing_files">Arhivei ZIP a pluginului îi lipsesc plugin.json, package.json sau dist/index.js</string> + <string name="deckymgr_plugin_name_missing">Numele pluginului lipsește</string> + <string name="deckymgr_session_started_plugin">O sesiune a pornit în timpul instalării; oprește-o înainte de a instala un plugin</string> + <string name="deckymgr_plugin_installed">Plugin instalat · repornește Steam pentru a-l încărca</string> + <string name="deckymgr_plugin_install_failed">Instalarea arhivei ZIP a pluginului a eșuat</string> + <string name="deckymgr_zip_too_many">Arhiva ZIP a pluginului conține prea multe intrări</string> + <string name="deckymgr_zip_invalid_path">Arhiva ZIP a pluginului conține o cale invalidă</string> + <string name="deckymgr_zip_duplicate_paths">Arhiva ZIP a pluginului conține căi duplicate</string> + <string name="deckymgr_zip_unsafe_path">Arhiva ZIP a pluginului conține o cale nesigură</string> + <string name="deckymgr_folder_failed">Folderul pluginului nu a putut fi creat</string> + <string name="deckymgr_zip_too_large">Arhiva ZIP a pluginului se extinde peste limita de 1 GiB</string> + <string name="deckymgr_bundled_not_executable">Binarul pluginului inclus nu a putut fi marcat executabil</string> + <string name="deckymgr_remote_entry_invalid">Intrare remote_binary invalidă</string> + <string name="deckymgr_remote_name_invalid">Nume de binar la distanță invalid</string> + <string name="deckymgr_remote_url_https">URL-ul binarului la distanță trebuie să folosească HTTPS</string> + <string name="deckymgr_remote_hash_invalid">Binarul la distanță %1$s are un hash SHA-256 invalid</string> + <string name="deckymgr_bin_dir_failed">Directorul bin al pluginului nu a putut fi creat</string> + <string name="deckymgr_downloading_binary">Se descarcă binarul pluginului · %1$s</string> + <string name="deckymgr_binary_download_failed">Binarul pluginului %1$s nu a putut fi descărcat</string> + <string name="deckymgr_binary_checksum_mismatch">Sumă de control incorectă pentru binarul pluginului · %1$s</string> + <string name="deckymgr_binary_not_executable">Binarul pluginului nu a putut fi marcat executabil · %1$s</string> + <string name="deckymgr_binary_replace_failed">Binarul pluginului nu a putut fi înlocuit · %1$s</string> + <string name="deckymgr_binary_install_failed">Binarul pluginului nu a putut fi instalat · %1$s</string> + <string name="deckymgr_folder_taken">Folderul pluginului aparține altui plugin</string> + <string name="deckymgr_preserve_failed">Pluginul existent nu a putut fi păstrat</string> + <string name="deckymgr_activate_failed">Pluginul importat nu a putut fi activat</string> + <string name="deckymgr_settings_failed">Setările pluginului Decky nu au putut fi actualizate</string> + <string name="deckymgr_restore_failed">Pluginul existent nu a putut fi restaurat</string> + + <string name="adbfix_save_failed">Identitatea Depanării wireless nu a putut fi salvată</string> + <string name="adbfix_command_failed">Comanda ADB a eșuat (%1$d)</string> + <string name="adbfix_not_confirmed">Android nu a confirmat modificarea limitei proceselor-copil: %1$s</string> + <string name="adbfix_no_pairing">Nicio asociere de Depanare wireless nu este salvată</string> + <string name="adbfix_enter_address">Introdu adresa IP și portul curente ale Depanării wireless</string> + + <string name="adbpair_timed_out">Timpul a expirat în așteptarea ferestrei de asociere. Încearcă din nou din DroidDeck.</string> + <string name="adbpair_discovery_failed_code">Fereastra de asociere nu a putut fi căutată (%1$d). Verifică dacă Wi-Fi-ul este activat.</string> + <string name="adbpair_discovery_failed">Fereastra de asociere nu a putut fi căutată: %1$s</string> + <string name="adbpair_six_digits">Introdu toate cele 6 cifre ale codului</string> + <string name="adbpair_pairing">Se asociază…</string> + <string name="adbpair_code_rejected">Acel cod nu a funcționat. Deschide din nou fereastra de asociere și introdu noul său cod.</string> + <string name="adbpair_connecting">Asociat. Se conectează…</string> + <string name="adbpair_port_not_found">Asociat, dar portul de Depanare wireless nu a fost găsit. Lasă Depanarea wireless activată și încearcă din nou.</string> + <string name="adbpair_applying">Se aplică setarea…</string> + <string name="adbpair_command_failed">Comanda Depanării wireless a eșuat</string> + <string name="adbpair_channel_name">Configurarea Depanării wireless</string> + <string name="adbpair_channel_description">Cere codul de asociere al Depanării wireless cât timp Setările sunt deschise</string> + <string name="adbpair_title_pair">Asociază Depanarea wireless</string> + <string name="adbpair_waiting_text">Activează Depanarea wireless, apoi apasă „Asociază dispozitivul cu un cod de asociere”.</string> + <string name="adbpair_waiting_long">Activează Depanarea wireless, apoi apasă „Asociază dispozitivul cu un cod de asociere”. Rămâi în Setări: codul apare în această notificare.</string> + <string name="adbpair_code_label">Cod de asociere din 6 cifre</string> + <string name="adbpair_code_hint">Apasă Introdu codul și scrie cele 6 cifre arătate în Setări.</string> + <string name="adbpair_title_code">Introdu codul de asociere Wi-Fi</string> + <string name="adbpair_title_retry">Încearcă codul din nou</string> + <string name="adbpair_enter_code">Introdu codul</string> + <string name="adbpair_title_working">Se configurează Steam</string> + <string name="adbpair_title_done">Steam este pregătit</string> + <string name="adbpair_done_text">Apasă pentru a reveni la DroidDeck. Poți dezactiva acum Depanarea wireless.</string> + <string name="adbpair_title_failed">Configurarea Depanării wireless s-a oprit</string> + + <string name="selfinst_http">Descărcarea a răspuns cu HTTP %1$d</string> + <string name="selfinst_size_mismatch">Dimensiunea descărcării nu s-a potrivit cu APK-ul publicat și a fost abandonată - încearcă din nou</string> + <string name="selfinst_checksum_mismatch">Descărcarea nu s-a potrivit cu suma sa de control și a fost abandonată - încearcă din nou</string> + <string name="selfinst_wrong_package">Actualizarea este pentru pachetul Android %1$s, nu %2$s</string> + <string name="selfinst_metadata_signer">Metadatele actualizării nu numesc cheia de semnare de lansare a DroidDeck</string> + <string name="selfinst_unreadable">Android nu a putut citi APK-ul descărcat</string> + <string name="selfinst_apk_wrong_package">APK-ul descărcat este pentru %1$s, nu %2$s</string> + <string name="selfinst_version_mismatch">APK-ul descărcat spune versionCode %1$d, nu cel publicat %2$d</string> + <string name="selfinst_downgrade">Android nu va instala versionCode %1$d peste cel instalat %2$d</string> + <string name="selfinst_no_signing">APK-ul descărcat nu are informații de semnare</string> + <string name="selfinst_wrong_signer">APK-ul descărcat nu este semnat cu cheia de lansare a DroidDeck</string> + <string name="appupd_http">Catalogul de actualizări a răspuns cu HTTP %1$d</string> + + <string name="loadstate_starting">Se pornește sesiunea…</string> + <string name="loadstate_elapsed">%1$d:%2$02d scurse · încă se lucrează</string> + <string name="loadstate_steam_update">Se descarcă actualizarea clientului Steam · %1$d%%</string> + <string name="loadstate_steam_client">Se descarcă clientul Steam · %1$s</string> + + <string name="pextra_busy">O instalare a unui instrument de compatibilitate este deja în curs</string> + <string name="pextra_stop_session">Oprește sesiunea activă înainte de a instala instrumente de compatibilitate</string> + <string name="pextra_no_release">Nu s-a găsit o versiune ARM64 pentru %1$s</string> + <string name="pextra_no_space">Este necesar cel puțin 4 GB liber, plus dimensiunea descărcării, pentru a instala %1$s</string> + <string name="pextra_downloading">Se descarcă %1$s %2$s</string> + <string name="pextra_retrying">Se reîncearcă descărcarea · tentativa %1$d din %2$d</string> + <string name="pextra_interrupted">Descărcare întreruptă</string> + <string name="pextra_download_failed">Descărcarea a eșuat; fișierul parțial este păstrat astfel încât o reîncercare să poată continua</string> + <string name="pextra_too_big">Descărcarea a depășit dimensiunea versiunii și a fost ștearsă</string> + <string name="pextra_incomplete">Descărcarea a fost incompletă; fișierul parțial este păstrat pentru o reîncercare care poate continua</string> + <string name="pextra_verifying">Se verifică descărcarea</string> + <string name="pextra_checksum_unreadable">Suma de control a versiunii nu a putut fi citită; încearcă din nou</string> + <string name="pextra_no_checksum">%1$s %2$s nu publică o sumă de control; nimic nu a fost instalat</string> + <string name="pextra_checksum_mismatch">Sumă de control incorectă; descărcarea a fost ștearsă</string> + <string name="pextra_session_started">O sesiune a pornit în timpul descărcării; oprește-o înainte de a instala instrumente de compatibilitate</string> + <string name="pextra_installing">Se instalează %1$s</string> + <string name="pextra_install_failed">Instalarea %1$s a eșuat (cod de ieșire %2$d)</string> + <string name="pextra_could_not_install">%1$s nu a putut fi instalat</string> + <string name="esync_fetching">Se obține pachetul droiddeck-esync</string> + + <string name="cmgr_fex_linux_helpers">cu auxiliare Linux</string> + <string name="cmgr_fex_dlls_only">doar DLL-uri</string> + <string name="cmgr_original_proton_updated">Original (Proton actualizat de la ultima comutare)</string> + <string name="cmgr_changed_outside">Modificat în afara Componentelor</string> + <string name="cmgr_not_present">absent</string> + <string name="cmgr_original_version">Original %1$s</string> + <string name="cmgr_original_from">Original din %1$s</string> + <string name="cmgr_swap_queued">%1$s %2$s intră în %3$s când jocul care rulează pe el se închide.</string> + <string name="cmgr_swap_done">%1$s %2$s este acum în %3$s. Se aplică la următoarea pornire a unui joc.</string> + <string name="cmgr_original_missing">Acel pachet original nu este salvat</string> + <string name="cmgr_restore_queued">%1$s al %2$s revine la original când jocul care rulează pe el se închide.</string> + <string name="cmgr_restore_done">%1$s al %2$s restaurat din originalul său %3$s.</string> + <string name="cmgr_package_in_use">%1$s este în uz în %2$s. Comută mai întâi acel Proton la altceva.</string> + <string name="cmgr_package_not_stored">%1$s nu este salvat</string> + <string name="cmgr_deleted">Șters %1$s.</string> + <string name="cmgr_original_kept">Originalul versiunii instalate este păstrat: este calea de revenire.</string> + <string name="cmgr_not_stored">Nesalvat</string> + <string name="cmgr_deleted_original">S-a șters originalul %1$s al %2$s.</string> + <string name="cmgr_nightlies_only">Descărcările vin doar din versiunile Nightlies</string> + <string name="cmgr_list_predates_checksums">Această listă de pachete e anterioară sumelor de control - reîmprospătează-o și încearcă din nou</string> + <string name="cmgr_checksum_mismatch">Sumă de control incorectă</string> + + <string name="sprefs_upscaler_linear">Liniar</string> + <string name="sprefs_upscaler_nearest">Cel mai apropiat</string> + <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (calitate)</string> + <string name="sprefs_upscaler_sharpen">Doar claritate</string> + <string name="sprefs_lod_bias_auto">Automat (se potrivește cu scalarea)</string> + + <string name="look_off_desc">Nimic aplicat.</string> + <string name="look_game_clarity">Claritate de joc</string> + <string name="look_game_clarity_desc">Claritate cu puțin contrast - cea de zi cu zi.</string> + <string name="look_vivid">Viu</string> + <string name="look_vivid_desc">Culoare intensificată puternic. Eliminarea benzilor activată, ca cerurile să nu aibă dungi.</string> + <string name="look_cinematic">Cinematic</string> + <string name="look_cinematic_desc">Contrast mai profund, culoare mai rece, margini netezite.</string> + <string name="look_competitive">Competitiv</string> + <string name="look_competitive_desc">Umbre ridicate și claritate puternică - observă persoanele mai întâi.</string> + <string name="look_adaptive_sharpen">Claritate adaptivă</string> + <string name="look_adaptive_sharpen_desc">Doar claritate, culoarea neschimbată.</string> + <string name="look_filmic">Filmic</string> + <string name="look_filmic_desc">Curbă asemănătoare filmului, puțin estompată.</string> + <string name="look_arcade">Arcade</string> + <string name="look_arcade_desc">Zgomotos și intens.</string> + <string name="look_retro_crt">CRT retro</string> + <string name="look_retro_crt_desc">Tubul CRT și semnalul analogic trec împreună.</string> + <string name="look_upscale_sharp">Scalare clară</string> + <string name="look_upscale_sharp_desc">Pentru a rula sub rezoluția panoului.</string> + <string name="look_pixel_clean">Pixeli curați</string> + <string name="look_pixel_clean_desc">Pixeli clari pentru 2D și titluri vechi.</string> + <string name="look_anime_edge">Margini anime</string> + <string name="look_anime_edge_desc">Contururi toon plus claritate.</string> + + <string name="gpuinfo_not_adreno">Nu este un GPU Adreno</string> + <string name="gpuinfo_supported">Acceptat</string> + <string name="gpuinfo_experimental">Experimental: driverele sale sunt versiuni de test</string> + <string name="gpuinfo_below_tested">Sub hardware-ul testat (Adreno 725 și mai nou): poate să nu ruleze</string> + <string name="gpuinfo_unsupported">Neacceptat: DroidDeck are nevoie de un GPU Adreno (Snapdragon)</string> + + <string name="turnip_rate_limited">S-a atins limita de solicitări a GitHub - încearcă din nou mai târziu</string> + <string name="turnip_github_http">GitHub a răspuns cu HTTP %1$d</string> + <string name="turnip_download_http">descărcarea a răspuns cu HTTP %1$d</string> + <string name="turnip_list_predates_checksums">Această listă de drivere e anterioară sumelor de control - reîmprospătează-o și încearcă din nou</string> + <string name="turnip_checksum_mismatch">Sumă de control incorectă - descărcarea a fost abandonată</string> + <string name="turnip_not_adrenotools">Nu este un driver AdrenoTools: %1$s</string> + <string name="turnip_reject_no_meta">fără meta.json</string> + <string name="turnip_reject_meta_unreadable">meta.json nu poate fi citit (%1$s)</string> + <string name="turnip_reject_linux_driver">acesta este un driver pentru mediul de execuție Linux (importă-l la „Driver pentru mediul de execuție Linux”)</string> + <string name="turnip_reject_wayland">acesta este un driver de joc Wayland, pe care această aplicație nu îl poate folosi</string> + <string name="turnip_reject_no_library_name">meta.json nu numește niciun libraryName, așa că nimic nu a putut fi oferit către AdrenoTools</string> + <string name="turnip_reject_library_missing">meta.json numește %1$s, care nu este în arhivă</string> + <string name="turnip_reject_not_aarch64">%1$s nu este o bibliotecă partajată ELF AArch64 pe 64 de biți</string> + <string name="turnip_reject_glibc">%1$s leagă glibc - un Turnip „-Linux”, care aparține la „Driver pentru mediul de execuție Linux”</string> + <string name="lvd_no_library">Niciun libvulkan_freedreno*.so în această arhivă. O arhivă Turnip Android (AdrenoTools) sau „-Wayland” nu este un driver pentru mediul de execuție Linux.</string> + <string name="lvd_not_aarch64">%1$s nu este o bibliotecă partajată ELF AArch64 pe 64 de biți.</string> + <string name="lvd_not_glibc">%1$s nu este un driver glibc - leagă libc-ul Android. Mediul de execuție Linux are nevoie de o arhivă „-Linux”; un Turnip simplu sau „-Wayland” nu poate fi încărcat deloc de clientul Steam.</string> + <string name="bundle_no_manifest">Nu este un pachet de drivere: niciun manifest.json nu numește un driver android și unul linux</string> + <string name="bundle_entry_outside">Nu este un pachet de drivere: %1$s indică în afara lui</string> + <string name="bundle_too_large">Nu este un pachet de drivere: se despachetează la mai mult de %1$d MB</string> + + <string name="gstore_sd_card">Card SD</string> + <string name="gstore_folder">Folder</string> + + <string name="logshare_chooser">Partajează jurnalele sesiunii</string> + + <string name="hdr_needs_android_10">necesită Android 10 sau mai nou (straturi de afișaj cu un spațiu de culoare)</string> + <string name="hdr_none">acest afișaj nu raportează suport HDR</string> + <string name="hdr_not_hdr10">acest afișaj acceptă %1$s, nu HDR10</string> + + <string name="gpu_this_gpu">acest GPU</string> + <string name="phantom_title_on">Limita proceselor-copil este activă</string> + <string name="phantom_title_unset">Limita proceselor-copil nu este setată</string> + <string name="phantom_title_unreadable">Limita proceselor-copil nu a putut fi verificată</string> + <string name="phantom_title_off">Limita proceselor-copil este dezactivată</string> + <string name="phantom_title_not_required">Limita proceselor-copil nu este necesară</string> + <string name="phantom_instructions_fix">%1$s Dacă nu este posibil, conectează acest dispozitiv la un computer cu ADB și rulează comanda de mai jos. Revino aici și verifică din nou.</string> + <string name="phantom_instructions_off">Sesiunile Steam pot porni.</string> + <string name="phantom_instructions_not_applicable">Această versiune de Android nu folosește această limită.</string> + <string name="phantom_fix_toggle">Activează „Dezactivează restricțiile pentru procesele-copil” în Opțiunile pentru dezvoltatori, și lasă Opțiunile pentru dezvoltatori activate după aceea.</string> + <string name="phantom_fix_no_toggle">Această versiune de Android nu are un comutator pentru asta. Folosește Depanarea wireless pentru a o schimba de pe acest dispozitiv.</string> + <string name="upd_block_no_apk">Această versiune nu are o descărcare pentru această copie de DroidDeck.</string> + <string name="upd_block_version_code">Android nu o va instala peste această versiune pentru că versionCode %1$d este sub cel instalat %2$d.</string> + <string name="upd_fail_downgrade">Android nu va instala o versiune mai veche peste una mai nouă. Rămâi pe această versiune până când canalul ajunge din urmă.</string> + <string name="upd_fail_signature">Această copie de DroidDeck este semnată diferit (o versiune locală?), așa că Android nu o va actualiza pe loc.</string> + <string name="upd_fail_storage">Nu este suficient spațiu liber pentru actualizare.</string> + <string name="upd_fail_aborted">Actualizarea a fost anulată.</string> + <string name="upd_fail_with_message">Actualizarea nu s-a instalat: %1$s</string> + <string name="upd_fail_generic">Actualizarea nu s-a instalat.</string> + <string name="saves_layout_zip">Arhivă %1$s</string> + <string name="saves_zip">arhivă</string> + <string name="saves_not_launched">%1$s nu a fost lansat încă</string> + <string name="saves_none_found">Niciun fișier de salvare găsit pentru %1$s</string> + <string name="saves_not_save_zip">%1$s nu are un folder drive_c, așa că nu este o arhivă de salvare GameHub sau Winlator</string> + <string name="gstore_cannot_create">nu se pot crea foldere în %1$s</string> + <string name="gstore_cannot_write">nu se poate scrie în %1$s</string> + <string name="gstore_cannot_write_detail">nu se poate scrie în %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">Folderul nu este disponibil</string> + <string name="gfiles_name_taken">Un fișier diferit folosește deja %1$s</string> + <string name="gfiles_art_unavailable">Folderul de grafică nu este disponibil</string> + <string name="gfiles_unrelated_inventory">Folderul are un inventar de sincronizare neînrudit</string> + <string name="gfiles_remove_failed">%1$s nu a putut fi eliminat</string> + <string name="gfiles_outside_folder">Fișierul lasă folderul de export</string> + <string name="cmgr_fex_unnamed">FEX (versiune fără nume de versiune)</string> + <string name="cmgr_no_profile">niciun profile.json în %1$s</string> + <string name="cmgr_not_component_package">%1$s nu este un pachet de componente Linux (tip %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s liber</string> + <string name="mode_storage_auto_short">Automat</string> + <string name="appupd_catalog_rejected">Catalogul de actualizări nu a trecut verificările (%1$s)</string> + <string name="main_adb_clip_label">Setarea proceselor-copil DroidDeck</string> + <string name="setup_signed_in_offline_unnamed">Autentificat · mod offline</string> + <string name="setup_signed_in_unnamed">Autentificat</string> + <string name="driver_tag_bundled">INCLUS</string> + <string name="driver_tag_downloaded">DESCĂRCAT</string> + <string name="driver_tag_imported">IMPORTAT</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Afișajul sesiunii</string> + <string name="display_resolution">Rezoluție</string> + <string name="display_game_fit">Potrivire joc 16:9</string> + <string name="display_game_fit_hint">Cum se potrivesc jocurile 16:9 cu acest afișaj. Sesiunea următoare.</string> + <string name="display_game_fit_unavailable">Alege Potrivire ecran sau o dimensiune personalizată mai îngustă decât 16:9.</string> + <string name="display_preserve">Păstrează proporțiile</string> + <string name="display_stretch">Întinde pe afișaj</string> + <string name="display_image_scaling">Scalarea imaginii</string> + <string name="display_filter">Filtru de scalare</string> + <string name="display_filter_hint">Partajat de Steam și Desktop.</string> + <string name="display_filter_live">Se aplică live. Partajat de Steam și Desktop.</string> + <string name="display_filter_note">FSR, GSR și NIS se aplică la mărirea imaginii sesiunii. Doar claritate funcționează și fără mărire.</string> + <string name="display_sharpness">Claritatea filtrului</string> + <string name="display_frame_rate">Rata de cadre</string> + <string name="display_look_filter">%1$s Setează scalarea la %2$s.</string> + <string name="display_match_screen">Potrivire ecran (%1$d×%2$d)</string> + <string name="display_follow_screen">Urmărește ecranul (se redimensionează la împăturire sau desfacere)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Arată detaliile dispozitivului</string> + <string name="device_info_hide">Ascunde detaliile dispozitivului</string> + <string name="device_info_loading">Se citește dispozitivul…</string> + <string name="device_info_copy">Copiază</string> + <string name="device_info_copy_hint">Copiază aceste detalii într-un raport de eroare.</string> + <string name="device_info_copied">Copiat.</string> + <string name="force_ssbs_title">Forțează SSBS</string> + <string name="force_ssbs_hint">Păstrează o setare de viteză a CPU-ului pe care Wine o dezactivează mereu. Poate face jocurile Windows mai rapide pe Snapdragon 8 Elite; schimbare mică observată pe cipuri mai vechi. Se aplică la următoarea lansare a jocului.</string> + + <string name="rail_stores">Magazine</string> + <string name="source_steam">Steam</string> + <string name="source_added">Personalizat</string> + <string name="setup_stores">Magazine</string> + <string name="setup_stores_open_on">Deschide un magazin pe</string> + <string name="setup_stores_speed">Nivelul vitezei de descărcare</string> + <string name="setup_stores_engine">Motorul magazinelor</string> + <string name="setup_stores_engine_chip_ready">● Motor pregătit</string> + <string name="setup_stores_engine_chip_missing">Necompilat</string> + <string name="stores_tier_balanced">Echilibrat</string> + <string name="stores_tier_fast">Rapid</string> + <string name="stores_tier_max">Maxim</string> + <string name="stores_title">Magazine</string> + <string name="stores_settings">Setările magazinelor</string> + <string name="stores_settings_eyebrow">Setări</string> + <string name="stores_chip_signed_out">(neautentificat)</string> + <string name="stores_tab_store">Magazin</string> + <string name="stores_tab_installed">Instalate</string> + <string name="stores_tab_library">Bibliotecă</string> + <string name="stores_tab_all">Toate</string> + <string name="stores_tab_downloads">Descărcări</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Autentificare</string> + <string name="stores_signout_named">%1$s · deconectare</string> + <string name="stores_signout_title">Te deconectezi de la %1$s?</string> + <string name="stores_signout_note">Jocurile instalate rămân. Biblioteca este reîncărcată când te autentifici din nou.</string> + <string name="stores_signout_stay">Rămâi autentificat</string> + <string name="stores_signout_go">Deconectare</string> + <string name="stores_search_catalog">Caută în catalogul %1$s…</string> + <string name="stores_search_installed">Caută jocurile instalate…</string> + <string name="stores_search_library">Caută în biblioteca ta…</string> + <string name="stores_search_open">Căutare</string> + <string name="stores_installed_empty">Nimic instalat</string> + <string name="stores_library_loading">Se încarcă</string> + <string name="stores_nothing_matches">Niciun joc</string> + <string name="stores_shelves_loading">Se încarcă magazinul…</string> + <string name="stores_amazon_no_catalog">Niciun catalog</string> + <string name="stores_shelf_new">Noutăți</string> + <string name="stores_shelf_deals">Oferte</string> + <string name="stores_shelf_free">Jocuri gratuite</string> + <string name="stores_shelf_trending">În tendințe</string> + <string name="stores_shelf_library">Biblioteca ta</string> + <string name="stores_play">Joacă</string> + <string name="stores_install">Instalează</string> + <string name="stores_resume_install">Reia instalarea</string> + <string name="stores_dl_interrupted">Întreruptă</string> + <string name="stores_install_size">Instalare · %1$s</string> + <string name="stores_get_free">Obține gratuit</string> + <string name="stores_view_on">Vezi pe %1$s</string> + <string name="stores_buy_price">Cumpără · %1$s</string> + <string name="stores_installed_chip">Instalat</string> + <string name="stores_in_library">În bibliotecă</string> + <string name="stores_free">Gratuit</string> + <string name="stores_resume">Reluare</string> + <string name="stores_refresh">Reîmprospătează</string> + <string name="stores_show_mature">Arată conținutul pentru adulți</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Offline</string> + <string name="epic_card_none">Dezactivat</string> + <string name="epic_eos">Autentificare Epic (EOS)</string> + <string name="epic_offline">Lansează offline</string> + <string name="epic_resolve">Rezolvă autentificarea Epic</string> + <string name="store_cloud_saves">Salvări în cloud</string> + <string name="cloud_upload">Încarcă în cloud</string> + <string name="cloud_download">Descarcă din cloud</string> + <string name="cloud_none">Nicio salvare în cloud</string> + <string name="cloud_conflict">Conflict în cloud</string> + <string name="cloud_conflict_files">Conflict în cloud · %1$d fișiere</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Păstrează cloud</string> + <string name="cloud_keep_local">Păstrează local</string> + <string name="cloud_checking">Se verifică cloud-ul…</string> + <string name="cloud_last_sync">Ultima sincronizare %1$s</string> + <string name="cloud_never">Nesincronizat încă</string> + <string name="cloud_done">%1$d fișiere</string> + <string name="stores_eyebrow_installed">instalat</string> + <string name="stores_eyebrow_library">în biblioteca ta</string> + <string name="stores_eyebrow_store">magazin</string> + <string name="achievements_title">Realizări</string> + <string name="achievements_count">%1$d din %2$d deblocate</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Deblocate · %1$d</string> + <string name="achievements_group_locked">Blocate · %1$d</string> + <string name="achievements_group_hidden">Ascunse · %1$d</string> + <string name="achievements_hidden">Realizare ascunsă</string> + <string name="achievements_hidden_detail">Detaliile apar după deblocare.</string> + <string name="achievements_all">Toate realizările deblocate</string> + <string name="achievements_reading">Se încarcă…</string> + <string name="achievements_signed_out">Conectează-te la %1$s pentru a-ți vedea realizările.</string> + <string name="achievements_unreachable">Nu s-a putut contacta %1$s. Verifică din nou când ești online.</string> + <string name="achievements_none">%1$s nu listează realizări pentru acest joc.</string> + <string name="achievements_show_all">Arată toate cele %1$d</string> + <string name="achievements_dialog_title">Realizări · %1$s</string> + <string name="stores_in_steam">✓ În Steam</string> + <string name="stores_uninstall">Dezinstalează</string> + <string name="stores_uninstalling">Se dezinstalează…</string> + <string name="stores_uninstall_title">Dezinstalezi %1$s?</string> + <string name="stores_uninstall_body_size">Eliberează %1$s și îl elimină din Steam.</string> + <string name="stores_uninstall_body">Elimină fișierele sale și comanda rapidă din Steam.</string> + <string name="stores_launch_not_registered">Steam preia jocul la următoarea pornire; lansează-l de acolo, sau pornește Steam acum.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Alegerea Steam per joc</string> + <string name="stores_card_audio">Sunet</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Componente</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Motor</string> + <string name="stores_card_engine_value">%1$s · nativ</string> + <string name="stores_card_engine_java">%1$s · integrat</string> + <string name="stores_how_it_runs">Cum rulează</string> + <string name="stores_kv_fetched">Obținut din</string> + <string name="stores_kv_fetched_value">%1$s, cu contul tău</string> + <string name="stores_kv_installed_to">Instalat în</string> + <string name="stores_kv_installed_to_value">Stocarea jocurilor (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Lansat de</string> + <string name="stores_kv_launched_by_value">Steam și Proton, aceeași cale ca jocurile Steam</string> + <string name="stores_kv_launched_by_epic">Steam și Proton · Autentificare Epic la fiecare lansare, suprapunerea EOS dezactivată</string> + <string name="stores_kv_launched_by_amazon">Steam și Proton · Variabile Amazon Games Services setate</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">adăugat la Steam la instalare (interfață Deck + Steam Input)</string> + <string name="stores_downloads_empty">Nicio descărcare</string> + <string name="stores_dl_paused">Pe pauză</string> + <string name="stores_dl_queued">În așteptare</string> + <string name="stores_dl_queued_at">În așteptare · #%1$d</string> + <string name="stores_dl_cancelled">Anulată</string> + <string name="stores_dl_failed">Eșuată</string> + <string name="stores_dl_failed_reason">Eșuată · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Pauză</string> + <string name="stores_dl_resume">Reluare</string> + <string name="stores_dl_cancel">Anulează</string> + <string name="stores_dl_retry">Reîncearcă</string> + <string name="stores_dl_clear">Șterge</string> + <string name="stores_dl_clear_all">Șterge cele finalizate</string> + <string name="stores_stage_manifest">Manifest</string> + <string name="stores_stage_download">Descărcare</string> + <string name="stores_stage_verify">Verificare</string> + <string name="stores_stage_install">Instalare</string> + <string name="stores_stage_done">Terminat</string> + <string name="stores_stage_checking">Se verifică</string> + <string name="stores_stage_downloading">Se descarcă</string> + <string name="stores_dl_cancel_confirm">Ștergi descărcarea?</string> + <string name="stores_dl_failed_kept">Eșuată · %1$s păstrat</string> + <string name="stores_stage_verifying">Se verifică</string> + <string name="stores_stage_installing">Se instalează</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d fișiere</string> + <string name="stores_download_manager">Administrator de descărcări</string> + <string name="stores_parallel">Descărcări simultane</string> + <string name="stores_install_where">Instalează în</string> + <string name="stores_install_title">Instalezi %1$s:</string> + <string name="stores_target_internal">Internă</string> + <string name="stores_target_free">%1$s liber</string> + <string name="stores_notification_channel">Descărcări din magazin</string> + <string name="stores_notification_channel_hint">Arată progresul descărcării jocului și păstrează descărcările în execuție în fundal</string> + <string name="stores_notification_one">Se descarcă</string> + <string name="stores_notification_many">%1$d descărcări</string> + <string name="stores_notification_waiting">Se pregătește…</string> + <string name="stores_notification_done_channel">Descărcări din magazin finalizate</string> + <string name="stores_notification_installed">%1$s instalat</string> + <string name="stores_notification_failed">Descărcarea %1$s a eșuat</string> + <string name="stores_notification_delete">Șterge</string> + <string name="stores_notification_keep">Păstrează</string> + <string name="stores_login_title">Autentifică-te la %1$s</string> + <string name="stores_login_provider_title">Autentifică-te cu furnizorul tău</string> + <string name="stores_login_reload">Reîncarcă</string> + <string name="stores_login_close">Închide</string> + <string name="stores_login_retry">Încearcă din nou</string> + <string name="stores_login_finishing">Se finalizează autentificarea…</string> + <string name="stores_login_error_network">Pagina de autentificare nu a putut fi încărcată: %1$s</string> + <string name="stores_login_error_idp_blocked">Acest furnizor refuză autentificarea dintr-o pagină încorporată. Autentifică-te cu parola magazinului în schimb.</string> + <string name="stores_login_error_http">Pagina de autentificare a răspuns cu o eroare (HTTP %1$d).</string> + <string name="stores_login_error_ssl">Conexiunea la pagina de autentificare nu este sigură.</string> + <string name="stores_login_error_verification">Autentificarea nu a putut fi verificată. Încearcă din nou.</string> + <string name="stores_login_error_generic">Autentificarea a eșuat. Încearcă din nou.</string> + <string name="common_close">Închide</string> + <string name="common_back">Înapoi</string> + <string name="common_edit">Editează</string> + <string name="games_add_all">Adaugă toate jocurile din acest folder</string> + <string name="games_added_none">Nimic nou în acest folder</string> + <string name="edit_name">Nume</string> + <string name="edit_target">Țintă</string> + <string name="edit_start_in">Pornește în</string> + <string name="edit_launch_options">Opțiuni de lansare</string> + <string name="edit_artwork">Grafică</string> + <string name="edit_other_exe">Alt exe…</string> + <string name="edit_remove">Elimină</string> + <string name="art_cover">Copertă</string> + <string name="art_background">Fundal</string> + <string name="art_logo">Logo</string> + <string name="art_icon">Iconiță</string> + <string name="art_src_folder">Folderul jocului</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">Fișierul tău</string> + <string name="art_src_exe">Din exe</string> + <string name="art_src_auto">Automat</string> + <string name="art_pick_image">Alege imaginea…</string> + <string name="art_pick_title">Alege o imagine: %1$s</string> + <string name="art_reset">Resetează la automat</string> + <plurals name="games_added_n"> + <item quantity="one">%1$d joc adăugat</item> + <item quantity="few">%1$d jocuri adăugate</item> + <item quantity="other">%1$d de jocuri adăugate</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="one">%1$d deja în Jocuri</item> + <item quantity="few">%1$d deja în Jocuri</item> + <item quantity="other">%1$d deja în Jocuri</item> + </plurals> + <string name="hop_back">B · Înapoi la %1$s</string> + <string name="hop_from_games">joc</string> + <string name="hop_from_play">Setările de lansare</string> + <string name="sheet_graphics_drivers">Drivere grafice</string> + <string name="comp_this_proton">Acest Proton</string> + <string name="comp_every_proton">Fiecare Proton</string> + <string name="comp_for_every_proton">Pentru fiecare Proton</string> + <string name="comp_available_count">Disponibile · %1$d</string> + <string name="comp_checked">Verificat %1$s</string> + <string name="comp_check_all">Verifică componente și drivere noi</string> + <string name="comp_get">Obține</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton și FEX de mai jos sunt ale acestui Proton</string> + <string name="comp_device_tested">%1$s · testat</string> + <string name="comp_device_untested">%1$s · netestat</string> + <string name="comp_device_unsupported">%1$s · neacceptat</string> + <string name="gpu_auto_keeps">Păstrează perechea recomandată actualizată</string> + <string name="gpu_bar_auto">Automat · %1$s</string> + <string name="gpu_separately">Drivere pentru mediul de execuție și pentru afișaj, separat</string> + <string name="gpu_use_title">Folosești %1$s?</string> + <string name="proton_remove_title">Elimini %1$s?</string> + <string name="setup_tab_diagnostics">Diagnosticare</string> + <string name="setup_ready">Pregătit de joc</string> + <string name="setup_not_ready">Încă nepregătit de joc</string> + <string name="setup_ready_runtime">mediu de execuție %1$s</string> + <string name="setup_ready_limit_off">limita proceselor dezactivată</string> + <string name="setup_ready_signed_in">autentificat ca %1$s</string> + <string name="setup_needs_you">Când ceva are nevoie de tine</string> + <string name="setup_group_look">Aspect</string> + <string name="setup_group_device">Acest dispozitiv</string> + <string name="setup_group_art">Grafica bibliotecii</string> + <string name="flathub_eyebrow">APLICAȚII LINUX</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Aplicații desktop pentru desktopul Linux, deschise din Desktop.</string> + <string name="flathub_setup">Configurează Flathub</string> + <string name="flathub_remove_title">Elimini %1$s?</string> + <string name="flathub_remove_note">Setările și fișierele sale sunt păstrate.</string> + <string name="flathub_keep">Păstrează</string> + <string name="desktop_size">Dimensiune pe disc</string> + <string name="desktop_uninstall_title">Dezinstalezi desktopul?</string> + <string name="desktop_all_systems">Toate</string> + <string name="gog_achievements">Realizări GOG</string> + <string name="gog_achievements_hint">Sincronizează realizările pentru jocurile GOG compatibile. Se aplică la următoarea lansare.</string> + <string name="gog_achievements_off">Realizări dezactivate</string> + <string name="gog_achievements_on">Realizări activate</string> + <string name="gog_achievements_signin">Conectează-te din nou la GOG</string> + <string name="gog_achievements_uploaded">Realizări încărcate</string> + <string name="gog_achievements_pending">Încărcarea realizării este în așteptare</string> + <string name="gog_achievements_enable_store">Activează realizările GOG din setările Stores</string> + <string name="gog_achievements_unavailable">Realizări indisponibile; verifică jurnalele sesiunii</string> + <string name="achievement_unlocked">Realizare deblocată</string> + <string name="touch_paste">Inserează</string> + <string name="touch_dpad">Cruce directională</string> + <string name="touch_left_stick">Stick stânga</string> + <string name="touch_right_stick">Stick dreapta</string> + <string name="touch_menu">Meniu</string> + <string name="touch_keyboard">Tastatură</string> + <string name="touch_dpad_up">Cruce directională sus</string> + <string name="touch_dpad_down">Cruce directională jos</string> + <string name="touch_dpad_left">Cruce directională stânga</string> + <string name="touch_dpad_right">Cruce directională dreapta</string> + <string name="touch_title">Comenzi tactile</string> + <string name="touch_controller">Controler tactil</string> + <string name="touch_mouse">Mouse</string> + <string name="touch_on">Activat</string> + <string name="touch_off">Dezactivat</string> + <string name="touch_speed_slow">Lentă</string> + <string name="touch_speed_normal">Normală</string> + <string name="touch_speed_fast">Rapidă</string> + <string name="touch_edit_layout">Editează aranjamentul</string> + <string name="touch_droiddeck_menu">Meniul DroidDeck</string> + <string name="touch_drag_hint">Trage elementele de control pe ecran</string> + <string name="touch_arrange">Aranjează</string> + <string name="touch_discard">Renunță</string> + <string name="touch_saving">Se salvează…</string> + <string name="touch_icon">Pictogramă</string> + <string name="touch_narrower">L−</string> + <string name="touch_wider">L+</string> + <string name="touch_shorter">Î−</string> + <string name="touch_taller">Î+</string> + <string name="touch_hide">Ascunde</string> + <string name="touch_color_opacity">Culoare și opacitate…</string> + <string name="touch_copy_layout">Copiază acest aranjament</string> + <string name="touch_layout_copied">Aranjament copiat; lipește-l în aranjamentul tactil al oricărui joc</string> + <string name="touch_paste_layout">Lipește aranjamentul copiat</string> + <string name="touch_reset_layout">Revino la cel implicit din Steam</string> + <string name="touch_no_binding">Acest element de control nu are o asociere în Steam pentru a i se atribui o pictogramă</string> + <string name="touch_icon_for">Pictogramă pentru %1$s</string> + <string name="touch_layout_color">Culoarea aranjamentului tactil</string> + <string name="touch_done">Terminat</string> + <string name="touch_save_failed">Aranjamentul tactil nu a putut fi salvat</string> + <string name="touch_saved">Aranjamentul tactil a fost salvat în Steam</string> + <string name="touch_app_id">Aplicația %1$d</string> + <string name="touch_label_hint">Etichetă (opțional)</string> + <string name="touch_icons_missing">Pictogramele Steam nu au fost găsite</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d pictogramă de la Steam</item> + <item quantity="few">%1$d pictograme de la Steam</item> + <item quantity="other">%1$d de pictograme de la Steam</item> + </plurals> + <string name="touch_icon_color">Culoarea pictogramei</string> + <string name="touch_button_color">Culoarea butonului</string> + <string name="touch_no_icon">Fără pictogramă</string> + + <string name="stores_sync_sign_in">Se verifică autentificarea…</string> + <string name="stores_sync_library">Se obține biblioteca ta…</string> + <string name="stores_sync_game_list">Se obține lista de jocuri…</string> + <string name="stores_sync_details">Se încarcă detaliile jocurilor… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">Se sincronizează %1$d joc…</item> + <item quantity="few">Se sincronizează %1$d jocuri…</item> + <item quantity="other">Se sincronizează %1$d de jocuri…</item> + </plurals> + <string name="stores_sync_progress">Se sincronizează… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Se obțin copertele…</string> + <string name="stores_sync_failed">Sincronizarea a eșuat</string> + <string name="stores_sync_no_games">Nu s-au găsit jocuri în biblioteca ta %1$s</string> + <string name="stores_sync_amazon_no_answer">Amazon Games nu a răspuns pentru biblioteca ta: reîmprospătează sau autentifică-te din nou</string> + <string name="stores_sync_gog_unreachable">Nu s-a putut contacta GOG</string> + <string name="stores_sync_gog_unreadable">Biblioteca ta GOG nu a putut fi citită</string> + <string name="stores_registering">Se înregistrează la Steam…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Mod Cardboard</string> + <string name="cardboard_mode_hint">Imagini pentru ochiul stâng și cel drept, pentru un vizor VR pentru telefon.</string> + <string name="cardboard_source">Imagine</string> + <string name="cardboard_source_screen">Ecran virtual</string> + <string name="cardboard_source_sbs">3D alăturat</string> + <string name="cardboard_viewer">Vizor</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Fără lentilă</string> + <string name="cardboard_ipd">Distanța dintre ochi</string> + <string name="cardboard_screen_width">Lățimea ecranului</string> + <string name="cardboard_screen_distance">Distanța ecranului</string> + <string name="cardboard_head_tracking">Urmărirea capului</string> + <string name="cardboard_no_sensor">Acest dispozitiv nu are senzor de rotație.</string> + <string name="cardboard_recenter">Recentrează vederea</string> + <string name="cardboard_recenter_button">Recentrează</string> + <string name="cardboard_steamvr">SteamVR (experimental)</string> + <string name="cardboard_steamvr_install">Instalează</string> + <string name="cardboard_steamvr_install_hint">Descarcă SteamVR ARM64 de la Valve (571 MB).</string> + <string name="cardboard_steamvr_installing">Se descarcă și se dezarhivează SteamVR…</string> + <string name="cardboard_steamvr_start">Pornește</string> + <string name="cardboard_steamvr_ready_hint">Rulează SteamVR cu capul tău drept cască; imaginea trece la 3D alăturat.</string> + <string name="cardboard_steamvr_stop">Oprește</string> + <string name="cardboard_steamvr_running_hint">SteamVR rulează.</string> + <string name="cardboard_steamvr_unavailable">Indisponibil în această sesiune.</string> + <string name="cardboard_steamvr_failed">SteamVR nu a răspuns; verifică jurnalul sesiunii.</string> + <string name="drawer_component_downloads">Descarcă componente</string> + <string name="drawer_component_downloads_hint">Descărcările sunt comune cu pagina Componente. Alege mai sus o versiune descărcată pentru a o folosi la următoarea lansare a jocului.</string> + <string name="drawer_component_choose_download">Alege o versiune de descărcat</string> + <string name="stores_fact_developer">Dezvoltator</string> + <string name="stores_fact_publisher">Editor</string> + <string name="stores_fact_released">Lansare</string> + <string name="stores_hltb_main">Principal</string> + <string name="stores_hltb_extras">+ Extra</string> + <string name="stores_hltb_completionist">Completist</string> + <string name="stores_hltb_hours">%1$s h</string> + <string name="stores_hltb_minutes">%1$d min</string> +</resources> diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 822694ddc..cf06c4e4c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -15,8 +15,10 @@ <string name="session_channel">Сессия</string> <string name="session_channel_description">Отображается, пока открыта сессия Steam или рабочего стола</string> <string name="session_notification">Steam запущен</string> + <string name="session_notification_downloads">Идет загрузка в Steam</string> <string name="session_notification_paused">Сессия приостановлена — нажмите «Продолжить»</string> <string name="resume_session">Продолжить</string> + <string name="suspend_now">Приостановить сейчас</string> <string name="stop_session">Остановить сессию</string> <string name="frame_gen_title">Генерация кадров</string> <string name="frame_gen_hint">Выберите режим генерации кадров</string> @@ -182,6 +184,8 @@ <string name="add_app_flatpak_id">ID приложения</string> <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> <string name="add_app_flatpak_invalid">Недействительный ID приложения Flathub</string> + <string name="add_app_flatpak_bundle_hint">Или файл .flatpak — для приложения, которого нет в Flathub.</string> + <string name="add_app_pick_flatpak">Выбрать файл .flatpak</string> <string name="add_app_source">Источник</string> <string name="add_app_appearance">На рабочем столе</string> <string name="add_app_preview_name">Новое приложение</string> @@ -251,6 +255,10 @@ <string name="mode_osc">Экранные элементы управления</string> <string name="mode_osc_qam">Steam + QAM</string> <string name="mode_osc_note">Автоматический режим показывает экранные элементы управления, если контроллер не подключен.</string> + <string name="osc_steam_touch">Сенсорное управление Steam (бета)</string> + <string name="osc_style_next_session">Сенсорное управление Steam включится со следующей сессии Steam</string> + <string name="osc_steam_layout">Сенсорная раскладка Steam</string> + <string name="osc_steam_layout_hint">Перемещайте, меняйте размер или скрывайте сенсорные кнопки этой игры; сохраняется в Steam</string> <string name="mode_controller">Контроллер</string> <string name="mode_controller_hint">Как Steam распознает ваш контроллер. Вступит в силу в следующей сессии.</string> <string name="mode_controller_deck">Контроллер Steam Deck</string> @@ -260,12 +268,16 @@ <string name="mode_controller_x360">Контроллер Xbox 360</string> <string name="mode_controller_note">Deck: кнопка быстрого доступа и гироскоп. Xbox 360: используйте Guide+A для быстрого доступа.</string> <string name="mode_back">Назад</string> + <string name="back_menu_then_qam">Меню → QAM</string> + <string name="back_qam_then_menu">QAM → Меню</string> <string name="mode_session">Сессия</string> <string name="mode_suspend">Поведение в фоне</string> <string name="mode_suspend_hint">Поведение при скрытии приложения или выключении экрана.</string> <string name="mode_suspend_manual">Вручную</string> <string name="mode_suspend_native">Штатный</string> <string name="mode_suspend_steam_note">Авто: пауза и возобновление при показе. Штатный: сначала подготавливает Steam к переходу в режим сна. Вручную: ждет нажатия «Продолжить». Никогда: сессия продолжает работать.</string> + <string name="mode_background_downloads">Не прерывать загрузки</string> + <string name="mode_background_downloads_hint">Откладывает приостановку в фоне, пока Steam что-то скачивает.</string> <string name="mode_suspend_note">Авто: пауза в фоне и возобновление при показе. Вручную: ждет нажатия «Продолжить». Никогда: сессия продолжает работать.</string> <string name="mode_startup">Запуск</string> <string name="mode_steam_startup">Запускать Steam при открытии DroidDeck</string> @@ -302,6 +314,52 @@ <string name="games_pick_exe_title">Выберите exe:</string> <string name="games_add_unreachable">Это расположение недоступно в сеансе</string> <string name="mode_added_exe_check">Проверьте этот exe</string> + <string name="wincomp_title">Компоненты Windows</string> + <string name="wincomp_pad_hint">LB / RB — переход между разделами.</string> + <string name="wincomp_card_none">Нет</string> + <string name="wincomp_applies">DLL для Windows из списка компонентов, которых нет в Proton. Включите те, что нужны этой игре: они попадают в собственную папку Windows игры (ее файлы не затрагиваются) и сохраняются при любой версии Proton.</string> + <string name="wincomp_offline">Не удалось получить список компонентов. Уже скачанные компоненты можно включать и выключать.</string> + <string name="wincomp_recommended">Рекомендуется для этой игры</string> + <string name="wincomp_checking">Проверка, что нужно этой игре…</string> + <string name="wincomp_when_online">Доступно при подключении к сети</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">Включение %1$d компонента…</item> + <item quantity="few">Включение %1$d компонентов…</item> + <item quantity="many">Включение %1$d компонентов…</item> + <item quantity="other">Включение %1$d компонента…</item> + </plurals> + <string name="wincomp_auto_section">Включено для этой игры</string> + <string name="wincomp_auto_steam">Steam устанавливает %1$s вместе с %2$s (приложение %3$s)</string> + <string name="wincomp_auto_gog">Из списка GOG для этой игры</string> + <string name="wincomp_auto_epic">Из требований Epic для этой игры</string> + <string name="wincomp_auto_amazon">Из шагов установки Amazon для этой игры</string> + <string name="wincomp_steam_by_name">Steam устанавливает %1$s с приложением %2$s (совпадение по имени)</string> + <string name="wincomp_suggestions">Также в списке</string> + <string name="wincomp_suggestion_reason">В списке %1$s; здесь это не компонент</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">Игра поставляется с %1$s</string> + <string name="wincomp_found_shipped">В игре есть %1$s (часто работает как есть)</string> + <string name="wincomp_already_here">Уже есть в этой игре</string> + <string name="wincomp_no_recommendation">Ничто в папке игры не указывает на нужный компонент.</string> + <string name="wincomp_active">Установлено для этой игры (%1$d)</string> + <string name="wincomp_all">Все компоненты (%1$d)</string> + <string name="wincomp_installers">Нужен установщик Windows (%1$d)</string> + <string name="wincomp_installers_note">Это отдельные программы установки: внутри нет пакета, который можно разобрать, и пока нет записи того, что они устанавливают. В основном это старые среды выполнения, которые уже заменяет Mono внутри Proton; записи добавляются по мере появления.</string> + <string name="wincomp_show">Показать</string> + <string name="wincomp_hide">Скрыть</string> + <string name="wincomp_needs_installer">Запускает свою программу установки (записи пока нет)</string> + <string name="wincomp_unsupported">Здесь пока не поддерживается</string> + <string name="wincomp_loading">Загрузка компонентов…</string> + <string name="wincomp_failed">Не удалось установить: %1$s</string> + <string name="wincomp_downloaded">Скачано</string> + <string name="wincomp_downloading">Загрузка %1$s</string> + <string name="wincomp_installing">Установка: %1$s</string> + <string name="wincomp_reverting">Отключение…</string> + <string name="wincomp_download_size">Загрузка: %1$s</string> + <string name="wincomp_next_launch">Вступит в силу при следующем запуске игры.</string> <string name="mode_stretch169">Растянуть игры 16:9</string> <string name="mode_stretch169_hint">На экранах уже 16:9 растягивает игры на весь экран вместо черных полос. Вступит в силу в следующей сессии.</string> <string name="mode_audio">Аудио</string> @@ -359,6 +417,44 @@ <string name="comp_check_nightlies">Проверить новые пакеты в Nightlies</string> <string name="comp_proton">Proton</string> <string name="comp_proton_note">Выбранный здесь Proton запускает все игры, для которых в Steam не выбран свой; изменение, сделанное в Steam, тоже отображается здесь. При обновлении Steam заменяет Proton от Valve; оригинальные файлы каждой сборки сохраняются.</string> + <string name="comp_scope">Конфигурация</string> + <string name="comp_profile_configure">Настроить профиль</string> + <string name="comp_profile">Профиль</string> + <string name="comp_scope_general">Профиль по умолчанию</string> + <string name="comp_scope_general_detail">Автоматически используется играми без переопределений.</string> + <string name="comp_profile_game_detail">Сохраненные настройки, которые автоматически применяются при запуске %1$s.</string> + <string name="comp_game_hint">Выберите только те настройки, которые нужны этой игре. Все остальное по-прежнему берется из профиля по умолчанию.</string> + <string name="comp_game_fex">Пресет FEX</string> + <string name="comp_game_fex_inherited">Используется пресет из профиля по умолчанию.</string> + <string name="comp_game_fex_override">Для этой игры сохранен свой пресет.</string> + <string name="comp_game_fex_custom">Свои переменные FEX</string> + <string name="comp_game_use_general">По умолчанию</string> + <string name="comp_game_compatibility">Совместимость игры</string> + <string name="comp_game_environment_hint">Параметры среды Proton, DXVK, VKD3D и синхронизации для этой игры.</string> + <string name="comp_game_proton_inherited">Используется Proton из профиля по умолчанию.</string> + <string name="comp_game_proton_override">Для этой игры сохранен свой Proton через настройку совместимости Steam.</string> + <string name="comp_no_proton_short">Без Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Использует пакет из профиля по умолчанию от %1$s, если для этой игры не выбран другой пакет.</string> + <string name="comp_game_component_general">Использует пакет компонентов из профиля по умолчанию.</string> + <string name="comp_game_component_override">Использует %1$s только для этой игры.</string> + <string name="comp_game_runtime_options">Параметры среды выполнения</string> + <string name="comp_game_environment_other_hint">Другие параметры среды Proton, DXVK, VKD3D-Proton и синхронизации для отдельной игры.</string> + <string name="comp_game_saved_next">Профиль %1$s сохранен и применится при следующем запуске игры.</string> + <string name="game_no_steam_title">Запуск без Steam</string> + <string name="game_no_steam_detail">Для игры, которая сразу закрывается: запускает её без настройки Steam от Proton. Функции Steam, такие как оверлей, для неё отключены.</string> + <string name="game_no_steam_on">Включено</string> + <string name="comp_game_proton_live">Выбор Proton для %1$s сохранен и уже доступен в Steam.</string> + <string name="comp_game_proton_next">Выбор Proton для %1$s сохранен. Перезапустите Steam перед запуском игры.</string> + <string name="comp_game_proton_not_runnable">%1$s не может использовать выбранный Proton на этом устройстве.</string> + <string name="comp_game_load_failed">Не удалось загрузить профиль %1$s: %2$s</string> + <string name="comp_game_save_failed">Не удалось сохранить профиль игры: %1$s</string> + <string name="comp_game_reset">Сбросить профиль игры</string> + <string name="comp_game_reset_title">Сбросить профиль %1$s?</string> + <string name="comp_game_reset_body">Удалить переопределения Proton, FEX, пакета компонентов, среды, фильтрации текстур и компонентов Windows для этой игры? Она будет использовать профиль по умолчанию.</string> + <string name="comp_game_reset_done">%1$s теперь использует профиль по умолчанию.</string> + <string name="comp_game_reset_failed">Не удалось полностью сбросить профиль игры: %1$s</string> <string name="comp_default_live">%1$s теперь запускает все игры, для которых в Steam не выбран свой Proton.</string> <string name="comp_default_next">%1$s будет запускать все игры, для которых не выбран свой Proton, со следующего запуска Steam.</string> <string name="comp_default_not_runnable">%1$s не может запускать игры здесь, поэтому не может быть Proton по умолчанию.</string> @@ -522,6 +618,21 @@ <string name="drawer_stop_title">Остановить сессию?</string> <string name="drawer_running_now">Сейчас выполняется</string> <string name="drawer_no_game">Сейчас нет игр, запущенных через Proton</string> + <string name="drawer_selected_profile">Профиль игры</string> + <string name="drawer_selected_profile_none">Сведения о профиле пока недоступны.</string> + <string name="drawer_profile_refresh_failed_initial">Не удалось загрузить профили игр. Повторная попытка будет выполнена автоматически.</string> + <string name="drawer_profile_refresh_failed_stale">Не удалось обновить профили игр. Показаны последние доступные данные; повторная попытка будет выполнена автоматически.</string> + <string name="drawer_profile_inspect_hint">Просмотр и изменение сохраненных настроек для следующего запуска.</string> + <string name="drawer_profile_default">Используется профиль по умолчанию</string> + <string name="drawer_profile_custom">Свой профиль</string> + <string name="drawer_component_inherited">Используется %1$s из профиля по умолчанию.</string> + <string name="drawer_game_components_note">Изменения сохраняются в профиль %1$s и вступят в силу при следующем запуске игры.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d переопределение для игры</item> + <item quantity="few">%1$d переопределения для игры</item> + <item quantity="many">%1$d переопределений для игры</item> + <item quantity="other">%1$d переопределения для игры</item> + </plurals> <string name="drawer_no_proton">Proton не установлен.</string> <string name="drawer_game_running">игра запущена</string> <string name="drawer_reapplied">повторно применено при запуске %1$s</string> @@ -578,9 +689,21 @@ <string name="setup_saved_logs">Сохраненные журналы сессии</string> <string name="setup_saved_logs_hint">Хранятся последние %1$d</string> <string name="setup_clear_logs">Очистить журналы</string> + <string name="setup_reset_settings">Сбросить все настройки</string> + <string name="setup_reset_settings_hint">Возвращает все настройки к значениям по умолчанию</string> + <string name="setup_reset_settings_button">Сбросить</string> + <string name="setup_reset_settings_title">Сбросить все настройки?</string> + <string name="setup_reset_settings_body">Настройки экрана, производительности, контроллера, темы, языка и лаунчера вернутся к значениям по умолчанию. Ваши игры, вход в Steam, аккаунты магазинов, загруженные драйверы и папки хранилища сохранятся.</string> + <string name="setup_reset_settings_profiles">Также сбросить профили игр (Proton, FEX, компоненты и окружение каждой игры, а также окружение профиля по умолчанию)</string> + <string name="setup_reset_settings_session_running">Остановите сессию перед сбросом настроек.</string> + <string name="setup_reset_settings_partial">Настройки сброшены, но некоторые части сбросить не удалось. Подробности в журналах.</string> <string name="setup_offline">Автономный режим</string> <string name="setup_sign_in_first">Сначала войдите в Steam</string> <string name="setup_launcher">Лаунчер</string> + <string name="setup_language">Язык</string> + <string name="setup_language_hint">Steam тоже будет его использовать после следующего запуска.</string> + <string name="setup_language_system">Как в системе (%1$s)</string> + <string name="setup_language_system_short">Как в системе</string> <string name="setup_app_scale">Масштаб приложения</string> <string name="setup_app_scale_hint">Текст и элементы управления DroidDeck</string> <string name="setup_app_scale_default">%1$d%% (по умолчанию)</string> @@ -589,6 +712,10 @@ <string name="setup_home">Использовать как главный экран</string> <string name="setup_home_on">DroidDeck может использоваться как приложение главного экрана</string> <string name="setup_home_off">DroidDeck не будет доступен как приложение главного экрана</string> + <string name="setup_orientation">Ориентация</string> + <string name="setup_orientation_auto">Альбомная (авто)</string> + <string name="setup_orientation_landscape">Альбомная</string> + <string name="setup_orientation_reverse">Перевернутая альбомная</string> <string name="setup_fullscreen">Полноэкранный режим</string> <string name="setup_fullscreen_on">Скрывает строку состояния и панель навигации Android</string> <string name="setup_fullscreen_off">Показывает строку состояния и панель навигации Android</string> @@ -855,6 +982,12 @@ <string name="content_page_gone">Эта страница больше не существует. Выберите раздел слева.</string> <string name="content_open_desktop">Открыть рабочий стол</string> <string name="content_install_open_desktop">Установить и открыть рабочий стол</string> + <string name="desktop_remove_title">Удалить рабочий стол</string> + <string name="desktop_remove_text">Удаление рабочего стола и встроенных в него приложений. Игры, добавленные приложения, настройки и общие библиотеки сохранятся.</string> + <string name="desktop_removing">Удаление рабочего стола</string> + <string name="desktop_removed">Рабочий стол удален</string> + <string name="desktop_cleaning_legacy">Удаление старых файлов LXQt</string> + <string name="desktop_removal_unknown">Обновите DroidDeck, прежде чем удалять эту версию рабочего стола.</string> <string name="content_linux_desktop">Рабочий стол Linux</string> <string name="content_linux_desktop_hint">KDE Plasma, Firefox и ваши эмуляторы</string> <string name="games_empty">Игры, установленные в Steam, появятся здесь после завершения сессии.</string> @@ -884,6 +1017,8 @@ <string name="games_last_played">Последний запуск %1$s</string> <string name="games_internal">Внутреннее хранилище</string> <string name="games_added">Добавленная игра</string> + <string name="games_custom">Своя игра</string> + <string name="games_store_storage">Хранилище игр</string> <string name="gpu_auto_hint">Лучшая пара для этого GPU, обновляется автоматически</string> <string name="gpu_manual_hint">Выбрать комбинацию драйверов вручную</string> <string name="gpu_checking">Проверка драйверов…</string> @@ -925,6 +1060,12 @@ <string name="deck_tab_left">Левый тачпад</string> <string name="deck_tab_right">Правый тачпад</string> <string name="deck_tab_both">Оба</string> + <string name="deck_tab_gamepad">Геймпад</string> + <string name="deck_surface_stick">Стик</string> + <string name="deck_surface_trackpad">Тачпад</string> + <string name="deck_surface_switch">Переключение между стиком и тачпадом</string> + <string name="deck_more_controls">Другие элементы управления Deck</string> + <string name="deck_panels">ПАНЕЛИ</string> <string name="deck_close">Закрыть управление вторым экраном</string> <string name="deck_open_steam_menu">Открыть меню Steam</string> <string name="deck_left_upper">Слева · сверху</string> @@ -960,6 +1101,10 @@ <string name="proton_installed">Установлено: %1$s</string> <string name="proton_queued">В очереди по предыдущему запросу</string> <string name="proton_not_installed">Не установлено</string> + <string name="proton_update_available">Доступно обновление → %1$s</string> + <string name="proton_remove_older">Удалить старые версии %1$s</string> + <string name="proton_remove_older_body">Будут удалены: %1$s. Игры, настроенные на эти версии, нужно переключить на другой Proton.</string> + <string name="proton_remove_body">Игры, настроенные на эту версию, нужно переключить на другой Proton.</string> <string name="proton_install_now">Установить сейчас</string> <string name="proton_cancel_queued">Убрать из очереди</string> <string name="pckb_android">Клавиатура Android</string> @@ -988,6 +1133,8 @@ <string name="game_env_eyebrow">Игры Proton</string> <string name="pip_title">Картинка в картинке</string> <string name="pip_auto">Автоматически переходить в режим PiP</string> + <string name="unfolded_controls">Управление на разложенном экране</string> + <string name="unfolded_controls_hint">В разложенном виде игра находится над тачпадами и грипсами Steam Deck. В сложенном — игра на весь экран.</string> <string name="pip_suspended">Приостановлено</string> <string name="pip_not_ready">PiP станет доступен после готовности сессии.</string> <string name="pip_permission">Разрешите DroidDeck использовать PiP в настройках Android.</string> @@ -1012,8 +1159,735 @@ <string name="game_file_folder">Выбрать папку для файлов игры фронтенда</string> <string name="game_file_exported">Файлы игры сохранены в %1$s</string> <string name="game_frontend_files">Файлы фронтенда</string> - <string name="fm_up_one_level">На уровень вверх</string> + + <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> + <string name="main_saves_importing">Импорт в %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="one">Импортирован %1$d файл (%2$s) в %3$s</item> + <item quantity="few">Импортировано %1$d файла (%2$s) в %3$s</item> + <item quantity="many">Импортировано %1$d файлов (%2$s) в %3$s</item> + <item quantity="other">Импортировано %1$d файла (%2$s) в %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">Импортирован %1$d файл (%2$s) в %3$s. Старые сохранения скопированы в Download/DroidDeck/Saves/backups</item> + <item quantity="few">Импортировано %1$d файла (%2$s) в %3$s. Старые сохранения скопированы в Download/DroidDeck/Saves/backups</item> + <item quantity="many">Импортировано %1$d файлов (%2$s) в %3$s. Старые сохранения скопированы в Download/DroidDeck/Saves/backups</item> + <item quantity="other">Импортировано %1$d файла (%2$s) в %3$s. Старые сохранения скопированы в Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Выберите zip-архив сохранений для %1$s</string> + <string name="main_saves_exporting">Экспорт %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="one">Экспортирован %1$d файл (%2$s): %3$s</item> + <item quantity="few">Экспортировано %1$d файла (%2$s): %3$s</item> + <item quantity="many">Экспортировано %1$d файлов (%2$s): %3$s</item> + <item quantity="other">Экспортировано %1$d файла (%2$s): %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Выберите, куда сохранить %1$s (%2$s)</string> + <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> + <plurals name="main_logs_cleared"> + <item quantity="one">Очищена %1$d папка журналов сессии.</item> + <item quantity="few">Очищено %1$d папки журналов сессии.</item> + <item quantity="many">Очищено %1$d папок журналов сессии.</item> + <item quantity="other">Очищено %1$d папки журналов сессии.</item> + </plurals> + <string name="main_wadb_pair_failed">Ошибка сопряжения для отладки по Wi-Fi</string> + <string name="main_wadb_command_failed">Ошибка команды отладки по Wi-Fi</string> + <string name="main_wadb_unavailable">Отладка по Wi-Fi недоступна</string> + <string name="main_adb_copied">Команда ADB скопирована</string> + <string name="main_pick_roms">Выберите папку ROM</string> + <string name="main_non_adreno_title">GPU не Adreno</string> + <string name="main_non_adreno_text">Turnip поддерживает GPU Adreno. На %1$s в Steam может быть черный экран. Загрузка: %2$.0f МБ.</string> + <string name="main_install_anyway">Все равно установить</string> + <string name="main_remove_runtime_title">Удалить среду выполнения Linux</string> + <string name="main_remove_runtime_text">Будут удалены среда выполнения, клиент Steam в ней и все установленные туда игры.</string> + <string name="main_applied">Применено: %1$s</string> + <string name="android_app_open_failed_primary">Не удалось открыть %1$s на главном экране</string> + <string name="android_app_open_failed_on">Не удалось открыть %1$s на экране «%2$s»</string> + <string name="android_app_open_failed_display">Не удалось открыть %1$s на экране %2$d</string> + <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> + <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> + <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> + <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">Выберите ZIP-файл плагина Decky</string> + <string name="main_storage_not_usable">Нельзя использовать: %1$s</string> + <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> + + <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> + <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> + + <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> + + <string name="drivers_not_checked">Еще не проверялось — нажмите «Обновить», чтобы найти новые драйверы</string> + <string name="drivers_bundle_sets_display">Android + Linux: заодно задает драйвер дисплея</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: заодно задает драйвер среды выполнения</string> + <string name="drivers_runtime_builtin">Turnip, встроенный в среду выполнения</string> + <string name="drivers_system_vulkan">системный Vulkan: для этого GPU нет встроенной сборки</string> + <string name="drivers_bundled">%1$s (встроенный)</string> + <string name="drivers_manual">Вручную</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">Сейчас опубликована только одна половина</string> + <string name="drivers_pair_size">%1$.0f МБ за оба</string> + <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> + <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> + <string name="drivers_switched_to">Выбран %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Выбран %1$s %2$s. Драйвер дисплея применится после перезапуска DroidDeck.</string> + <string name="drivers_using">Используется %1$s %2$s.</string> + <string name="drivers_using_restart">Используется %1$s %2$s. Драйвер дисплея применится после перезапуска DroidDeck.</string> + <string name="drivers_not_a_driver">Это не zip-архив драйвера: ни драйвер AdrenoTools, ни -Linux Turnip, ни пакет Android + Linux</string> + <string name="drivers_import_failed">Ошибка импорта: %1$s</string> + <string name="drivers_imported_unnamed">Импортированный драйвер</string> + <string name="drivers_using_bundle">%1$s используется как драйвер среды выполнения и драйвер дисплея.</string> + <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> + <string name="drivers_release_status">Последние: %1$s · проверено %2$s</string> + <string name="drivers_release_status_failed">Последние: %1$s · %2$s недоступно · проверено %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">%1$d мин назад</item> + <item quantity="few">%1$d мин назад</item> + <item quantity="many">%1$d мин назад</item> + <item quantity="other">%1$d мин назад</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">%1$d ч назад</item> + <item quantity="few">%1$d ч назад</item> + <item quantity="many">%1$d ч назад</item> + <item quantity="other">%1$d ч назад</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">%1$d день назад</item> + <item quantity="few">%1$d дня назад</item> + <item quantity="many">%1$d дней назад</item> + <item quantity="other">%1$d дня назад</item> + </plurals> + <string name="drivers_checking_sources">Проверка Banners-Turnip, WinNative и DroidDeck…</string> + <string name="drivers_check_failed">ошибка проверки</string> + <string name="drivers_installed_pick_either">Установлен %1$s — выберите его в любом из списков, чтобы задать оба драйвера</string> + <string name="drivers_installed_pick">Установлен %1$s — выберите его в меню</string> + + <string name="session_wait_removal">Дождитесь завершения удаления среды выполнения</string> + <string name="session_title_game">Игра</string> + <string name="session_touch_auto_touchpad">тачпад</string> + <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> + <string name="session_named_paused">%1$s на паузе</string> + <string name="session_program_paused">Программа приостановлена</string> + <string name="session_starting_desktop">Запуск рабочего стола</string> + <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> + <string name="session_runtime_downloading">загрузка среды выполнения Linux</string> + <string name="session_runtime_downloading_mb">загрузка среды выполнения Linux · %1$d из %2$d МБ</string> + <string name="session_runtime_checking">проверка среды выполнения Linux</string> + <string name="session_runtime_unpacking">распаковка среды выполнения Linux</string> + <string name="session_desktop_downloading">загрузка рабочего стола</string> + <string name="session_desktop_downloading_mb">загрузка рабочего стола · %1$d из %2$d МБ</string> + <string name="session_desktop_checking">проверка рабочего стола</string> + <string name="session_desktop_unpacking">распаковка рабочего стола</string> + <string name="session_proton_downloading">загрузка Proton Experimental (ARM64)</string> + <string name="session_proton_downloading_mb">загрузка Proton Experimental (ARM64) · %1$d из %2$d МБ</string> + <string name="session_proton_checking">проверка Proton Experimental (ARM64)</string> + <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> + <string name="session_desktop_install_failed">Рабочий стол не установился (%1$s). Проверьте подключение и снова нажмите «Рабочий стол».</string> + <string name="session_ended_named">Работа %1$s неожиданно прервалась. Приложите журналы к отчету об ошибке или попробуйте снова.</string> + <string name="session_ended_program">Программа неожиданно завершила работу. Приложите журналы к отчету об ошибке или попробуйте снова.</string> + <string name="session_ended_desktop">Рабочий стол неожиданно завершил работу. Приложите журналы к отчету об ошибке или попробуйте снова.</string> + <string name="session_ended_detail">Код выхода %1$d · %2$s</string> + <string name="session_ended_no_log">нет журнала</string> + <plurals name="session_hint_enosys"> + <item quantity="one">В журнале %1$d ошибка «Function not implemented»: ускорение seccomp в proot вызывает сбой вспомогательной программы на этом устройстве. Попробуйте %2$s → «%3$s» (или «%4$s») и запустите снова.</item> + <item quantity="few">В журнале %1$d ошибки «Function not implemented»: ускорение seccomp в proot вызывает сбой вспомогательной программы на этом устройстве. Попробуйте %2$s → «%3$s» (или «%4$s») и запустите снова.</item> + <item quantity="many">В журнале %1$d ошибок «Function not implemented»: ускорение seccomp в proot вызывает сбой вспомогательной программы на этом устройстве. Попробуйте %2$s → «%3$s» (или «%4$s») и запустите снова.</item> + <item quantity="other">В журнале %1$d ошибки «Function not implemented»: ускорение seccomp в proot вызывает сбой вспомогательной программы на этом устройстве. Попробуйте %2$s → «%3$s» (или «%4$s») и запустите снова.</item> + </plurals> + + <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> + <string name="store_cat_games">Игры</string> + <string name="store_cat_audio_video">Аудио и видео</string> + <string name="store_cat_graphics">Графика</string> + <string name="store_cat_internet">Интернет</string> + <string name="store_cat_office">Офис</string> + <string name="store_cat_developer">Разработка</string> + <string name="store_cat_education">Образование</string> + <string name="store_cat_science">Наука</string> + <string name="store_cat_system">Система</string> + <string name="store_cat_utilities">Утилиты</string> + <string name="store_section_popular">Популярное</string> + <string name="store_section_trending">В тренде</string> + <string name="store_section_new">Новое</string> + <string name="store_section_updated">Обновленное</string> + <string name="store_section_loading">загрузка…</string> + <string name="store_flatpak_setup">Настройка Flatpak</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Не удалось проверить обновления в Flathub</string> + <string name="content_rom_installed">установлено</string> + <string name="content_rom_folder">папка</string> + <string name="theme_graphite">Графит</string> + <string name="theme_graphite_detail">Графитовые поверхности; синий цвет сферы из логотипа выделяет все выбранное, активное и нажатое.</string> + <string name="theme_paper">Бумага на черном</string> + <string name="theme_paper_detail">Основной цвет — белый, синий цвет значка служит акцентом для линий, колец и точек.</string> + <string name="theme_icon_blue">Синий из значка</string> + <string name="theme_icon_blue_detail">Черный фон, белый текст, синий — для всего выбранного, активного или нажатого.</string> + <string name="theme_electric">Электрик</string> + <string name="theme_electric_detail">Более яркий синий на поверхностях темно-синего, а не серого оттенка.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Пробел</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <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> + <string name="content_system_many">Множество систем</string> + <string name="fm_drive_internal">Внутреннее хранилище</string> <string name="fm_drive_linux">Система Linux</string> + <string name="fm_drive_sd">SD-карта</string> + <string name="fm_drive_storage">Хранилище</string> + <string name="fm_select">Выбрать</string> + <string name="fm_properties">Свойства</string> + <string name="fm_favorite_add">Добавить в избранное</string> + <string name="fm_favorite_remove">Удалить из избранного</string> + <string name="fm_rename">Переименовать</string> + <string name="fm_copy">Копировать</string> + <string name="fm_cut">Вырезать</string> + <string name="fm_actions">Действия</string> + <string name="fm_favorites">Избранное</string> + <string name="fm_pin_current">Закрепить текущую папку</string> + <string name="fm_no_favorites">Избранного пока нет — закрепите папку через ее меню ⋮, чтобы быстро возвращаться к ней.</string> + <string name="fm_unpin_favorite">Удалить из избранного</string> + <string name="fm_size_zero">0 байт</string> + <string name="fm_unit_bytes">байт</string> + <string name="fm_prop_location">Расположение</string> + <string name="fm_prop_type">Тип</string> + <string name="fm_prop_size">Размер</string> + <string name="fm_prop_modified">Изменено</string> + <string name="fm_type_folder">Папка</string> + <string name="fm_type_file">Файл</string> + <string name="fm_type_ext_file">Файл %1$s</string> + <string name="fm_read_only">Только чтение</string> + <string name="fm_read_only_hint">Не дает играм (и Wine) перезаписать или удалить этот файл.</string> + <string name="fm_read_only_failed">Не удалось изменить атрибут «Только чтение»</string> + <string name="fm_hidden">Скрытый</string> + <string name="fm_hidden_unsupported">Это хранилище не поддерживает атрибут «Скрытый».</string> + <string name="fm_hidden_hint">Помечает файл как скрытый в Windows (атрибут hidden в Wine).</string> + <string name="fm_hidden_failed">Не удалось изменить атрибут «Скрытый» в этом хранилище</string> + <string name="fm_done">Готово</string> + <string name="fm_op_deleting">Удаление...</string> + <string name="fm_delete_failed">Ошибка удаления</string> + <string name="fm_op_moving">Перемещение...</string> + <string name="fm_op_copying">Копирование...</string> + <string name="fm_op_moving_item">Перемещение — %1$s</string> + <string name="fm_op_copying_item">Копирование — %1$s</string> + <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> + <string name="fm_paste_failed">Готово: %1$d, ошибок: %2$d</string> + <string name="fm_paste_skipped">Готово: %1$d, пропущено: %2$d</string> + <plurals name="fm_paste_moved"> + <item quantity="one">Перемещен %1$d элемент</item> + <item quantity="few">Перемещено %1$d элемента</item> + <item quantity="many">Перемещено %1$d элементов</item> + <item quantity="other">Перемещено %1$d элемента</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">Скопирован %1$d элемент</item> + <item quantity="few">Скопировано %1$d элемента</item> + <item quantity="many">Скопировано %1$d элементов</item> + <item quantity="other">Скопировано %1$d элемента</item> + </plurals> + <string name="fm_already_exists">«%1$s» уже существует</string> + <string name="fm_op_renaming">Переименование...</string> + <string name="fm_rename_failed">Ошибка переименования</string> + <string name="fm_op_creating_folder">Создание папки...</string> + <string name="fm_create_folder_failed">Не удалось создать папку</string> + <string name="fm_new_folder">Новая папка</string> + <string name="fm_folder_name">Имя папки</string> + <string name="fm_create">Создать</string> + <string name="fm_new_name">Новое имя</string> + <string name="fm_delete_title">Удалить?</string> + <string name="fm_delete_text">Удалить «%1$s» навсегда?</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">Удалить %1$d элемент?</item> + <item quantity="few">Удалить %1$d элемента?</item> + <item quantity="many">Удалить %1$d элементов?</item> + <item quantity="other">Удалить %1$d элемента?</item> + </plurals> + <string name="fm_cannot_undo">Это действие нельзя отменить.</string> + <plurals name="fm_and_more"> + <item quantity="one">…и еще %1$d</item> + <item quantity="few">…и еще %1$d</item> + <item quantity="many">…и еще %1$d</item> + <item quantity="other">…и еще %1$d</item> + </plurals> + <string name="fm_op_deleting_batch">Удаление %1$d/%2$d — %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">Не удалось удалить %1$d элемент</item> + <item quantity="few">Не удалось удалить %1$d элемента</item> + <item quantity="many">Не удалось удалить %1$d элементов</item> + <item quantity="other">Не удалось удалить %1$d элемента</item> + </plurals> + <string name="fm_conflict_merge_hint">«Объединить» добавляет и заменяет файлы внутри существующей папки.</string> + <string name="fm_conflict_overwrite_hint">«Перезаписать» заменяет существующий файл.</string> + <string name="fm_conflict_apply_all">Применить ко всем конфликтам</string> + <string name="fm_conflict_merge">Объединить</string> + <string name="fm_conflict_overwrite">Перезаписать</string> + <string name="fm_conflict_keep_both">Оставить оба</string> + <string name="fm_conflict_skip">Пропустить</string> + <string name="fm_select_this_folder">Выбрать эту папку</string> + <string name="fm_back">Назад</string> + <string name="fm_drive_not_readable">%1$s: подключено, но сейчас недоступно для чтения</string> + <string name="fm_list_view">Список</string> + <string name="fm_grid_view">Сетка</string> + <string name="fm_search">Поиск</string> + <string name="fm_sort">Сортировка</string> + <string name="fm_sort_name">Имя</string> + <string name="fm_sort_date">Дата изменения</string> + <string name="fm_rows_comfortable">Просторные строки</string> + <string name="fm_rows_compact">Компактные строки</string> + <string name="fm_hide_hidden">Не показывать скрытые файлы</string> + <string name="fm_show_hidden">Показывать скрытые файлы</string> + <string name="fm_hide_favorites">Скрыть избранное</string> + <string name="fm_show_favorites">Показать избранное</string> + <string name="fm_filter_folder">Фильтр по этой папке</string> + <string name="fm_free_space">Свободно: %1$s</string> + <string name="fm_selected_count">Выбрано: %1$d</string> + <string name="fm_select_none">Снять</string> + <string name="fm_select_all">Все</string> + <string name="fm_paste">Вставить</string> + <plurals name="fm_items"> + <item quantity="one">%1$d элемент</item> + <item quantity="few">%1$d элемента</item> + <item quantity="many">%1$d элементов</item> + <item quantity="other">%1$d элемента</item> + </plurals> + <string name="fm_paste_here">Вставить сюда: %1$s</string> + <string name="fm_paste_move_here">Переместить сюда: %1$s</string> + <string name="fm_rail_title">Файлы</string> + <string name="fm_rail_storage">ХРАНИЛИЩЕ</string> + <string name="fm_rail_quick">БЫСТРЫЙ ДОСТУП</string> + <string name="fm_rail_favorites">ИЗБРАННОЕ</string> + <string name="fm_rail_downloads">Загрузки</string> + <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> + <string name="fm_favorite_added">Папка «%1$s» добавлена в избранное</string> + <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> + <string name="ctrl_tint_steam_blue">Синий Steam</string> + <string name="ctrl_tint_sky">Небесный</string> + <string name="ctrl_tint_white">Белый</string> + <string name="ctrl_tint_violet">Фиолетовый</string> + <string name="ctrl_tint_green">Зеленый</string> + <string name="ctrl_tint_amber">Янтарный</string> + <string name="ctrl_tint_red">Красный</string> + <string name="ctrl_tint_pink">Розовый</string> + <string name="ctrl_button_a">Кнопка A</string> + <string name="ctrl_button_b">Кнопка B</string> + <string name="ctrl_button_x">Кнопка X</string> + <string name="ctrl_button_y">Кнопка Y</string> + <string name="ctrl_button_lb">Левый бампер</string> + <string name="ctrl_button_rb">Правый бампер</string> + <string name="ctrl_button_lt">Левый триггер</string> + <string name="ctrl_button_rt">Правый триггер</string> + <string name="ctrl_button_view">Кнопка View</string> + <string name="ctrl_button_menu">Кнопка «Меню»</string> + <string name="ctrl_target_view">View</string> + <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> + <string name="ctrl_target_hidden">Скрыто</string> + <string name="ctrl_editor_drag">Перетащите для перемещения</string> + <string name="ctrl_editor_snap">Привязка</string> + <string name="ctrl_editor_save">Сохранить</string> + <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> + + <string name="rtinst_removing">Удаление среды выполнения Linux</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">Удаление среды выполнения Linux · %1$d элемент</item> + <item quantity="few">Удаление среды выполнения Linux · %1$d элемента</item> + <item quantity="many">Удаление среды выполнения Linux · %1$d элементов</item> + <item quantity="other">Удаление среды выполнения Linux · %1$d элемента</item> + </plurals> + <string name="rtinst_removed">Среда выполнения Linux удалена</string> + <string name="rtinst_removal_failed">Не удалось завершить удаление среды выполнения Linux. Повторите удаление на странице «Настройка».</string> + <string name="rtinst_removing_leftovers">Удаление остатков среды выполнения</string> + <string name="rtinst_downloading">Загрузка</string> + <string name="rtinst_verifying">Проверка</string> + <string name="rtinst_extracting">Распаковка</string> + <string name="rtinst_channel_description">Отображается во время установки или удаления среды выполнения Linux</string> + <string name="rtinst_title_installing">Установка среды выполнения Linux</string> + <string name="rtinst_title_removing">Удаление среды выполнения Linux</string> + + <string name="deskpkg_runtime_missing">Среда выполнения Linux не установлена</string> + <string name="deskpkg_no_checksum">В каталоге нет контрольной суммы для %1$s</string> + <string name="deskpkg_checksum_mismatch">Контрольная сумма не совпадает — ничего не изменено</string> + <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> + + <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> + <string name="flatpakmgr_installing">Установка %1$s</string> + <string name="flatpakmgr_updating">Обновление %1$s</string> + <string name="flatpakmgr_removing">Удаление %1$s</string> + <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> + <string name="flatpakmgr_ref_translations">переводы</string> + <string name="flatpakmgr_ref_graphics_drivers">графические драйверы</string> + <string name="flatpakmgr_ref_media_codecs">медиакодеки</string> + <string name="flatpakmgr_ref_runtime">среда выполнения %1$s</string> + <string name="flatpakmgr_ref_runtime_branch">среда выполнения %1$s %2$s</string> + + <string name="appimg_too_small">Файл слишком мал для AppImage</string> + <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">Импортировать можно только AppImage типа 2 (у этого тип %1$d)</string> + <string name="appimg_no_space_import">Недостаточно свободного места: для импорта нужно около %1$s</string> + <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> + <string name="appimg_extracting">Распаковка %1$s</string> + <string name="appimg_extract_failed">Не удалось распаковать AppImage</string> + <string name="appimg_extract_failed_detail">Не удалось распаковать AppImage: %1$s</string> + + <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> + <string name="deckymgr_checksum_download_failed">Не удалось скачать контрольную сумму выпуска</string> + <string name="deckymgr_checksum_missing">Контрольная сумма выпуска отсутствует</string> + <string name="deckymgr_checksum_invalid">Недопустимая контрольная сумма выпуска</string> + <string name="deckymgr_checksum_mismatch">Контрольная сумма Decky Loader не совпадает</string> + <string name="deckymgr_not_elf">Файл выпуска не является совместимым 64-битным исполняемым файлом Linux</string> + <string name="deckymgr_session_started_loader">Во время загрузки запустилась сессия; остановите ее перед установкой Decky Loader</string> + <string name="deckymgr_loader_not_executable">Не удалось сделать PluginLoader исполняемым</string> + <string name="deckymgr_cef_failed">Не удалось включить удаленную отладку CEF в Steam</string> + <string name="deckymgr_stage_failed">Не удалось подготовить PluginLoader</string> + <string name="deckymgr_loader_preserve_failed">Не удалось сохранить текущий PluginLoader</string> + <string name="deckymgr_loader_activate_failed">Не удалось активировать новый PluginLoader</string> + <string name="deckymgr_stop_session_plugin">Остановите текущую сессию перед установкой плагина</string> + <string name="deckymgr_loader_required">Установите Decky Loader перед импортом плагина</string> + <string name="deckymgr_zip_unreadable">Не удалось прочитать выбранный ZIP-файл</string> + <string name="deckymgr_plugins_dir_failed">Не удалось создать папку плагинов Decky</string> + <string name="deckymgr_extracting_zip">Распаковка ZIP-архива плагина</string> + <string name="deckymgr_zip_one_folder">ZIP-архив плагина должен содержать одну папку плагина верхнего уровня</string> + <string name="deckymgr_zip_missing_files">В ZIP-архиве плагина нет plugin.json, package.json или dist/index.js</string> + <string name="deckymgr_plugin_name_missing">Не указано имя плагина</string> + <string name="deckymgr_session_started_plugin">Во время установки запустилась сессия; остановите ее перед установкой плагина</string> + <string name="deckymgr_plugin_installed">Плагин установлен · перезапустите Steam, чтобы загрузить его</string> + <string name="deckymgr_plugin_install_failed">Ошибка установки ZIP-архива плагина</string> + <string name="deckymgr_zip_too_many">В ZIP-архиве плагина слишком много записей</string> + <string name="deckymgr_zip_invalid_path">ZIP-архив плагина содержит недопустимый путь</string> + <string name="deckymgr_zip_duplicate_paths">ZIP-архив плагина содержит повторяющиеся пути</string> + <string name="deckymgr_zip_unsafe_path">ZIP-архив плагина содержит небезопасный путь</string> + <string name="deckymgr_folder_failed">Не удалось создать папку плагина</string> + <string name="deckymgr_zip_too_large">После распаковки ZIP-архив плагина превышает лимит 1 ГиБ</string> + <string name="deckymgr_bundled_not_executable">Не удалось сделать исполняемым встроенный бинарный файл плагина</string> + <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> + <string name="deckymgr_remote_hash_invalid">У удаленного бинарного файла %1$s недопустимый хеш SHA-256</string> + <string name="deckymgr_bin_dir_failed">Не удалось создать папку bin плагина</string> + <string name="deckymgr_downloading_binary">Загрузка бинарного файла плагина · %1$s</string> + <string name="deckymgr_binary_download_failed">Не удалось скачать бинарный файл плагина %1$s</string> + <string name="deckymgr_binary_checksum_mismatch">Контрольная сумма бинарного файла плагина не совпадает · %1$s</string> + <string name="deckymgr_binary_not_executable">Не удалось сделать бинарный файл плагина исполняемым · %1$s</string> + <string name="deckymgr_binary_replace_failed">Не удалось заменить бинарный файл плагина · %1$s</string> + <string name="deckymgr_binary_install_failed">Не удалось установить бинарный файл плагина · %1$s</string> + <string name="deckymgr_folder_taken">Папка плагина принадлежит другому плагину</string> + <string name="deckymgr_preserve_failed">Не удалось сохранить текущий плагин</string> + <string name="deckymgr_activate_failed">Не удалось активировать импортированный плагин</string> + <string name="deckymgr_settings_failed">Не удалось обновить настройки плагинов Decky</string> + <string name="deckymgr_restore_failed">Не удалось восстановить прежний плагин</string> + + <string name="adbfix_save_failed">Не удалось сохранить идентификатор для отладки по Wi-Fi</string> + <string name="adbfix_command_failed">Ошибка команды ADB (%1$d)</string> + <string name="adbfix_not_confirmed">Android не подтвердил изменение ограничения дочерних процессов: %1$s</string> + <string name="adbfix_no_pairing">Нет сохраненного сопряжения для отладки по Wi-Fi</string> + <string name="adbfix_enter_address">Введите текущие IP-адрес и порт отладки по Wi-Fi</string> + + <string name="adbpair_timed_out">Время ожидания окна сопряжения истекло. Повторите попытку из DroidDeck.</string> + <string name="adbpair_discovery_failed_code">Не удалось найти окно сопряжения (%1$d). Убедитесь, что Wi-Fi включен.</string> + <string name="adbpair_discovery_failed">Не удалось найти окно сопряжения: %1$s</string> + <string name="adbpair_six_digits">Введите все 6 цифр кода</string> + <string name="adbpair_pairing">Сопряжение…</string> + <string name="adbpair_code_rejected">Код не подошел. Снова откройте окно сопряжения и введите новый код.</string> + <string name="adbpair_connecting">Сопряжено. Подключение…</string> + <string name="adbpair_port_not_found">Сопряжено, но порт отладки по Wi-Fi не найден. Не отключайте отладку по Wi-Fi и повторите попытку.</string> + <string name="adbpair_applying">Применение настройки…</string> + <string name="adbpair_command_failed">Ошибка команды отладки по Wi-Fi</string> + <string name="adbpair_channel_name">Настройка отладки по Wi-Fi</string> + <string name="adbpair_channel_description">Запрашивает код сопряжения для отладки по Wi-Fi, пока открыты Настройки</string> + <string name="adbpair_title_pair">Сопряжение отладки по Wi-Fi</string> + <string name="adbpair_waiting_text">Включите отладку по Wi-Fi и нажмите «Сопряжение устройства с помощью кода».</string> + <string name="adbpair_waiting_long">Включите отладку по Wi-Fi и нажмите «Сопряжение устройства с помощью кода». Не выходите из Настроек: код вводится в этом уведомлении.</string> + <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> + <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. Теперь отладку по Wi-Fi можно отключить.</string> + <string name="adbpair_title_failed">Настройка отладки по Wi-Fi остановлена</string> + + <string name="selfinst_http">Сервер загрузки ответил HTTP %1$d</string> + <string name="selfinst_size_mismatch">Размер загруженного файла не совпал с опубликованным APK, файл удален — повторите попытку</string> + <string name="selfinst_checksum_mismatch">Загруженный файл не совпал с контрольной суммой и был удален — повторите попытку</string> + <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> + <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> + <string name="selfinst_no_signing">В загруженном APK нет сведений о подписи</string> + <string name="selfinst_wrong_signer">Загруженный APK не подписан ключом релизов DroidDeck</string> + <string name="appupd_http">Каталог обновлений ответил HTTP %1$d</string> + + <string name="loadstate_starting">Запуск сессии…</string> + <string name="loadstate_elapsed">Прошло %1$d:%2$02d · работа продолжается</string> + <string name="loadstate_steam_update">Загрузка обновления клиента Steam · %1$d%%</string> + <string name="loadstate_steam_client">Загрузка клиента Steam · %1$s</string> + + <string name="pextra_busy">Инструмент совместимости уже устанавливается</string> + <string name="pextra_stop_session">Остановите текущую сессию перед установкой инструментов совместимости</string> + <string name="pextra_no_release">Не найден выпуск %1$s для ARM64</string> + <string name="pextra_no_space">Для установки %1$s нужно не менее 4 ГБ свободного места плюс размер загрузки</string> + <string name="pextra_downloading">Загрузка %1$s %2$s</string> + <string name="pextra_retrying">Повторная загрузка · попытка %1$d из %2$d</string> + <string name="pextra_interrupted">Загрузка прервана</string> + <string name="pextra_download_failed">Ошибка загрузки; частично скачанный файл сохранен, чтобы повторная попытка продолжила с того же места</string> + <string name="pextra_too_big">Загрузка превысила размер выпуска и была удалена</string> + <string name="pextra_incomplete">Загрузка не завершена; частично скачанный файл сохранен, чтобы ее можно было продолжить</string> + <string name="pextra_verifying">Проверка загрузки</string> + <string name="pextra_checksum_unreadable">Не удалось прочитать контрольную сумму выпуска; повторите попытку</string> + <string name="pextra_no_checksum">%1$s %2$s не публикует контрольную сумму; ничего не установлено</string> + <string name="pextra_checksum_mismatch">Контрольная сумма не совпадает; загруженный файл удален</string> + <string name="pextra_session_started">Во время загрузки запустилась сессия; остановите ее перед установкой инструментов совместимости</string> + <string name="pextra_installing">Установка %1$s</string> + <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> + + <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> + <string name="cmgr_not_present">отсутствует</string> + <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> + <string name="cmgr_restore_queued">%1$s в %2$s вернется к оригиналу после закрытия запущенной на нем игры.</string> + <string name="cmgr_restore_done">%1$s в %2$s восстановлен из оригинала %3$s.</string> + <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> + <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> + + <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> + + <string name="look_off_desc">Без эффектов.</string> + <string name="look_game_clarity">Игровая четкость</string> + <string name="look_game_clarity_desc">Резкость с легким добавлением контраста — на каждый день.</string> + <string name="look_vivid">Яркий</string> + <string name="look_vivid_desc">Сильно усиленные цвета. Удаление полос включено, чтобы небо не шло полосами.</string> + <string name="look_cinematic">Кинематографичный</string> + <string name="look_cinematic_desc">Глубже контраст, холоднее цвета, сглаженные края.</string> + <string name="look_competitive">Соревновательный</string> + <string name="look_competitive_desc">Высветленные тени и сильная резкость — замечайте противников первым.</string> + <string name="look_adaptive_sharpen">Адаптивная резкость</string> + <string name="look_adaptive_sharpen_desc">Только резкость, цвета не меняются.</string> + <string name="look_filmic">Пленочный</string> + <string name="look_filmic_desc">Кривая как у кинопленки, слегка приглушенная.</string> + <string name="look_arcade">Аркада</string> + <string name="look_arcade_desc">Броско и сочно.</string> + <string name="look_retro_crt">Ретро-CRT</string> + <string name="look_retro_crt_desc">Эффекты кинескопа CRT и аналогового сигнала вместе.</string> + <string name="look_upscale_sharp">Четкое увеличение</string> + <string name="look_upscale_sharp_desc">Для работы ниже разрешения экрана.</string> + <string name="look_pixel_clean">Чистые пиксели</string> + <string name="look_pixel_clean_desc">Четкие пиксели для 2D и старых игр.</string> + <string name="look_anime_edge">Аниме-контуры</string> + <string name="look_anime_edge_desc">Мультяшные контуры плюс резкость.</string> + + <string name="gpuinfo_not_adreno">GPU не Adreno</string> + <string name="gpuinfo_supported">Поддерживается</string> + <string name="gpuinfo_experimental">Экспериментально: его драйверы — тестовые сборки</string> + <string name="gpuinfo_below_tested">Ниже проверенного оборудования (Adreno 725 и новее): может не работать</string> + <string name="gpuinfo_unsupported">Не поддерживается: DroidDeck требует GPU Adreno (Snapdragon)</string> + + <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> + <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> + <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 — это Turnip «-Linux», его место в разделе «Драйвер среды выполнения Linux»</string> + <string name="lvd_no_library">В этом zip-архиве нет libvulkan_freedreno*.so. Zip-архив Turnip для Android (AdrenoTools) или -Wayland не является драйвером среды выполнения Linux.</string> + <string name="lvd_not_aarch64">%1$s не является 64-битной общей библиотекой AArch64 ELF.</string> + <string name="lvd_not_glibc">%1$s не является драйвером glibc — он связан с libc Android. Среде выполнения Linux нужен zip-архив «-Linux»; обычный Turnip или Turnip «-Wayland» клиент Steam вообще не может загрузить.</string> + <string name="bundle_no_manifest">Не пакет драйверов: нет manifest.json с указанием драйверов для Android и Linux</string> + <string name="bundle_entry_outside">Не пакет драйверов: %1$s указывает за его пределы</string> + <string name="bundle_too_large">Не пакет драйверов: после распаковки занимает больше %1$d МБ</string> + + <string name="gstore_sd_card">SD-карта</string> + <string name="gstore_folder">Папка</string> + + <string name="logshare_chooser">Поделиться журналами сессии</string> + + <string name="hdr_needs_android_10">требуется Android 10 или новее (слои дисплея с цветовым пространством)</string> + <string name="hdr_none">этот дисплей не сообщает о поддержке HDR</string> + <string name="hdr_not_hdr10">этот дисплей поддерживает %1$s, но не HDR10</string> + + <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> + <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 нет такого переключателя. Измените настройку с этого устройства через отладку по Wi-Fi.</string> + <string name="upd_block_no_apk">В этой сборке нет загрузки для этой копии DroidDeck.</string> + <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> + <string name="upd_fail_with_message">Обновление не установлено: %1$s</string> + <string name="upd_fail_generic">Обновление не установлено.</string> + <string name="saves_layout_zip">Архив %1$s</string> + <string name="saves_zip">zip-архив</string> + <string name="saves_not_launched">Игра %1$s еще не запускалась</string> + <string name="saves_none_found">Файлы сохранений для %1$s не найдены</string> + <string name="saves_not_save_zip">В %1$s нет папки drive_c, поэтому это не архив сохранений GameHub или Winlator</string> + <string name="gstore_cannot_create">невозможно создать папки в %1$s</string> + <string name="gstore_cannot_write">невозможно записать в %1$s</string> + <string name="gstore_cannot_write_detail">невозможно записать в %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">Папка недоступна</string> + <string name="gfiles_name_taken">Имя %1$s уже занято другим файлом</string> + <string name="gfiles_art_unavailable">Папка обложек недоступна</string> + <string name="gfiles_unrelated_inventory">В папке уже есть чужой список синхронизации</string> + <string name="gfiles_remove_failed">Не удалось удалить %1$s</string> + <string name="gfiles_outside_folder">Файл выходит за пределы папки экспорта</string> + <string name="cmgr_fex_unnamed">FEX (сборка без названия версии)</string> + <string name="cmgr_no_profile">нет profile.json в %1$s</string> + <string name="cmgr_not_component_package">%1$s не является пакетом компонента Linux (тип %2$s)</string> + <string name="gstore_volume_free">%1$s · свободно %2$s</string> + <string name="mode_storage_auto_short">Авто</string> + <string name="appupd_catalog_rejected">Каталог обновлений не прошел проверку (%1$s)</string> + <string name="main_adb_clip_label">Настройка дочерних процессов DroidDeck</string> + <string name="setup_signed_in_offline_unnamed">Вход выполнен · автономный режим</string> + <string name="setup_signed_in_unnamed">Вход выполнен</string> + <string name="driver_tag_bundled">ВСТРОЕННЫЙ</string> + <string name="driver_tag_downloaded">СКАЧАННЫЙ</string> + <string name="driver_tag_imported">ИМПОРТИРОВАННЫЙ</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> <string name="display_session">Экран сессии</string> <string name="display_resolution">Разрешение</string> <string name="display_game_fit">Игры 16:9</string> @@ -1030,6 +1904,7 @@ <string name="display_frame_rate">Частота кадров</string> <string name="display_look_filter">%1$s Устанавливает масштабирование: %2$s.</string> <string name="display_match_screen">По размеру экрана (%1$d×%2$d)</string> + <string name="display_follow_screen">По экрану (меняется при складывании и раскладывании)</string> <string name="mode_tab_steam">Steam</string> <string name="device_info_show">Показать сведения об устройстве</string> <string name="device_info_hide">Скрыть сведения об устройстве</string> @@ -1037,6 +1912,188 @@ <string name="device_info_copy">Копировать</string> <string name="device_info_copy_hint">Скопируйте эти сведения в отчет об ошибке.</string> <string name="device_info_copied">Скопировано.</string> + <string name="force_ssbs_title">Принудительный SSBS</string> + <string name="force_ssbs_hint">Удерживает включенной настройку скорости CPU, которую Wine постоянно отключает. Может ускорить игры Windows на Snapdragon 8 Elite; на старых чипах разница почти не заметна. Вступит в силу при следующем запуске игры.</string> + + <string name="rail_stores">Магазины</string> + <string name="source_steam">Steam</string> + <string name="source_added">Свои</string> + <string name="setup_stores">Магазины</string> + <string name="setup_stores_open_on">Открывать магазин на</string> + <string name="setup_stores_speed">Уровень скорости загрузки</string> + <string name="setup_stores_engine">Движок магазинов</string> + <string name="setup_stores_engine_chip_ready">● Движок готов</string> + <string name="setup_stores_engine_chip_missing">Не собран</string> + <string name="stores_tier_balanced">Сбалансированный</string> + <string name="stores_tier_fast">Быстрый</string> + <string name="stores_tier_max">Максимальный</string> + <string name="stores_title">Магазины</string> + <string name="stores_settings">Настройки магазинов</string> + <string name="stores_settings_eyebrow">Настройки</string> + <string name="stores_chip_signed_out">(вход не выполнен)</string> + <string name="stores_tab_store">Магазин</string> + <string name="stores_tab_installed">Установлено</string> + <string name="stores_tab_library">Библиотека</string> + <string name="stores_tab_all">Все</string> + <string name="stores_tab_downloads">Загрузки</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Войти</string> + <string name="stores_signout_named">%1$s · выйти</string> + <string name="stores_signout_title">Выйти из %1$s?</string> + <string name="stores_signout_note">Установленные игры останутся. Библиотека загрузится снова, когда вы войдете.</string> + <string name="stores_signout_stay">Не выходить</string> + <string name="stores_signout_go">Выйти</string> + <string name="stores_search_catalog">Поиск в каталоге %1$s…</string> + <string name="stores_search_installed">Поиск установленных игр…</string> + <string name="stores_search_library">Поиск в библиотеке…</string> + <string name="stores_search_open">Поиск</string> + <string name="stores_installed_empty">Ничего не установлено</string> + <string name="stores_library_loading">Загрузка</string> + <string name="stores_nothing_matches">Нет игр</string> + <string name="stores_shelves_loading">Загрузка магазина…</string> + <string name="stores_amazon_no_catalog">Нет каталога</string> + <string name="stores_shelf_new">Новинки</string> + <string name="stores_shelf_deals">Скидки</string> + <string name="stores_shelf_free">Бесплатные игры</string> + <string name="stores_shelf_trending">Популярное</string> + <string name="stores_shelf_library">Ваша библиотека</string> + <string name="stores_play">Играть</string> + <string name="stores_install">Установить</string> + <string name="stores_resume_install">Продолжить установку</string> + <string name="stores_dl_interrupted">Прервано</string> + <string name="stores_install_size">Установить · %1$s</string> + <string name="stores_get_free">Получить бесплатно</string> + <string name="stores_view_on">Открыть в %1$s</string> + <string name="stores_buy_price">Купить · %1$s</string> + <string name="stores_installed_chip">Установлено</string> + <string name="stores_in_library">В библиотеке</string> + <string name="stores_free">Бесплатно</string> + <string name="stores_resume">Продолжить</string> + <string name="stores_refresh">Обновить</string> + <string name="stores_show_mature">Показывать контент для взрослых</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Офлайн</string> + <string name="epic_card_none">Выкл.</string> + <string name="epic_eos">Вход в Epic (EOS)</string> + <string name="epic_offline">Запускать офлайн</string> + <string name="epic_resolve">Устранить проблему входа в Epic</string> + <string name="store_cloud_saves">Облачные сохранения</string> + <string name="cloud_upload">Загрузить в облако</string> + <string name="cloud_download">Скачать из облака</string> + <string name="cloud_none">Нет облачных сохранений</string> + <string name="cloud_conflict">Конфликт с облаком</string> + <string name="cloud_conflict_files">Конфликт с облаком · файлов: %1$d</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Оставить облачные</string> + <string name="cloud_keep_local">Оставить локальные</string> + <string name="cloud_checking">Проверка облака…</string> + <string name="cloud_last_sync">Последняя синхронизация: %1$s</string> + <string name="cloud_never">Еще не синхронизировано</string> + <string name="cloud_done">Файлов: %1$d</string> + <string name="stores_eyebrow_installed">установлено</string> + <string name="stores_eyebrow_library">в вашей библиотеке</string> + <string name="stores_eyebrow_store">магазин</string> + <string name="achievements_title">Достижения</string> + <string name="achievements_count">Получено %1$d из %2$d</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Получены · %1$d</string> + <string name="achievements_group_locked">Не получены · %1$d</string> + <string name="achievements_group_hidden">Скрытые · %1$d</string> + <string name="achievements_hidden">Скрытое достижение</string> + <string name="achievements_hidden_detail">Подробности появятся после получения.</string> + <string name="achievements_all">Все достижения получены</string> + <string name="achievements_reading">Загрузка…</string> + <string name="achievements_signed_out">Войдите в %1$s, чтобы увидеть свои достижения.</string> + <string name="achievements_unreachable">Не удалось связаться с %1$s. Проверьте снова, когда будете в сети.</string> + <string name="achievements_none">В %1$s нет достижений для этой игры.</string> + <string name="achievements_show_all">Показать все (%1$d)</string> + <string name="achievements_dialog_title">Достижения · %1$s</string> + <string name="stores_in_steam">✓ В Steam</string> + <string name="stores_uninstall">Удалить</string> + <string name="stores_uninstalling">Удаление…</string> + <string name="stores_uninstall_title">Удалить %1$s?</string> + <string name="stores_uninstall_body_size">Освободит %1$s и уберет игру из Steam.</string> + <string name="stores_uninstall_body">Удаляет файлы игры и ее ярлык в Steam.</string> + <string name="stores_launch_not_registered">Steam подхватит игру при следующем запуске; запустите ее оттуда или запустите Steam сейчас.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Выбор Steam для каждой игры</string> + <string name="stores_card_audio">Аудио</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Компоненты</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Движок</string> + <string name="stores_card_engine_value">%1$s · нативный</string> + <string name="stores_card_engine_java">%1$s · встроенный</string> + <string name="stores_how_it_runs">Как запускается</string> + <string name="stores_kv_fetched">Получено из</string> + <string name="stores_kv_fetched_value">%1$s, с вашим аккаунтом</string> + <string name="stores_kv_installed_to">Установлено в</string> + <string name="stores_kv_installed_to_value">Хранилище игр (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Запускается через</string> + <string name="stores_kv_launched_by_value">Steam и Proton, так же как игры Steam</string> + <string name="stores_kv_launched_by_epic">Steam и Proton · вход в Epic при каждом запуске, оверлей EOS отключен</string> + <string name="stores_kv_launched_by_amazon">Steam и Proton · заданы переменные Amazon Games Services</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">добавляется в Steam при установке (интерфейс Deck + Steam Input)</string> + <string name="stores_downloads_empty">Нет загрузок</string> + <string name="stores_dl_paused">Приостановлено</string> + <string name="stores_dl_queued">В очереди</string> + <string name="stores_dl_queued_at">В очереди · №%1$d</string> + <string name="stores_dl_cancelled">Отменено</string> + <string name="stores_dl_failed">Ошибка</string> + <string name="stores_dl_failed_reason">Ошибка · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Пауза</string> + <string name="stores_dl_resume">Продолжить</string> + <string name="stores_dl_cancel">Отмена</string> + <string name="stores_dl_retry">Повторить</string> + <string name="stores_dl_clear">Убрать</string> + <string name="stores_dl_clear_all">Убрать завершенные</string> + <string name="stores_stage_manifest">Манифест</string> + <string name="stores_stage_download">Загрузка</string> + <string name="stores_stage_verify">Проверка</string> + <string name="stores_stage_install">Установка</string> + <string name="stores_stage_done">Готово</string> + <string name="stores_stage_checking">Проверка</string> + <string name="stores_stage_downloading">Загрузка</string> + <string name="stores_dl_cancel_confirm">Удалить загрузку?</string> + <string name="stores_dl_failed_kept">Ошибка · %1$s сохранено</string> + <string name="stores_stage_verifying">Проверка</string> + <string name="stores_stage_installing">Установка</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">Файлы: %1$d/%2$d</string> + <string name="stores_download_manager">Менеджер загрузок</string> + <string name="stores_parallel">Одновременных загрузок</string> + <string name="stores_install_where">Установить в</string> + <string name="stores_install_title">Установка %1$s:</string> + <string name="stores_target_internal">Внутреннее хранилище</string> + <string name="stores_target_free">Свободно: %1$s</string> + <string name="stores_notification_channel">Загрузки магазинов</string> + <string name="stores_notification_channel_hint">Показывает ход загрузки игр и продолжает загрузки в фоне</string> + <string name="stores_notification_one">Загрузка</string> + <string name="stores_notification_many">Загрузок: %1$d</string> + <string name="stores_notification_waiting">Подготовка…</string> + <string name="stores_notification_done_channel">Завершенные загрузки магазинов</string> + <string name="stores_notification_installed">Установлено: %1$s</string> + <string name="stores_notification_failed">Ошибка загрузки %1$s</string> + <string name="stores_notification_delete">Удалить</string> + <string name="stores_notification_keep">Оставить</string> + <string name="stores_login_title">Вход в %1$s</string> + <string name="stores_login_provider_title">Вход через сторонний аккаунт</string> + <string name="stores_login_reload">Перезагрузить</string> + <string name="stores_login_close">Закрыть</string> + <string name="stores_login_retry">Повторить</string> + <string name="stores_login_finishing">Завершение входа…</string> + <string name="stores_login_error_network">Не удалось загрузить страницу входа: %1$s</string> + <string name="stores_login_error_idp_blocked">Этот сервис не разрешает вход со встроенной страницы. Войдите с паролем от магазина.</string> + <string name="stores_login_error_http">Страница входа вернула ошибку (HTTP %1$d).</string> + <string name="stores_login_error_ssl">Подключение к странице входа не защищено.</string> + <string name="stores_login_error_verification">Не удалось подтвердить вход. Повторите попытку.</string> + <string name="stores_login_error_generic">Ошибка входа. Повторите попытку.</string> <string name="common_close">Закрыть</string> <string name="common_back">Назад</string> <string name="common_edit">Изменить</string> @@ -1074,21 +2131,128 @@ <item quantity="many">%1$d уже в «Играх»</item> <item quantity="other">%1$d уже в «Играх»</item> </plurals> - <string name="achievements_title">Достижения</string> - <string name="achievements_count">Получено %1$d из %2$d</string> - <string name="achievements_count_short">%1$d / %2$d</string> - <string name="achievements_group_unlocked">Получены · %1$d</string> - <string name="achievements_group_locked">Не получены · %1$d</string> - <string name="achievements_group_hidden">Скрытые · %1$d</string> - <string name="achievements_hidden">Скрытое достижение</string> - <string name="achievements_hidden_detail">Подробности появятся после получения.</string> - <string name="achievements_all">Все достижения получены</string> - <string name="achievements_reading">Загрузка…</string> - <string name="achievements_signed_out">Войдите в %1$s, чтобы увидеть свои достижения.</string> - <string name="achievements_unreachable">Не удалось связаться с %1$s. Проверьте снова, когда будете в сети.</string> - <string name="achievements_none">В %1$s нет достижений для этой игры.</string> - <string name="achievements_show_all">Показать все (%1$d)</string> - <string name="achievements_dialog_title">Достижения · %1$s</string> + <string name="hop_back">B · Назад к %1$s</string> + <string name="hop_from_games">игре</string> + <string name="hop_from_play">настройкам запуска</string> + <string name="sheet_graphics_drivers">Графические драйверы</string> + <string name="comp_this_proton">Этот Proton</string> + <string name="comp_every_proton">Все Proton</string> + <string name="comp_for_every_proton">Для всех Proton</string> + <string name="comp_available_count">Доступно · %1$d</string> + <string name="comp_checked">Проверено %1$s</string> + <string name="comp_check_all">Проверить новые компоненты и драйверы</string> + <string name="comp_get">Получить</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton и FEX ниже относятся к этому Proton</string> + <string name="comp_device_tested">%1$s · проверено</string> + <string name="comp_device_untested">%1$s · не проверено</string> + <string name="comp_device_unsupported">%1$s · не поддерживается</string> + <string name="gpu_auto_keeps">Поддерживает рекомендуемую пару актуальной</string> + <string name="gpu_bar_auto">Авто · %1$s</string> + <string name="gpu_separately">Драйверы среды выполнения и дисплея по отдельности</string> + <string name="gpu_use_title">Использовать %1$s?</string> + <string name="proton_remove_title">Удалить %1$s?</string> + <string name="setup_tab_diagnostics">Диагностика</string> + <string name="setup_ready">Готово к игре</string> + <string name="setup_not_ready">Пока не готово к игре</string> + <string name="setup_ready_runtime">среда выполнения %1$s</string> + <string name="setup_ready_limit_off">ограничение процессов отключено</string> + <string name="setup_ready_signed_in">вход как %1$s</string> + <string name="setup_needs_you">Когда требуется ваше участие</string> + <string name="setup_group_look">Внешний вид</string> + <string name="setup_group_device">Это устройство</string> + <string name="setup_group_art">Обложки библиотеки</string> + <string name="flathub_eyebrow">ПРИЛОЖЕНИЯ LINUX</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Приложения для рабочего стола Linux, запускаются на рабочем столе.</string> + <string name="flathub_setup">Настроить Flathub</string> + <string name="flathub_remove_title">Удалить %1$s?</string> + <string name="flathub_remove_note">Его настройки и файлы сохранятся.</string> + <string name="flathub_keep">Оставить</string> + <string name="desktop_size">Размер на диске</string> + <string name="desktop_uninstall_title">Удалить рабочий стол?</string> + <string name="desktop_all_systems">Все</string> + <string name="gog_achievements">Достижения GOG</string> + <string name="gog_achievements_hint">Синхронизирует достижения для поддерживаемых игр GOG. Применится при следующем запуске.</string> + <string name="gog_achievements_off">Достижения отключены</string> + <string name="gog_achievements_on">Достижения включены</string> + <string name="gog_achievements_signin">Войдите в GOG снова</string> + <string name="gog_achievements_uploaded">Достижения отправлены</string> + <string name="gog_achievements_pending">Отправка достижения в ожидании</string> + <string name="gog_achievements_enable_store">Включите достижения GOG в настройках магазинов</string> + <string name="gog_achievements_unavailable">Достижения недоступны; проверьте журналы сессии</string> + <string name="achievement_unlocked">Достижение открыто</string> + <string name="touch_paste">Вставить</string> + <string name="touch_dpad">Крестовина</string> + <string name="touch_left_stick">Левый стик</string> + <string name="touch_right_stick">Правый стик</string> + <string name="touch_menu">Меню</string> + <string name="touch_keyboard">Клавиатура</string> + <string name="touch_dpad_up">Крестовина вверх</string> + <string name="touch_dpad_down">Крестовина вниз</string> + <string name="touch_dpad_left">Крестовина влево</string> + <string name="touch_dpad_right">Крестовина вправо</string> + <string name="touch_title">Сенсорное управление</string> + <string name="touch_controller">Сенсорный контроллер</string> + <string name="touch_mouse">Мышь</string> + <string name="touch_on">Вкл.</string> + <string name="touch_off">Выкл.</string> + <string name="touch_speed_slow">Медленно</string> + <string name="touch_speed_normal">Обычно</string> + <string name="touch_speed_fast">Быстро</string> + <string name="touch_edit_layout">Изменить раскладку</string> + <string name="touch_droiddeck_menu">Меню DroidDeck</string> + <string name="touch_drag_hint">Перетащите элементы управления на экран</string> + <string name="touch_arrange">Расположить</string> + <string name="touch_discard">Отменить</string> + <string name="touch_saving">Сохранение…</string> + <string name="touch_icon">Значок</string> + <string name="touch_narrower">Ш−</string> + <string name="touch_wider">Ш+</string> + <string name="touch_shorter">В−</string> + <string name="touch_taller">В+</string> + <string name="touch_hide">Скрыть</string> + <string name="touch_color_opacity">Цвет и непрозрачность…</string> + <string name="touch_copy_layout">Скопировать эту раскладку</string> + <string name="touch_layout_copied">Раскладка скопирована; вставьте её в сенсорную раскладку любой игры</string> + <string name="touch_paste_layout">Вставить скопированную раскладку</string> + <string name="touch_reset_layout">Восстановить раскладку Steam</string> + <string name="touch_no_binding">У этого элемента нет привязки в Steam, чтобы назначить значок</string> + <string name="touch_icon_for">Значок для %1$s</string> + <string name="touch_layout_color">Цвет сенсорной раскладки</string> + <string name="touch_done">Готово</string> + <string name="touch_save_failed">Не удалось сохранить сенсорную раскладку</string> + <string name="touch_saved">Сенсорная раскладка сохранена в Steam</string> + <string name="touch_app_id">Приложение %1$d</string> + <string name="touch_label_hint">Подпись (необязательно)</string> + <string name="touch_icons_missing">Значки Steam не найдены</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d значок из Steam</item> + <item quantity="few">%1$d значка из Steam</item> + <item quantity="many">%1$d значков из Steam</item> + <item quantity="other">%1$d значка из Steam</item> + </plurals> + <string name="touch_icon_color">Цвет значка</string> + <string name="touch_button_color">Цвет кнопки</string> + <string name="touch_no_icon">Без значка</string> + + <string name="stores_sync_sign_in">Проверка входа…</string> + <string name="stores_sync_library">Получение вашей библиотеки…</string> + <string name="stores_sync_game_list">Получение списка игр…</string> + <string name="stores_sync_details">Загрузка сведений об играх… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">Синхронизация %1$d игры…</item> + <item quantity="few">Синхронизация %1$d игр…</item> + <item quantity="many">Синхронизация %1$d игр…</item> + <item quantity="other">Синхронизация %1$d игры…</item> + </plurals> + <string name="stores_sync_progress">Синхронизация… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Получение обложек…</string> + <string name="stores_sync_failed">Синхронизация не удалась</string> + <string name="stores_sync_no_games">В вашей библиотеке %1$s игр не найдено</string> + <string name="stores_sync_amazon_no_answer">Amazon Games не ответил насчёт вашей библиотеки: обновите или войдите снова</string> + <string name="stores_sync_gog_unreachable">Не удалось связаться с GOG</string> + <string name="stores_sync_gog_unreadable">Не удалось прочитать вашу библиотеку GOG</string> + <string name="stores_registering">Регистрация в Steam…</string> <string name="drawer_page_glasses">Очки</string> <string name="glasses_open">Очки VITURE</string> <string name="glasses_control">Управление очками</string> @@ -1137,4 +2301,34 @@ <string name="stores_hltb_completionist">Полностью</string> <string name="stores_hltb_hours">%1$s ч</string> <string name="stores_hltb_minutes">%1$d мин</string> -</resources> \ No newline at end of file + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Режим Cardboard</string> + <string name="cardboard_mode_hint">Изображения для левого и правого глаза для VR-очков под телефон.</string> + <string name="cardboard_source">Изображение</string> + <string name="cardboard_source_screen">Виртуальный экран</string> + <string name="cardboard_source_sbs">Стереопара 3D</string> + <string name="cardboard_viewer">Очки</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Без линз</string> + <string name="cardboard_ipd">Расстояние между глазами</string> + <string name="cardboard_screen_width">Ширина экрана</string> + <string name="cardboard_screen_distance">Расстояние до экрана</string> + <string name="cardboard_head_tracking">Отслеживание головы</string> + <string name="cardboard_no_sensor">На этом устройстве нет датчика поворота.</string> + <string name="cardboard_recenter">Отцентрировать вид</string> + <string name="cardboard_recenter_button">Отцентрировать</string> + <string name="cardboard_steamvr">SteamVR (экспериментально)</string> + <string name="cardboard_steamvr_install">Установить</string> + <string name="cardboard_steamvr_install_hint">Скачивает SteamVR от Valve для ARM64 (571 МБ).</string> + <string name="cardboard_steamvr_installing">Скачивание и распаковка SteamVR…</string> + <string name="cardboard_steamvr_start">Запустить</string> + <string name="cardboard_steamvr_ready_hint">Запускает SteamVR, где гарнитурой служит ваша голова; изображение переключается на стереопару 3D.</string> + <string name="cardboard_steamvr_stop">Остановить</string> + <string name="cardboard_steamvr_running_hint">SteamVR запущен.</string> + <string name="cardboard_steamvr_unavailable">Недоступно в этой сессии.</string> + <string name="cardboard_steamvr_failed">SteamVR не отвечает; проверьте журнал сессии.</string> + <string name="drawer_component_downloads">Скачать компоненты</string> + <string name="drawer_component_downloads_hint">Загрузки общие со страницей «Компоненты». Выберите выше скачанную версию, чтобы использовать её при следующем запуске игры.</string> + <string name="drawer_component_choose_download">Выберите версию для скачивания</string> +</resources> diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml new file mode 100644 index 000000000..76c1eb376 --- /dev/null +++ b/app/src/main/res/values-sv/strings.xml @@ -0,0 +1,2235 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>Första starten laddar ned Steam. Det kan ta några minuter.</item> + <item>Som standard pausas Steam när du lämnar appen. Det fortsätter där det slutade när du kommer tillbaka.</item> + <item>Din handkontroll visas som en Xbox 360-kontroll. Annars kan du använda skärmkontrollerna.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>Första starten konfigurerar skrivbordet. Det kan ta några minuter.</item> + <item>Som standard pausas sessionen när du lämnar appen. Den fortsätter där den slutade när du kommer tillbaka.</item> + <item>Pekskärmen fungerar som en pekplatta: dra för att flytta pekaren, tryck för att klicka.</item> + <item>Din handkontroll visas som en Xbox 360-kontroll. Annars kan du använda skärmkontrollerna.</item> + </string-array> + <string name="session_channel">Session</string> + <string name="session_channel_description">Visas medan en Steam- eller skrivbordssession är öppen</string> + <string name="session_notification">Steam körs</string> + <string name="session_notification_downloads">Steam-nedladdning pågår</string> + <string name="session_notification_paused">Sessionen är pausad - tryck på Återuppta när du är redo</string> + <string name="resume_session">Återuppta</string> + <string name="suspend_now">Pausa nu</string> + <string name="stop_session">Stoppa sessionen</string> + <string name="frame_gen_title">Bildrutegenerering</string> + <string name="frame_gen_hint">Välj läge för bildrutegenerering</string> + <string name="frame_gen_off">Av</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG adaptiv %1$d</string> + <string name="lsfg_needs_steam">Kräver Lossless Scaling på Steam</string> + <string name="lsfg_needs_install">Installera Lossless Scaling i Steam eller importera Lossless.dll</string> + <string name="lsfg_import">Importera Lossless.dll…</string> + <string name="lsfg_using_steam">Använder kopian från Steam</string> + <string name="lsfg_using_import">Använder en importerad kopia</string> + <string name="lsfg_pick_title">Välj Lossless.dll</string> + <string name="lsfg_imported">Lossless.dll har importerats</string> + <string name="lsfg_import_unchanged">Den här Lossless.dll används redan</string> + <string name="lsfg_import_not_owned">Lossless Scaling ägs inte på det här Steam-kontot</string> + <string name="lsfg_import_invalid">Filen är inte Lossless.dll från Lossless Scaling</string> + <string name="lsfg_import_failed">Kunde inte förbereda LSFG från den här Lossless.dll</string> + <string name="lsfg_problem_missing">LSFG kräver Lossless Scaling från Steam eller en importerad Lossless.dll</string> + <string name="lsfg_problem_failed">LSFG kunde inte förbereda sina shaders från Lossless.dll</string> + <string name="hud_fg_unsupported">%1$s: stöds inte</string> + <string name="hud_fg_failed">%1$s: motorn misslyckades</string> + <string name="hud_fg_needs_lossless">%1$s: kräver Lossless Scaling</string> + <string name="hud_fg_starting">%1$s %2$s · startar</string> + <string name="second_library_import_hint">Lägger till ett Steam-bibliotek och söker igenom det efter Windows-spel. Spel du äger använder sina Steam-poster; resten blir icke-Steam-genvägar.</string> + <string name="game_env_title">Spelmiljö</string> + <string name="game_env_hint">Miljövariabler för Proton-spel.</string> + <string name="game_env_edit">Redigera</string> + <string name="game_env_shared">Alla Proton-spel</string> + <string name="game_env_scope">Profil</string> + <string name="game_env_app_id">Steam-app-ID</string> + <string name="game_env_open">Öppna</string> + <string name="game_env_profile">Spel %1$s</string> + <string name="game_env_add">Lägg till variabel</string> + <string name="game_env_name">Variabelnamn</string> + <string name="game_env_value">Värde</string> + <string name="game_env_save">Spara</string> + <string name="game_env_remove">Ta bort</string> + <string name="game_env_restore">Ärv igen</string> + <string name="game_env_reset">Rensa miljöåsidosättningar</string> + <string name="game_env_unset">Ta bort värdet i den här profilen</string> + <string name="game_env_empty">Inga variabler angivna.</string> + <string name="game_env_inherited">Ärvd</string> + <string name="game_env_custom">Anpassad variabel</string> + <string name="game_env_error">Kunde inte läsa in eller spara. Kontrollera ledigt lagringsutrymme och försök igen.</string> + <string name="game_env_invalid">Använd ett giltigt namn och ett värde på högst 8192 tecken.</string> + <string name="game_env_unused">Den vanliga Proton-körmiljön läser inte den här variabeln.</string> + <string name="game_env_loading">Läser in…</string> + <string name="game_env_feature">Åsidosätter D3D12-funktionsnivån. 12_2 rapporterar DirectX 12 Ultimate-funktioner; funktioner som inte stöds kan misslyckas. Lämna tomt för automatisk identifiering.</string> + <string name="game_env_shader">Åsidosätter D3D12-shadermodellen. 6_9 kräver en tillräckligt ny VKD3D och matchande GPU-funktioner. Lämna tomt för automatisk identifiering.</string> + <string name="game_env_cache">Behåll Mesas shadercache aktiverad för att minska upprepad kompilering. false aktiverar cachning.</string> + <string name="game_env_cache_size">Största storlek på Mesas shadercache, t.ex. 1G. Större cache använder mer lagringsutrymme.</string> + <string name="game_env_vkd3d_config">VKD3D-alternativ för D3D12. nodxr inaktiverar strålspårning; använd bara för ett spel som behöver det.</string> + <string name="game_env_proton_log">Ange 1 för en diagnostiklogg från Proton. Loggning kan sänka prestandan och tar lagringsutrymme.</string> + <string name="game_env_wined3d">Ange 1 för att använda OpenGL-baserade WineD3D i stället för DXVK vid kompatibilitetstester för D3D9–11.</string> + <string name="game_env_no_ntsync">Ange 1 för att hålla spelet borta från ntsync (droiddeck-ntsync eller en kärndrivrutin); det använder droiddeck-esync där ett paket finns, annars droiddeck-fsync.</string> + <string name="game_env_no_esync">Ange 1 för att stänga av droiddeck-esync för det här spelet; det använder droiddeck-fsync i stället, eller droiddeck-ntsync där även fsync är av.</string> + <string name="game_env_no_fsync">Ange 1 för att hålla spelet borta från droiddeck-fsync; det använder droiddeck-esync där ett paket finns, annars droiddeck-ntsync.</string> + <string name="game_env_fsync_first">Ange 1 för att köra spelet på droiddeck-fsync, eller 0 för att prioritera droiddeck-esync där ett paket passar dess Proton.</string> + <string name="game_env_xalia">Ange 0 för att inaktivera Protons hjälp för handkontrollsnavigering i det här spelet.</string> + <string name="game_env_hud">DXVK-överlägg för D3D9–11. fps visar bildfrekvensen; utan värde döljs det.</string> + <string name="game_env_dxvk_config">DXVK-inställningar avgränsade med semikolon. Nyare versioner använder dxvk.maxFrameRate för bildbegränsning; använd hellre sessionens bildbegränsare vid vanligt spelande.</string> + <string name="game_env_vkd3d_limit">Gräns för bildfrekvens i D3D12. 0 inaktiverar gränsen.</string> + <string name="game_env_glthread">Trådning för Mesa OpenGL. true kan hjälpa CPU-begränsade OpenGL-spel; testa för varje spel.</string> + <string name="sync_backend_title">Wine-synk</string> + <string name="sync_backend_hint">Hur spel håller sina trådar i takt. Gäller från nästa session.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">FEX-förinställning</string> + <string name="fex_next_launch">Gäller nästa gång ett spel startas.</string> + <string name="game_settings_title">Spel</string> + <string name="fex_default_label">FEX-standard</string> + <string name="fex_default_detail">Använder FEX:s standardinställningar.</string> + <string name="fex_stability_label">Stabilitet</string> + <string name="fex_stability_detail">Säkraste och långsammaste inställningarna.</string> + <string name="fex_compatibility_label">Kompatibilitet</string> + <string name="fex_compatibility_detail">Full TSO med multiblock.</string> + <string name="fex_intermediate_label">Mellannivå</string> + <string name="fex_intermediate_detail">Balanserad TSO med sänkt x87-precision.</string> + <string name="fex_performance_label">Prestanda</string> + <string name="fex_performance_detail">All TSO inaktiverad; multiblock.</string> + <string name="fex_performance_tso_label">Prestanda + TSO</string> + <string name="fex_performance_tso_detail">Prestandainställningar med huvud-TSO aktiverad.</string> + <string name="fex_extreme_label">Extrem</string> + <string name="fex_extreme_detail">Inaktiverar kontroller av självmodifierande kod. JIT-spel och DRM kan sluta fungera.</string> + <string name="fex_extreme_tso_label">Extrem + TSO</string> + <string name="fex_extreme_tso_detail">Extrema inställningar med huvud-TSO aktiverad.</string> + <string name="fex_extreme_gn_label">Extrem-gn</string> + <string name="fex_extreme_gn_detail">Prestanda med TSC- och metadatatips. Prova före Extrem.</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">Prestanda med fullständiga kodkontroller och dold hypervisor-bit.</string> + <string name="game_env_custom_value">Anpassat värde</string> + <string name="game_env_select_value">Välj värde</string> + <string name="user_apps_section">Emulatorer & appar</string> + <string name="user_apps_add">Lägg till</string> + <string name="user_apps_add_detail">Din egen app eller ditt eget spel</string> + <string name="user_apps_kind_script">Skript</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d %%</string> + <string name="user_apps_starting">Startar…</string> + <string name="user_apps_added">%1$s har lagts till</string> + <string name="user_apps_removed">%1$s har tagits bort</string> + <string name="user_apps_back">Skrivbord</string> + <string name="user_apps_gone">Appen finns inte längre. Välj den igen från skrivbordet.</string> + <string name="user_apps_open">Öppna %1$s</string> + <string name="user_apps_remove">Ta bort</string> + <string name="user_apps_remove_confirm">Tryck igen för att ta bort</string> + <string name="user_apps_stop_session">Stoppa sessionen för att ta bort</string> + <string name="user_apps_script_linked">Kör %1$s från sin mapp, som länkas in i sessionen där den ligger. Om du tar bort appen lämnas mappen orörd.</string> + <string name="user_apps_script_copied">Kör %1$s från en kopia av mappen i DroidDeck. Om du tar bort appen raderas kopian, inte originalet.</string> + <string name="user_apps_runtime_required">Installera Linux-körmiljön först</string> + <string name="user_apps_file_gone">Filen finns inte längre</string> + <string name="user_apps_no_space">Inte tillräckligt med ledigt utrymme: att kopiera mappen kräver %1$s</string> + <string name="user_apps_copying">Kopierar %1$s</string> + <string name="user_apps_failed">Det gick inte att lägga till appen</string> + <string name="user_apps_github_checking">Söker efter en utgåva av %1$s</string> + <string name="user_apps_github_unreachable">Kunde inte läsa utgåvorna av %1$s</string> + <string name="user_apps_github_no_appimage">Ingen utgåva av %1$s har en AppImage för ARM64 eller x86_64</string> + <string name="user_apps_downloading">Laddar ned %1$s</string> + <string name="user_apps_download_failed">Nedladdningen misslyckades</string> + <string name="user_apps_update_failed">Det gick inte att uppdatera appen</string> + <string name="user_apps_icon_failed">Ikonen kunde inte läsas</string> + <string name="user_apps_saved">%1$s har sparats</string> + <string name="user_apps_edit">Redigera</string> + <string name="user_apps_check_updates">Sök efter uppdateringar</string> + <string name="user_apps_checking_updates">Söker efter uppdateringar…</string> + <string name="user_apps_update_to">Uppdatera till %1$s</string> + <string name="user_apps_up_to_date">Senaste versionen · %1$s</string> + <string name="user_apps_updated">%1$s har uppdaterats</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">Lägg till en app</string> + <string name="add_app_tab_script">Skript</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">En .sh-fil. Den körs från sin egen mapp, i helskärm under gamescope.</string> + <string name="add_app_appimage_hint">En ARM64-AppImage (aarch64), eller en x86_64-AppImage som körs via FEX. Den packas upp i Linux-körmiljön en gång.</string> + <string name="add_app_github_hint">Installerar ARM64-AppImagen från repots senaste utgåva, eller dess x86_64-variant via FEX.</string> + <string name="app_fex_title">Använd FEX</string> + <string name="app_fex_auto">Auto</string> + <string name="app_fex_on">På</string> + <string name="app_fex_off">Av</string> + <string name="app_fex_hint">FEX kör Linux-program för x86 på den här ARM64-enheten. Auto använder det när programmet är byggt för x86.</string> + <string name="app_fex_x86">%1$s via FEX</string> + <string name="app_fex_not_ready">Programmet är byggt för %1$s-datorer och körs via FEX, som inte har konfigurerats än. Öppna Steam en gång medan du är inloggad: DroidDeck låter Steam ladda ned FEX och den Steam Linux Runtime som behövs, sedan öppnas detta.</string> + <string name="session_end_program">Det här programmet</string> + <string name="session_end_fex_missing">%1$s är byggt för x86-datorer och körs via FEX, som inte är installerat än. Starta Steam en gång medan du är inloggad och låt det ladda ned FEX och Steam Linux Runtime. Starta sedan detta igen.</string> + <string name="add_app_flatpak_hint">Ett app-ID från Flathub eller appens Flathub-sida. Flatpak konfigureras först om det inte redan är gjort.</string> + <string name="add_app_file">Fil</string> + <string name="add_app_no_file">Ingen fil vald</string> + <string name="add_app_choose_file">Välj fil</string> + <string name="add_app_change_file">Ändra</string> + <string name="add_app_pick_script">Välj ett skript</string> + <string name="add_app_pick_appimage">Välj en AppImage</string> + <string name="add_app_pick_icon">Välj en ikon</string> + <string name="add_app_repository">Repo</string> + <string name="add_app_repository_placeholder">ägare/repo eller en github.com-länk</string> + <string name="add_app_repository_invalid">Inte ett GitHub-repo</string> + <string name="add_app_flatpak_id">App-ID</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">Inte ett app-ID från Flathub</string> + <string name="add_app_flatpak_bundle_hint">Eller en .flatpak-fil, för en app som inte finns på Flathub.</string> + <string name="add_app_pick_flatpak">Välj en .flatpak-fil</string> + <string name="add_app_source">Källa</string> + <string name="add_app_appearance">På ditt skrivbord</string> + <string name="add_app_preview_name">Ny app</string> + <string name="add_app_name">Namn</string> + <string name="add_app_name_placeholder">Från appen</string> + <string name="add_app_icon">Ikon</string> + <string name="add_app_icon_default">Standardikon</string> + <string name="add_app_choose_icon">Välj ikon</string> + <string name="add_app_clear_icon">Rensa</string> + <string name="add_app_icon_suggestions">Från repot</string> + <string name="add_app_icon_searching">Söker efter ikoner…</string> + <string name="add_app_icon_reset">Standard</string> + <string name="edit_app_title">Redigera %1$s</string> + <string name="edit_app_save">Spara</string> + <string name="add_app_checking_folder">Kontrollerar mappen…</string> + <string name="add_app_folder_linked">Mappen länkas in i sessionen: %1$s</string> + <string name="add_app_folder_copied">Mappen innehåller Linux-program, som inte kan köras från delad lagring, så den kopieras till DroidDeck (%1$s).</string> + <string name="add_app_confirm">Lägg till</string> + <string name="add_app_cancel">Avbryt</string> + <string name="add_app_runtime_required">Installera Linux-körmiljön från Konfiguration först.</string> + <string name="add_app_busy">Vänta tills %1$s är klart.</string> + <string name="steam_compat_label">Kompatibel</string> + <string name="proton_compatible_note">Versioner märkta ”%1$s” i Steams kompatibilitetsmeny har DroidDecks prefixkorrigeringar och startar spel här. Standard är Proton Experimental ARM64 (%1$s), eller Proton 11.0 ARM64 (%1$s) när den nyare versionen är installerad. Valves egna Proton-poster utan märkningen kan inte starta spel på den här enheten.</string> + <string name="proton_installed_compatible">Installerad %1$s · %2$s</string> + <string name="common_cancel">Avbryt</string> + <string name="common_auto">Auto</string> + <string name="common_never">Aldrig</string> + <string name="common_always">Alltid</string> + <string name="common_uninstall">Avinstallera</string> + <string name="common_check">Kontrollera</string> + <string name="common_update">Uppdatera</string> + <string name="common_checking">Kontrollerar…</string> + <string name="common_working">Arbetar…</string> + <string name="common_forget">Glöm</string> + <string name="common_add_ellipsis">Lägg till…</string> + <string name="common_use">Använd</string> + <string name="common_applies_next_session">Gäller från nästa session.</string> + <string name="mode_steam_title">Steam-session</string> + <string name="mode_desktop_title">Skrivbordssession</string> + <string name="mode_display">Skärm</string> + <string name="mode_resolution">Upplösning</string> + <string name="mode_res_720">Upp till 720p</string> + <string name="mode_res_900">Upp till 900p</string> + <string name="mode_res_1080">Upp till 1080p</string> + <string name="mode_res_native">Inbyggd</string> + <string name="mode_res_default">%1$s (standard)</string> + <string name="mode_res_custom_value">Anpassad · %1$d×%2$d</string> + <string name="mode_res_custom">Anpassad…</string> + <string name="mode_res_note">720p kan få menyer att reagera snabbare.</string> + <string name="mode_ratio">Bildförhållande</string> + <string name="mode_ratio_custom">Bestäms av den anpassade upplösningen.</string> + <string name="mode_ratio_auto">Auto använder minst 16:9.</string> + <string name="mode_fps">Bildgräns</string> + <string name="mode_fps_note">Begränsar hela sessionen. Skärmen byter till en uppdateringsfrekvens som är en jämn multipel av gränsen.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">HDR10-utgång</string> + <string name="mode_hdr_unavailable">Inte tillgängligt: %1$s.</string> + <string name="mode_hdr_restart">Starta om appen för att tillämpa.</string> + <string name="mode_drivers">Drivrutiner</string> + <string name="mode_gpu_drivers">GPU-drivrutiner</string> + <string name="mode_gpu_drivers_hint">Delas av Steam och skrivbordet. Ställs in på sidan Komponenter.</string> + <string name="mode_touch_controls">Pekskärm & kontroller</string> + <string name="mode_touch">Pekskärm</string> + <string name="mode_touch_touchpad">Pekplatta</string> + <string name="mode_touch_direct">Direkt</string> + <string name="mode_touch_note">Auto: pekplatta på skrivbordet, direkt i Steam.</string> + <string name="mode_osc">Skärmkontroller</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">Auto visar kontrollerna när ingen handkontroll är ansluten.</string> + <string name="osc_steam_touch">Steam-pekstyrning (beta)</string> + <string name="osc_style_next_session">Steams pekstyrning startar med nästa Steam-session</string> + <string name="osc_steam_layout">Steams peklayout</string> + <string name="osc_steam_layout_hint">Flytta, ändra storlek på eller dölj spelets pekkontroller; sparas i Steam</string> + <string name="mode_controller">Handkontroll</string> + <string name="mode_controller_hint">Hur Steam ser din handkontroll. Gäller från nästa session.</string> + <string name="mode_controller_deck">Steam Deck-kontroll</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Xbox 360-kontroll</string> + <string name="mode_controller_note">Deck: snabbåtkomstknappen och gyro. Steam Controller: samma, med greppkänsla på bakknapparna. DualShock 4 och DualSense: gyro, och styrplattorna som deras pekplatta. Xbox 360: öppna snabbåtkomst med Guide+A.</string> + <string name="mode_back">Back</string> + <string name="back_menu_then_qam">Meny → QAM</string> + <string name="back_qam_then_menu">QAM → Meny</string> + <string name="mode_session">Session</string> + <string name="mode_suspend">Bakgrundsbeteende</string> + <string name="mode_suspend_hint">När appen är dold eller skärmen stängs av.</string> + <string name="mode_suspend_manual">Manuell</string> + <string name="mode_suspend_native">Inbyggd</string> + <string name="mode_suspend_steam_note">Auto pausar och återupptar när den visas. Inbyggd förbereder Steam för vila först. Manuell väntar på Återuppta. Aldrig fortsätter köra.</string> + <string name="mode_background_downloads">Håll nedladdningar igång</string> + <string name="mode_background_downloads_hint">Skjut upp bakgrundspausning medan Steam aktivt laddar ned.</string> + <string name="mode_suspend_note">Auto pausar i bakgrunden och återupptar när den visas. Manuell väntar på Återuppta. Aldrig fortsätter köra.</string> + <string name="mode_startup">Uppstart</string> + <string name="mode_steam_startup">Starta Steam med DroidDeck</string> + <string name="mode_steam_startup_hint">Öppnar Steam-sessionen när DroidDeck startar.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">Söker efter utgåvor…</string> + <string name="mode_decky_ready">Redo att installera %1$s.</string> + <string name="mode_decky_none">Inget kompatibelt bygge hittades. Försök igen senare.</string> + <string name="mode_decky_installed">Installerad · %1$s</string> + <string name="mode_decky_update">Uppdatering tillgänglig · %1$s</string> + <string name="mode_decky_current">Senaste versionen · %1$s</string> + <string name="mode_decky_loader">Loader</string> + <string name="mode_decky_install_latest">Installera senaste</string> + <string name="mode_decky_on">Startar med Steam. Medan det är på kan andra appar på enheten eventuellt styra Steam.</string> + <string name="mode_decky_off">Av. Steams fjärrfelsökningsport förblir stängd.</string> + <string name="mode_decky_remove_title">Avinstallera Decky Loader?</string> + <string name="mode_decky_remove_text">Tar bort inläsaren. Dina tillägg och inställningar behålls.</string> + <string name="mode_client">Klient</string> + <string name="mode_mangoapp">Prestandaöverlägg</string> + <string name="mode_mangoapp_hint">Stäng av om spel kraschar eller blir svarta i Deck-läge. Gäller från nästa session.</string> + <string name="mode_steam_repair">Reparera Steam-klienten</string> + <string name="mode_steam_repair_hint">Laddar ned Steam-klienten igen nästa gång Steam startar. Använd det om Steam fastnar på sin inläsningsskärm. Spel, inloggning och inställningar behålls.</string> + <string name="mode_steam_repair_queued">Steam-klienten laddas ned igen nästa gång Steam startar.</string> + <string name="mode_steam_repair_button">Reparera</string> + <string name="mode_added_art">Grafik</string> + <string name="mode_sgdb_key">SteamGridDB API-nyckel</string> + <string name="mode_sgdb_key_yours">Din nyckel</string> + <string name="mode_sgdb_edit">Redigera…</string> + <string name="mode_added_art_hint">Fyller i saknad grafik från Steam, sedan SteamGridDB. Din egen cover.jpg, hero.jpg, logo.png eller icon.png i spelmappen har företräde.</string> + <string name="summary_remove_game">Ta bort %1$s</string> + <string name="mode_added_remove_confirm">Ta bort?</string> + <string name="games_add">Lägg till spel</string> + <string name="games_add_title">Lägg till ett spel:</string> + <string name="games_pick_exe_title">Välj exe-filen:</string> + <string name="games_add_unreachable">Den här platsen är inte tillgänglig i sessionen</string> + <string name="mode_added_exe_check">Kontrollera den här exe-filen</string> + <string name="wincomp_title">Windows-komponenter</string> + <string name="wincomp_pad_hint">LB/RB hoppar mellan avsnitten.</string> + <string name="wincomp_card_none">Ingen</string> + <string name="wincomp_applies">Windows-DLL:er som Proton inte innehåller, från komponentlistan. Slå på de som det här spelet behöver: de hamnar i spelets egen Windows-mapp, aldrig bland dess filer, och finns kvar oavsett vilken Proton det körs på.</string> + <string name="wincomp_offline">Kunde inte nå komponentlistan. Komponenter som redan laddats ned kan fortfarande växlas.</string> + <string name="wincomp_recommended">Rekommenderas för det här spelet</string> + <string name="wincomp_checking">Kontrollerar vad det här spelet behöver…</string> + <string name="wincomp_when_online">Tillgängligt när du är uppkopplad</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">Slår på %1$d komponent…</item> + <item quantity="other">Slår på %1$d komponenter…</item> + </plurals> + <string name="wincomp_auto_section">På för det här spelet</string> + <string name="wincomp_auto_steam">Steam installerar %1$s med %2$s (app %3$s)</string> + <string name="wincomp_auto_gog">Från GOG:s lista för det här spelet</string> + <string name="wincomp_auto_epic">Från Epics förutsättningar för det här spelet</string> + <string name="wincomp_auto_amazon">Från Amazons installationssteg för det här spelet</string> + <string name="wincomp_steam_by_name">Steam installerar %1$s med app %2$s, matchad på namn</string> + <string name="wincomp_suggestions">Listas även</string> + <string name="wincomp_suggestion_reason">Listad av %1$s; inte en komponent här</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">Spelet levereras med %1$s</string> + <string name="wincomp_found_shipped">Spelet har %1$s (fungerar ofta som det är)</string> + <string name="wincomp_already_here">Redan i det här spelet</string> + <string name="wincomp_no_recommendation">Inget i spelets mapp pekar på en komponent.</string> + <string name="wincomp_active">Installerade för det här spelet (%1$d)</string> + <string name="wincomp_all">Alla komponenter (%1$d)</string> + <string name="wincomp_installers">Behöver ett Windows-installationsprogram (%1$d)</string> + <string name="wincomp_installers_note">Dessa är egna installationsprogram utan något paket inuti att plocka isär, och det finns ännu ingen uppteckning av vad de installerar. De flesta är gamla körmiljöer som Mono inuti Proton redan ersätter; uppteckningar läggs till när de görs.</string> + <string name="wincomp_show">Visa</string> + <string name="wincomp_hide">Dölj</string> + <string name="wincomp_needs_installer">Körs med sitt eget installationsprogram (ingen uppteckning än)</string> + <string name="wincomp_unsupported">Stöds inte här än</string> + <string name="wincomp_loading">Läser in komponenterna…</string> + <string name="wincomp_failed">Kunde inte installera: %1$s</string> + <string name="wincomp_downloaded">Nedladdad</string> + <string name="wincomp_downloading">Laddar ned %1$s</string> + <string name="wincomp_installing">Installerar: %1$s</string> + <string name="wincomp_reverting">Stänger av…</string> + <string name="wincomp_download_size">%1$s nedladdning</string> + <string name="wincomp_next_launch">Börjar gälla nästa gång spelet startar.</string> + <string name="mode_stretch169">Sträck ut 16:9-spel</string> + <string name="mode_stretch169_hint">På skärmar som är fyrkantigare än 16:9 sträcks spel ut för att fylla skärmen i stället för att visa svarta ränder. Gäller från nästa session.</string> + <string name="mode_audio">Ljud</string> + <string name="mode_directaudio">DirectAudio för spel</string> + <string name="mode_directaudio_hint">Kringgår PulseAudio för lägre latens.</string> + <string name="mode_mic">Mikrofon</string> + <string name="mode_mic_hint">För röstchatt.</string> + <string name="mode_network">Nätverk</string> + <string name="mode_wifi_discovery">Wi-Fi-identifiering</string> + <string name="mode_wifi_discovery_hint">Visa Wi-Fi-namn och nätverk i närheten i Steam.</string> + <string name="mode_wifi_location_off">Platstjänster är av.</string> + <string name="mode_wifi_permission_denied">Platsbehörighet har inte beviljats.</string> + <string name="mode_wifi_open_settings">Öppna inställningar</string> + <string name="mode_wifi_continue">Fortsätt</string> + <string name="mode_wifi_explain_title">Aktivera Wi-Fi-identifiering?</string> + <string name="mode_wifi_explain_text">Android kräver platsbehörighet för att läsa Wi-Fi-namn och upptäcka nätverk i närheten. DroidDeck använder den här åtkomsten för att visa dessa nätverk i Steam. Vi spårar inte din plats. Internetåtkomst fungerar utan den här behörigheten.</string> + <string name="mode_storage">Spellagring</string> + <string name="mode_storage_auto">Automatiskt – SD-kortet när det finns, plus dina Games-mappar</string> + <string name="mode_storage_auto_none">Automatiskt – dina Games-mappar, plus SD-kortet när det finns (inget nu)</string> + <string name="mode_storage_internal">Endast internt</string> + <string name="mode_storage_folder">Mapp: %1$s</string> + <string name="mode_second_library">Andra biblioteket</string> + <string name="mode_storage_note">Spel som strömmar tillgångar från SD-kortet eller delad lagring kan hacka.</string> + <string name="mode_storage_diagnostics">Lagringsdiagnostik</string> + <string name="mode_storage_diagnostics_hint">Registrerar allokeringsresultat och uppmätta lagringstider i Dela loggar. Kan göra installationer långsammare medan funktionen är aktiverad. Gäller från nästa session.</string> + <string name="mode_choose_folder">Välj en mapp…</string> + <string name="mode_custom_res_title">Anpassad upplösning</string> + <string name="mode_custom_res_text">Ersätter gränsen och bildförhållandet. En storlek som inte matchar skärmen får ränder.</string> + <string name="mode_width">Bredd</string> + <string name="mode_height">Höjd</string> + <string name="mode_custom_res_range">Mellan 320×240 och 3840×2160.</string> + <plurals name="mode_added_count"> + <item quantity="one">%1$d spel</item> + <item quantity="other">%1$d spel</item> + </plurals> + <string name="common_ok">OK</string> + <string name="common_delete">Ta bort</string> + <string name="common_restore">Återställ</string> + <string name="common_download">Ladda ned</string> + <string name="common_delete_named">Ta bort %1$s</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Körmiljödrivrutin</string> + <string name="comp_runtime_driver_hint">Används av Steam, spel och skrivbordet. Gäller från nästa session.</string> + <string name="comp_import_turnip">Importera Turnip-zip…</string> + <string name="comp_display_driver">Skärmdrivrutin</string> + <string name="comp_display_driver_hint">Ritar bildrutor på skärmen i båda lägena. Starta om appen för att verkställa.</string> + <string name="comp_import_adrenotools">Importera en AdrenoTools-zip…</string> + <string name="comp_never">aldrig</string> + <string name="comp_nightlies_checked">Nattbyggena kontrollerade %1$s</string> + <string name="comp_title">Komponenter</string> + <string name="comp_about">Om komponenter</string> + <string name="comp_check_drivers">Sök efter nya drivrutiner</string> + <string name="comp_check_nightlies">Sök i nattbyggena efter nya paket</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">Den Proton som väljs här kör varje spel som inte har valt sin egen i Steam, och en ändring som görs i Steam visas även här. Steam ersätter Valves Proton när den uppdateras; originalen behålls för varje bygge.</string> + <string name="comp_scope">Konfiguration</string> + <string name="comp_profile_configure">Konfigurera profil</string> + <string name="comp_profile">Profil</string> + <string name="comp_scope_general">Standardprofil</string> + <string name="comp_scope_general_detail">Används automatiskt av spel utan åsidosättningar.</string> + <string name="comp_profile_game_detail">Sparade inställningar som används automatiskt när %1$s startar.</string> + <string name="comp_game_hint">Välj bara de inställningar det här spelet behöver. Allt annat fortsätter att använda standardprofilen.</string> + <string name="comp_game_fex">FEX-förinställning</string> + <string name="comp_game_fex_inherited">Använder standardprofilens förinställning.</string> + <string name="comp_game_fex_override">Anpassad förinställning sparad för det här spelet.</string> + <string name="comp_game_fex_custom">Anpassade FEX-variabler</string> + <string name="comp_game_use_general">Använd standard</string> + <string name="comp_game_compatibility">Spelkompatibilitet</string> + <string name="comp_game_environment_hint">Miljöalternativ för Proton, DXVK, VKD3D och synkronisering för det här spelet.</string> + <string name="comp_game_proton_inherited">Använder standardprofilens Proton.</string> + <string name="comp_game_proton_override">Anpassad Proton sparad för det här spelet via Steams kompatibilitetsmappning.</string> + <string name="comp_no_proton_short">Ingen Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Använder standardprofilens paket från %1$s om inte det här spelet väljer ett annat paket.</string> + <string name="comp_game_component_general">Använder standardprofilens komponentpaket.</string> + <string name="comp_game_component_override">Använder %1$s bara för det här spelet.</string> + <string name="comp_game_runtime_options">Körmiljöalternativ</string> + <string name="comp_game_environment_other_hint">Övriga miljöalternativ per spel för Proton, DXVK, VKD3D-Proton och synkronisering.</string> + <string name="comp_game_saved_next">%1$s:s profil är sparad för nästa spelstart.</string> + <string name="game_no_steam_title">Starta utan Steam</string> + <string name="game_no_steam_detail">För ett spel som stängs direkt: startar det utan Protons Steam-inställning. Steam-funktioner som overlay är avstängda för det.</string> + <string name="game_no_steam_on">På</string> + <string name="comp_game_proton_live">%1$s:s Proton-val är sparat och tillgängligt i Steam nu.</string> + <string name="comp_game_proton_next">%1$s:s Proton-val är sparat. Starta om Steam innan du startar spelet.</string> + <string name="comp_game_proton_not_runnable">%1$s kan inte använda den valda Proton på den här enheten.</string> + <string name="comp_game_load_failed">Kunde inte läsa in %1$s:s profil: %2$s</string> + <string name="comp_game_save_failed">Spelprofilen kunde inte sparas: %1$s</string> + <string name="comp_game_reset">Återställ spelprofil</string> + <string name="comp_game_reset_title">Återställ %1$s:s profil?</string> + <string name="comp_game_reset_body">Ta bort det här spelets Proton, FEX, komponentpaket, miljö, texturfiltrering och Windows-komponentåsidosättningar? Det kommer att använda standardprofilen.</string> + <string name="comp_game_reset_done">%1$s använder nu standardprofilen.</string> + <string name="comp_game_reset_failed">Spelprofilen kunde inte återställas helt: %1$s</string> + <string name="comp_default_live">%1$s körs nu för varje spel som inte har valt sin egen Proton i Steam.</string> + <string name="comp_default_next">%1$s kommer att köras för varje spel som inte har valt sin egen Proton, från nästa gång Steam startar.</string> + <string name="comp_default_not_runnable">%1$s kan inte starta spel här och kan därför inte vara standard.</string> + <string name="comp_default_failed">Standard-Proton kunde inte sparas: %1$s</string> + <string name="comp_swapped">%1$s · %2$d utbytta</string> + <string name="comp_all_original">%1$s · allt är original</string> + <string name="comp_game_running_suffix">%1$s · spel körs</string> + <string name="comp_esync_pack">%1$s · droiddeck-esync-paket</string> + <string name="comp_esync_wanted">%1$s · inget droiddeck-esync-paket än</string> + <string name="comp_esync_builtin">%1$s · esync inbyggt, inget paket behövs</string> + <string name="comp_gpu_drivers">GPU-drivrutiner</string> + <string name="comp_reapplied">%1$s · återapplicerad vid start %2$s</string> + <string name="comp_next_queued">Nästa: %1$s · efter att spelet stängs</string> + <string name="comp_in_use">%1$s används: %2$s</string> + <string name="comp_waits">Väntar tills spelet stängs</string> + <string name="comp_applies_next_game">Gäller nästa gång ett spel startar</string> + <string name="comp_import_zip">Importera .zip</string> + <string name="comp_reading">Läser in Proton-versioner…</string> + <string name="comp_no_proton">Ingen Proton installerad än. Starta Steam en gång för att ladda ned dess ARM64-Proton, eller lägg till GE-Proton, CachyOS eller DroidDeck-Proton från Konfiguration.</string> + <string name="comp_original">Original · %1$s</string> + <string name="comp_this_build">%1$s · det här bygget</string> + <string name="comp_earlier_build">%1$s · tidigare bygge</string> + <string name="comp_installed_count">Installerade · %1$d</string> + <string name="comp_nothing_stored">Inget sparat för %1$s än. Den här Protonens egna filer sparas som dess original.</string> + <string name="comp_waits_long">Ett spel körs på den här Proton, så detta väntar tills det stängs.</string> + <string name="comp_applies_long">Gäller nästa gång ett spel startar.</string> + <string name="comp_swap_title">Byt ut %1$s?</string> + <string name="comp_swap_body">Lägg %1$s i %2$s? De ursprungliga filerna förblir sparade. %3$s</string> + <string name="comp_restore_title">Återställ %1$s?</string> + <string name="comp_restore_body">Lägg tillbaka originalet av %1$s i %2$s? %3$s</string> + <string name="comp_delete_package_body">Tar bort paketet från appen. Du kan ladda ned det igen.</string> + <string name="comp_delete_original_title">Ta bort det här originalet?</string> + <string name="comp_delete_original_body">Det här originalet av %1$s är från ett tidigare bygge av %2$s. Originalet från det aktuella bygget behålls alltid.</string> + <string name="comp_nightlies_count">I nattbyggena · %1$d</string> + <string name="comp_refresh_to_list">Uppdatera för att lista paket i nattbyggena.</string> + <string name="comp_nothing_new">Inget nytt sedan senaste kontrollen.</string> + <string name="comp_import_wcp">Importera .wcp</string> + <string name="comp_restore_original">Återställ den här Protonens original</string> + <string name="comp_restore_original_body">Lägg tillbaka %1$s:s original %2$s?</string> + <string name="comp_swap">Byt</string> + <string name="comp_about_text">FEX, DXVK och VKD3D-Proton för varje Proton som Steam kör spel med.\n\nVälj en installerad rad för att byta till den. Det börjar gälla nästa gång ett spel startar, eller efter att det körande spelet stängs.\n\nDina val kontrolleras igen före varje start och återställs om något har ändrat dem. Varje Protons originalfiler behålls, så Steam-uppdateringar förlorar dem aldrig.</string> + <string name="comp_tag_original">Original</string> + <string name="comp_tag_in_use">I bruk</string> + <string name="comp_tag_new">Ny</string> + <string name="comp_tag_stored">Sparad</string> + <string name="perf_title">Prestanda</string> + <string name="perf_lede">CPU-kärnor, sessionsfixar och värdnamnet. Gäller från nästa session.</string> + <string name="perf_client_cores_group">Steam-klientens kärnor</string> + <string name="perf_override">Åsidosätt Steams kärnval</string> + <string name="perf_override_hint">Fäster Steam, dess UI-hjälpprocess och gamescope vid kärnorna nedan.</string> + <string name="perf_client_cores">Klientkärnor</string> + <string name="perf_client_cores_on">Kärnor för Steam.</string> + <string name="perf_client_cores_off">Slå på åsidosättningen för att välja.</string> + <string name="perf_game_cores_group">Spelkärnor</string> + <string name="perf_game_cores">Spelkärnor</string> + <string name="perf_game_cores_hint">Kärnor för spel. Med alla valda schemalägger Android fritt.</string> + <string name="perf_client_ui">Klientgränssnitt</string> + <string name="perf_glthread">Trådad GL</string> + <string name="perf_glthread_hint">Kan göra Steams menyer mer responsiva.</string> + <string name="perf_zink">Zink: lata deskriptorer</string> + <string name="perf_zink_hint">Rekommenderas för drivrutiner utan deskriptorbuffertar.</string> + <string name="perf_noglerror">Hoppa över GL-felkontroller</string> + <string name="perf_noglerror_hint">Stänger av GL-validering per anrop.</string> + <string name="perf_gsrealtime">gamescope: GPU-kö i realtid</string> + <string name="perf_gsrealtime_hint">Prioriterar kompositorns GPU-arbete. Kan jämna ut bildtakten till viss kostnad för spelen.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">Håll GPU:n på dess högsta klockfrekvens</string> + <string name="perf_gpuclock_hint">Bara Adreno. Färre hack, högre batteriförbrukning och mer värme.</string> + <string name="perf_fixes">Sessionsfixar</string> + <string name="perf_sysmem">Turnip: sysmem-rendering</string> + <string name="perf_sysmem_hint">Snabbare för Steam och de flesta spel. Krävs på Adreno 710/720/722.</string> + <string name="perf_xalia">Hoppa över Steams xalia-hjälp</string> + <string name="perf_fastsync">droiddeck-ntsync (experimentell)</string> + <string name="perf_fastsync_hint">En ersättning i användarutrymmet för Linux ntsync-drivrutinen, för testning. Medan det är på använder Proton det i stället för droiddeck-esync och droiddeck-fsync. Fungerar med Proton Experimental ARM64, GE-Proton och proton-cachyos. Gäller från nästa session.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync först</string> + <string name="perf_fsync_hint">Kör Protons egen fsync på de ursprungliga Proton-filerna, med futex_waitv tillhandahållen i användarutrymmet, så det behöver inget paket och fortsätter fungera så snart Steam uppdaterar Proton. Medan det är på använder spel det före droiddeck-esync, förutom de få som Proton håller borta från fsync. Gäller från nästa session.</string> + <string name="perf_esync_hint">Spelens Windows-trådar väntar på varandra via eventfd och delat minne i stället för tur-och-returer genom wineserver. Använder ett droiddeck-esync-paket som är gjort för exakt det Proton-bygget; paket hämtas automatiskt. Tills ett sådant finns för ett nytt Proton-bygge används droiddeck-fsync i stället. Gäller från nästa session.</string> + <string name="perf_xalia_hint">Inaktiverar Protons hjälp för handkontrollsnavigering, vilket kostar CPU i alla spel.</string> + <string name="perf_seccomp">Kör proot utan seccomp</string> + <string name="perf_seccomp_hint">Kan åtgärda fel med saknade systemanrop på vissa Linux-kärnor. Kan minska prestandan.</string> + <string name="perf_fastpath">proot snabbväg</string> + <string name="perf_fastpath_needs_seccomp">Kräver proots seccomp-filter.</string> + <string name="perf_fastpath_hint">Snabbare filuppslagningar och mindre hack. Stäng av om ett program inte hittar sina filer.</string> + <string name="perf_identity">Sessionsidentitet</string> + <string name="perf_hostname">Värdnamn</string> + <string name="perf_hostname_hint">Namnet Steam ser för den här enheten. Lämna tomt för att återställa till %1$s.</string> + <string name="perf_hostname_invalid">Endast bokstäver, siffror och bindestreck inuti, upp till 63 tecken.</string> + <string name="perf_phantom_warning">Android är inställt på att avsluta den här sessionen</string> + <string name="driver_installed">Installerad</string> + <string name="driver_available">Tillgänglig att ladda ned</string> + <string name="driver_nothing_new">Inget nytt sedan senaste kontrollen.</string> + <string name="driver_restore_builtin">Återställ inbyggda drivrutiner</string> + <string name="driver_delete_builtin">Inbyggd i appen: detta tar bort dess uppackade filer och döljer den. Återställ inbyggda drivrutiner tar tillbaka den.</string> + <string name="driver_delete_bundle">Hälften av ett Android + Linux-paket: båda halvorna tas bort. En drivrutin som används faller tillbaka till standard.</string> + <string name="driver_delete_plain">Dess filer tas bort från appen.</string> + <string name="driver_delete_in_use">Dess filer tas bort. Den används, så standarden tar över.</string> + <string name="drawer_page_display">Skärm</string> + <string name="drawer_page_effects">Effekter</string> + <string name="drawer_page_controls">Kontroller</string> + <string name="drawer_page_components">Komponenter</string> + <string name="drawer_page_controller">Handkontroll</string> + <string name="drawer_page_settings">Inställningar</string> + <string name="drawer_page_desc">%1$s-sidan</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Skrivbord</string> + <string name="drawer_steam_menu">Steam-meny</string> + <string name="drawer_quick_access">Snabbåtkomst</string> + <string name="drawer_open_qam">Öppna snabbåtkomstmenyn</string> + <string name="drawer_prev_page">Föregående sida</string> + <string name="drawer_next_page">Nästa sida</string> + <string name="drawer_hud">Prestanda-HUD</string> + <string name="drawer_scaling">Skalningsläge</string> + <string name="drawer_scaling_sharpness">Skalningsskärpa</string> + <string name="drawer_effects">Skärmeffekter</string> + <string name="drawer_effects_hint">Live. Kostar lite GPU-tid medan någon effekt är på.</string> + <string name="drawer_look">Förinställningar</string> + <string name="drawer_look_custom">Anpassad</string> + <string name="drawer_effects_advanced">Avancerat</string> + <string name="widgets_expanded">Expanderad</string> + <string name="widgets_collapsed">Hopfälld</string> + <string name="drawer_cas">Skärpning (CAS)</string> + <string name="drawer_cas_level">Skärpningsnivå</string> + <string name="drawer_fake_hdr">Falsk HDR</string> + <string name="drawer_deband">Avbandning</string> + <string name="drawer_deband_strength">Avbandningsstyrka</string> + <string name="drawer_brightness">Ljusstyrka</string> + <string name="drawer_contrast">Kontrast</string> + <string name="drawer_gamma">Gamma</string> + <string name="drawer_saturation">Mättnad</string> + <string name="drawer_fxaa">FXAA (kantutjämning)</string> + <string name="drawer_toon">Toon-konturer</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Texturfiltrering</string> + <string name="drawer_texture_hint">DirectX 9–11-spel, från nästa start.</string> + <string name="drawer_anisotropy">Anisotropisk filtrering</string> + <string name="drawer_texture_sharpness">Texturskärpa</string> + <string name="drawer_texture_sharpness_note">En negativ mip-bias: skarpare texturer, lite skimmer. Auto matchar hur mycket sessionen förstoras till skärmen.</string> + <string name="drawer_touch_auto">Auto (%1$s)</string> + <string name="drawer_keyboard">Tangentbord</string> + <string name="drawer_pc_keyboard">PC-tangentbord</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">Andra skärmen</string> + <string name="drawer_second_screen_mode">Läge</string> + <string name="drawer_second_screen_shows">Visar</string> + <string name="drawer_android_apps">Android-appar</string> + <string name="drawer_launch_app">Starta en app</string> + <string name="common_hide">Dölj</string> + <string name="common_show">Visa</string> + <string name="drawer_no_apps">Inga startbara appar</string> + <string name="drawer_session_behavior">Sessionsbeteende</string> + <string name="drawer_suspend_hint">Gäller nu och för framtida sessioner i det här läget.</string> + <string name="drawer_next_session">Nästa session</string> + <string name="drawer_support">Support</string> + <string name="drawer_logs">Sessionsloggar</string> + <string name="drawer_logs_hint">Bifoga till en felrapport</string> + <string name="drawer_share_logs">Dela loggar</string> + <string name="drawer_background">Bakgrund</string> + <string name="drawer_stop">Stoppa</string> + <string name="drawer_stop_title">Stoppa sessionen?</string> + <string name="drawer_running_now">Körs nu</string> + <string name="drawer_no_game">Inget spel körs på en Proton</string> + <string name="drawer_selected_profile">Spelprofil</string> + <string name="drawer_selected_profile_none">Profildetaljer är inte tillgängliga än.</string> + <string name="drawer_profile_refresh_failed_initial">Kunde inte läsa in spelprofiler. Försöker igen automatiskt.</string> + <string name="drawer_profile_refresh_failed_stale">Kunde inte uppdatera spelprofiler. Visar senast tillgängliga data; försöker igen automatiskt.</string> + <string name="drawer_profile_inspect_hint">Visa och redigera de sparade inställningarna som används vid nästa start.</string> + <string name="drawer_profile_default">Använder standardprofilen</string> + <string name="drawer_profile_custom">Anpassad profil</string> + <string name="drawer_component_inherited">Använder %1$s från standardprofilen.</string> + <string name="drawer_game_components_note">Ändringar sparas i %1$s:s profil och gäller nästa gång spelet startar.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d spelåsidosättning</item> + <item quantity="other">%1$d spelåsidosättningar</item> + </plurals> + <string name="drawer_no_proton">Ingen Proton är installerad än.</string> + <string name="drawer_game_running">spel körs</string> + <string name="drawer_reapplied">återapplicerad vid start %1$s</string> + <string name="drawer_original">Original</string> + <string name="drawer_components_note">Importera och ta bort på sidan Komponenter.</string> + <string name="setup_check_done">Klart</string> + <string name="setup_check_warn">Behöver åtgärdas</string> + <string name="setup_check_busy">Arbetar</string> + <string name="setup_limit_adb_failed">Kontrollera IP-adressen och porten för Trådlös felsökning, eller para ihop enheten igen om Android tog bort den.</string> + <string name="setup_install">Installera</string> + <string name="setup_manage">Hantera</string> + <string name="setup_tab_overview">Översikt</string> + <string name="setup_tab_controller">Handkontroll</string> + <string name="setup_tab_session">Session</string> + <string name="setup_tab_launcher">Startprogram</string> + <string name="setup_title">Konfiguration</string> + <string name="setup_all_set">● Allt klart</string> + <string name="setup_n_ready">%1$d av %2$d klara</string> + <string name="setup_system_check">Systemkontroll</string> + <string name="setup_gpu_supported">Enheten stöds</string> + <string name="setup_gpu_untested">Otestad GPU</string> + <string name="setup_gpu_unsupported">GPU stöds inte</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">Steam behöver en Adreno-drivrutin; %1$s kan visa en svart skärm</string> + <string name="setup_runtime">Linux-körmiljö</string> + <string name="setup_runtime_progress">%1$s · %2$d %%</string> + <string name="setup_runtime_missing">Inte installerad · cirka 3 GB, installeras första gången du trycker på Play</string> + <string name="setup_installed">Installerad</string> + <string name="setup_runtime_update">%1$s · uppdatering tillgänglig</string> + <string name="setup_runtime_current">%1$s · senaste versionen</string> + <string name="setup_limit">Gräns för underprocesser</string> + <string name="setup_limit_on">På · Android kan stänga Steam. Det tar en minut att stänga av den.</string> + <string name="setup_limit_unset">Inte inställd · Android kan stänga Steam. Det tar en minut att stänga av den.</string> + <string name="setup_limit_unknown">Kunde inte kontrolleras · det tar en minut att stänga av den.</string> + <string name="setup_fix_it">Åtgärda</string> + <string name="setup_details">Detaljer</string> + <string name="setup_dev_options">Utvecklaralternativ</string> + <string name="setup_use_wireless">Använd Trådlös felsökning</string> + <string name="setup_check_again">Kontrollera igen</string> + <string name="setup_limit_turn_on">Slå på gränsen</string> + <string name="setup_account">Steam-konto</string> + <string name="setup_signed_in_offline">Inloggad som %1$s · offlineläge</string> + <string name="setup_signed_in">Inloggad som %1$s</string> + <string name="setup_sign_in">Tryck på Play och logga in på Steam</string> + <string name="setup_tools">Verktyg</string> + <string name="setup_controller">Handkontroll</string> + <string name="setup_controller_unavailable">Handkontrollsinställningar är inte tillgängliga.</string> + <string name="setup_session">Session</string> + <string name="setup_logs">Sessionsloggar</string> + <string name="setup_logs_hint">Sparas efter varje session</string> + <string name="setup_agent_commands">Felsökningsverktyg</string> + <string name="setup_latest_logs">Senaste sessionsloggar</string> + <string name="setup_saved_logs">Sparade sessionsloggar</string> + <string name="setup_saved_logs_hint">Behåller de %1$d senaste</string> + <string name="setup_clear_logs">Rensa loggar</string> + <string name="setup_offline">Offlineläge</string> + <string name="setup_sign_in_first">Logga in på Steam först</string> + <string name="setup_launcher">Startprogram</string> + <string name="setup_language">Språk</string> + <string name="setup_language_hint">Steam använder det också, från nästa gång det startar.</string> + <string name="setup_language_system">Systemstandard (%1$s)</string> + <string name="setup_language_system_short">Systemstandard</string> + <string name="setup_app_scale">Appskala</string> + <string name="setup_app_scale_hint">DroidDecks text och kontroller</string> + <string name="setup_app_scale_default">%1$d %% (Standard)</string> + <string name="setup_theme">Tema</string> + <string name="setup_theme_hint">Startprogrammets utseende</string> + <string name="setup_home">Använd som startskärm</string> + <string name="setup_home_on">DroidDeck kan vara startskärmsappen</string> + <string name="setup_home_off">DroidDeck erbjuds aldrig som startskärmsapp</string> + <string name="setup_orientation">Orientering</string> + <string name="setup_orientation_auto">Automatiskt liggande</string> + <string name="setup_orientation_landscape">Liggande</string> + <string name="setup_orientation_reverse">Liggande, upp och ned</string> + <string name="setup_fullscreen">Helskärm</string> + <string name="setup_fullscreen_on">Döljer Androids statusfält och navigeringsfält</string> + <string name="setup_fullscreen_off">Visar Androids statusfält och navigeringsfält</string> + <string name="setup_default_home">Standardapp för startskärmen</string> + <string name="setup_choose_home">Välj en app för startskärmen</string> + <string name="setup_choose">Välj</string> + <string name="setup_linux_apps">Linux-appar (beta)</string> + <string name="setup_tool_files">Filer</string> + <string name="setup_tool_files_hint">Bläddra och hantera filer</string> + <string name="setup_tool_protons">Proton-versioner</string> + <string name="setup_tool_protons_hint">Installera ARM64-byggen av Proton</string> + <string name="setup_tool_performance">Prestanda</string> + <string name="setup_tool_performance_hint">Tilldelning av CPU-kärnor</string> + <string name="setup_tool_roms">ROM-mapp</string> + <string name="setup_tool_roms_hint">Där emulatorspel lagras</string> + <string name="setup_card_components">Komponenter</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Kontroller</string> + <string name="setup_card_controls_hint">Knappmappning</string> + <string name="store_tab_discover">Upptäck</string> + <string name="store_tab_search">Sök</string> + <string name="store_tab_installed">Installerade</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Butik</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak redo</string> + <string name="store_needs_runtime">Installera Linux-körmiljön från Konfiguration först.</string> + <string name="store_setup_title">Konfigurera Flatpak</string> + <string name="store_setup_text">Lägger till Flatpak och ansluter det till Flathub (cirka 20 MB). Den första appen laddar även ned sin körmiljö.</string> + <string name="store_setting_up">Konfigurerar…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Uppdateringar</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d %%</string> + <string name="store_starting">Startar…</string> + <string name="store_categories">Kategorier</string> + <string name="store_unreachable">Kunde inte nå Flathub. Kontrollera din anslutning och försök igen.</string> + <string name="store_try_again">Försök igen</string> + <string name="store_search_label">Sök på Flathub</string> + <string name="store_search_placeholder">Firefox, spel, emulatorer…</string> + <string name="store_search">Sök</string> + <string name="store_all">Alla</string> + <string name="store_searching">Söker…</string> + <string name="store_search_hint">Sök efter namn eller välj en kategori. Bara ARM64-appar listas.</string> + <string name="store_no_results">Inga ARM64-appar på Flathub matchar det.</string> + <string name="store_results">Resultat</string> + <string name="store_check_updates">Sök efter uppdateringar</string> + <string name="store_update_all">Uppdatera alla (%1$d)</string> + <string name="store_all_current">● Allt är uppdaterat</string> + <string name="store_no_apps">Inga appar än. Hitta några under Upptäck eller Sök.</string> + <string name="store_setup_first_note">Konfigurera Flatpak för att installera appar.</string> + <string name="store_update_available_prefix">Uppdatering tillgänglig · %1$s</string> + <string name="store_open">Öppna</string> + <string name="store_details">Detaljer</string> + <string name="store_remove">Ta bort</string> + <string name="store_press_again">Tryck igen för att ta bort</string> + <string name="store_installing">Installerar…</string> + <string name="store_install">Installera</string> + <string name="store_setup_first">Konfigurera Flatpak först</string> + <string name="store_download_size">%1$s nedladdning</string> + <string name="store_installed_chip">● Installerad</string> + <string name="store_details_failed">Kunde inte läsa in appens Flathub-sida.</string> + <string name="store_loading">Läser in från Flathub…</string> + <string name="store_no_arm">Inget ARM64-bygge på Flathub, så den kan inte installeras här.</string> + <string name="store_screenshots">Skärmbilder</string> + <string name="store_about">Om</string> + <string name="store_app_id">App-ID</string> + <string name="store_license">Licens</string> + <string name="store_runtime">Körmiljö</string> + <string name="store_installed_size">Installerad storlek</string> + <string name="store_installed_size_value">%1$s (plus dess delade körmiljö)</string> + <string name="store_website">Webbplats</string> + <string name="store_verified">Verifierad</string> + <string name="upd_title">Uppdateringar</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Kontrollerad %1$s</string> + <string name="upd_not_checked">Inte kontrollerad än</string> + <string name="upd_perm_title">Låt DroidDeck uppdatera sig själv</string> + <string name="upd_perm_text">På nästa skärm, slå på Tillåt från den här källan och kom sedan tillbaka. Uppdateringen fortsätter själv.</string> + <string name="upd_open_settings">Öppna inställningar</string> + <string name="upd_not_now">Inte nu</string> + <string name="upd_signed_status">Signerad annorlunda</string> + <string name="upd_signed_headline">Kan inte uppdateras på plats</string> + <string name="upd_signed_detail">Den här kopian är inte signerad med DroidDecks utgivningsnyckel, så uppdateringar kan inte installeras över den. Avinstallera den för att byta.</string> + <string name="upd_available">Uppdatering tillgänglig</string> + <string name="upd_blocked_status">Kan inte installeras</string> + <string name="upd_switch">Redo att byta</string> + <string name="upd_ahead_status">Före Stabil</string> + <string name="upd_ahead_headline">Du ligger före Stabil</string> + <string name="upd_ahead_detail">Det här bygget är nyare än senaste Stabil. Du flyttas till Stabil vid dess nästa utgåva.</string> + <string name="upd_gone_status">Test avslutat</string> + <string name="upd_gone_headline">Det här testet har avslutats</string> + <string name="upd_gone_detail">Dess fix har slagits samman eller slopats. Följ Förhandsversion för de senaste fixarna.</string> + <string name="upd_current_status">Senaste versionen</string> + <string name="upd_current_headline">Du har den senaste %1$s</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d %%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Uppdatera%1$s</string> + <string name="upd_install_button">Installera%1$s</string> + <string name="upd_install_anyway">Installera Stabil %1$s trots allt</string> + <string name="upd_follow_preview">Följ Förhandsversion</string> + <string name="upd_stop_session">Stoppa den körande sessionen för att uppdatera.</string> + <string name="upd_less">Mindre</string> + <string name="upd_more">Mer</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Nytt bygge av Förhandsversion</string> + <string name="upd_new_test">Testbygge för PR #%1$d</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">UPPDATERINGSKANAL</string> + <string name="upd_stable">Stabil</string> + <string name="upd_stable_hint">Testade utgåvor, för de flesta</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Förhandsversion</string> + <string name="upd_preview_hint">De senaste fixarna, före Stabil</string> + <string name="upd_preview_current">Senaste versionen</string> + <string name="upd_tests">Testbyggen</string> + <string name="upd_tests_hint">Prova en fix innan den släpps</string> + <string name="upd_tests_none">Inga just nu</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Inte byggd för den här kopian av DroidDeck</string> + <string name="upd_updated">Uppdaterad %1$s</string> + <string name="upd_test_name">testet för PR #%1$d</string> + <string name="upd_while_ago">för ett tag sedan</string> + <string name="upd_just_now">just nu</string> + <plurals name="upd_tests_count"> + <item quantity="one">%1$d att prova</item> + <item quantity="other">%1$d att prova</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="one">Ändringar sedan ditt bygge · %1$d bygge efter</item> + <item quantity="other">Ändringar sedan ditt bygge · %1$d byggen efter</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="one">Senaste ändringar · %1$d bygge</item> + <item quantity="other">Senaste ändringar · %1$d byggen</item> + </plurals> + <string name="gate_title">En Android-inställning att ändra</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Android stoppar appar som startar många bakgrundsprocesser, och Steam startar dussintals. Tills detta ändras stängs spel utan felmeddelande.</string> + <string name="gate_fix_for_me">Fixa det åt mig</string> + <string name="gate_toggle_steps">Slå på ”Inaktivera begränsningar för underprocesser” och kom sedan tillbaka. Lämna Utvecklaralternativ på, annars stängs omkopplaren av igen.</string> + <string name="gate_open_dev">Öppna Utvecklaralternativ</string> + <string name="gate_no_toggle">Den här Android-versionen har ingen omkopplare för det, så DroidDeck ändrar det via Trådlös felsökning. Tar ungefär en minut, ingen dator behövs.</string> + <string name="gate_needs_wifi">Trådlös felsökning kräver Wi-Fi. Alla nätverk fungerar; det behöver inte internet.</string> + <string name="gate_needs_notifications">Ihopkopplingskoden skickas i en avisering. Tillåt aviseringar för DroidDeck, eller använd Andra sätt.</string> + <string name="gate_allow_notifications">Tillåt aviseringar</string> + <string name="gate_step1">Öppna Inställningar → Utvecklaralternativ → Trådlös felsökning och slå på det</string> + <string name="gate_step2">Tryck på ”Para ihop enhet med ihopkopplingskod”</string> + <string name="gate_step3">Skriv in koden i DroidDeck-aviseringen</string> + <string name="gate_waiting">Väntar på popup-fönstret för ihopkoppling…</string> + <string name="gate_code_needed">Popup-fönster hittat. Ange dess kod i aviseringen.</string> + <string name="gate_status">Status</string> + <string name="gate_polling">Kontrollerar var 2 sekunder. Den här sidan stängs när det är av.</string> + <string name="gate_other_ways">Andra sätt</string> + <string name="gate_other_toggle">Trådlös felsökning, manuell adress, dator-ADB</string> + <string name="gate_other_no_toggle">Manuell adress, dator-ADB, Utvecklaralternativ</string> + <string name="gate_use_wireless_instead">Använd Trådlös felsökning i stället</string> + <string name="gate_some_roms">Vissa ROM:ar lägger till omkopplaren trots allt. Leta efter ”underprocess” i Utvecklaralternativ.</string> + <string name="gate_manual_hint">Om aviseringen inte fungerar, ange popup-fönstrets adress och kod själv.</string> + <string name="gate_enter_manually">Ange adress manuellt</string> + <string name="gate_from_computer">Från en dator med ADB:</string> + <string name="gate_copy_command">Kopiera kommando</string> + <string name="gate_choose_display">Välj vilken skärm Android-inställningar ska öppnas på.</string> + <string name="gate_main_screen">Huvudskärm</string> + <string name="wadb_finding_port">Ihopkopplad. Letar efter porten för Trådlös felsökning…</string> + <string name="wadb_enter_port">Ihopkopplad. Ange porten från Inställningar.</string> + <string name="wadb_applying">Enhet hittad. Tillämpar inställningen…</string> + <string name="wadb_apply_failed">Kunde inte tillämpas automatiskt. Kontrollera adressen nedan och försök igen. %1$s</string> + <string name="wadb_title_wireless">Trådlös felsökning</string> + <string name="wadb_title_done">Gränsen för underprocesser har uppdaterats</string> + <string name="wadb_title">Ändra gränsen för underprocesser</string> + <string name="wadb_lede_compact0">Om alternativet saknas, para ihop via Trådlös felsökning.</string> + <string name="wadb_lede_compact1">Ihopkopplad. Kontrollera IP-adressen och porten i Trådlös felsökning.</string> + <string name="wadb_lede_done">Gränsen för underprocesser har uppdaterats.</string> + <string name="wadb_lede0">Använd steg 2 om Utvecklaralternativ inte har någon inställning för underprocesser. Öppna Utvecklaralternativ → Trådlös felsökning → Para ihop enhet med ihopkopplingskod.</string> + <string name="wadb_lede1">Ihopkopplad. Använd adressen nedan, eller uppdatera den från Trådlös felsökning → IP-adress & port.</string> + <string name="wadb_lede_done_long">Gränsen för underprocesser uppdaterades via Trådlös felsökning.</string> + <string name="wadb_confirmed_on">Android bekräftade att gränsen är på.</string> + <string name="wadb_confirmed_off">Android bekräftade att gränsen är av.</string> + <string name="wadb_pair_group">2 · Para ihop Trådlös felsökning</string> + <string name="wadb_pair_address">Ihopkopplingsrutans IP-adress & port</string> + <string name="wadb_pin">Ihopkopplingskod (PIN)</string> + <string name="wadb_pin_placeholder">6 siffror</string> + <string name="wadb_pair">Para ihop</string> + <string name="wadb_apply_group">2 · Tillämpa via Trådlös felsökning</string> + <string name="wadb_connect_address">Anslutningens IP-adress & port</string> + <string name="wadb_pair_again">Para ihop igen</string> + <string name="wadb_turn_off">Stäng av gränsen</string> + <string name="wadb_other_ways">Andra sätt att ändra det</string> + <string name="wadb_dev_try_first">1 · Utvecklaralternativ (prova först)</string> + <string name="wadb_restrict_on">Slå på ”Begränsa underprocesser”.</string> + <string name="wadb_restrict_off">Slå av ”Begränsa underprocesser”.</string> + <string name="wadb_computer_last">3 · Dator-ADB (sista utväg)</string> + <string name="wadb_only_if">Bara om Trådlös felsökning inte är tillgänglig.</string> + <string name="wadb_computer_group">3 · Dator-ADB (sista utväg)</string> + <string name="ctrl_osc_hint">När skärmkontrollerna visas i en session</string> + <string name="ctrl_color">Färg</string> + <string name="ctrl_color_hint">Nyans på skärmknapparna</string> + <string name="ctrl_custom">Anpassad</string> + <string name="ctrl_opacity">Opacitet</string> + <string name="ctrl_percent">%1$d %%</string> + <string name="ctrl_size">Knappstorlek</string> + <string name="ctrl_size_hint" formatted="false">100 % är standardstorleken</string> + <string name="ctrl_stick_click">Spakklick</string> + <string name="ctrl_stick_click_hint">Dubbeltryck och håll en spak för L3 eller R3</string> + <string name="ctrl_adaptive">Adaptiva spakar</string> + <string name="ctrl_rumble">Spelvibration</string> + <string name="ctrl_steam_button">Steam-knappen</string> + <string name="ctrl_qam_button">QAM-knappen</string> + <string name="ctrl_keyboard_button">Tangentbordsknappen</string> + <string name="ctrl_select_steam">Tryck på Select för att öppna Steam-menyn</string> + <string name="ctrl_select_steam_hint">I stället för View/Select.</string> + <string name="setup_animations">Animationer</string> + <string name="ctrl_adaptive_hint">Spakarna visas där du trycker och döljs när du släpper</string> + <string name="ctrl_layout">Layout</string> + <string name="ctrl_layout_custom">Anpassade positioner sparade</string> + <string name="ctrl_layout_default">Anpassad till den här skärmen</string> + <string name="ctrl_edit">Redigera</string> + <string name="ctrl_reset">Återställ</string> + <string name="ctrl_mapping">Knappmappning</string> + <string name="ctrl_mapping_default">Varje knapp skickar sin egen inmatning</string> + <string name="ctrl_mapping_count">%1$d av %2$d knappar ommappade</string> + <string name="ctrl_configure">Konfigurera</string> + <string name="ctrl_reset_all">Återställ handkontroll</string> + <string name="ctrl_reset_all_hint">Återställer alla handkontrollsinställningar och layouten</string> + <string name="ctrl_mapping_lede">Vad varje skärmknapp skickar till spelet. Dold tar bort knappen.</string> + <string name="ctrl_onscreen_buttons">Skärmknappar</string> + <string name="ctrl_defaults">Standardvärden</string> + <string name="ctrl_reset_mapping">Återställ mappning</string> + <string name="content_android_apps">Android-appar</string> + <string name="content_apps_count">%1$d appar</string> + <string name="content_no_android_apps">Inga startbara Android-appar hittades.</string> + <string name="content_desktop_installed">● Skrivbord installerat</string> + <string name="content_desktop_first_open">Installeras vid första öppningen</string> + <string name="content_available">Tillgänglig att installera</string> + <string name="content_not_installed">Inte installerad.</string> + <string name="content_not_installed_suffix">%1$s · inte installerad</string> + <string name="content_open_named">Öppna %1$s</string> + <string name="content_games">Spel</string> + <string name="content_choose_roms">Välj en ROM-mapp.</string> + <string name="content_retroarch_roms">Bläddra till /root/ROMs i RetroArch.</string> + <string name="content_add_roms">Lägg till %1$s-spel i ROMs/%2$s.</string> + <string name="content_install_named">Installera %1$s</string> + <string name="content_stop_to_install">Stoppa sessionen för att installera</string> + <string name="content_runtime_required">Körmiljö krävs</string> + <string name="content_loading_details">Läser in installationsdetaljer…</string> + <string name="content_details_unavailable">Installationsdetaljer är inte tillgängliga. Försök igen senare.</string> + <string name="content_needs_runtime">Installera Linux-körmiljön från Konfiguration först.</string> + <string name="content_stop_first">Stoppa den körande sessionen för att installera skrivbordsappar.</string> + <string name="content_install_into">Installerar %1$s i Linux-skrivbordet.</string> + <string name="store_busy_percent_short">%1$s · %2$d %%</string> + <string name="content_hide_note">Att dölja tar bort den från Skrivbord. Dess filer finns kvar i Linux-körmiljön.</string> + <string name="content_rom_gone">Det spelet finns inte längre i ROM-mappen.</string> + <string name="content_desktop_system">Skrivbord · %1$s</string> + <string name="content_launch_in">Starta i %1$s</string> + <string name="content_also_in">Även i %1$s</string> + <string name="content_page_gone">Den sidan finns inte längre. Välj ett avsnitt till vänster.</string> + <string name="content_open_desktop">Öppna skrivbordet</string> + <string name="content_install_open_desktop">Installera & öppna skrivbordet</string> + <string name="desktop_remove_title">Avinstallera skrivbordet</string> + <string name="desktop_remove_text">Ta bort skrivbordet och dess medföljande appar. Behåll spel, tillagda appar, inställningar och delade bibliotek.</string> + <string name="desktop_removing">Tar bort skrivbordet</string> + <string name="desktop_removed">Skrivbordet har tagits bort</string> + <string name="desktop_cleaning_legacy">Tar bort gamla LXQt-filer</string> + <string name="desktop_removal_unknown">Uppdatera DroidDeck innan du tar bort den här skrivbordsversionen.</string> + <string name="content_linux_desktop">Linux-skrivbord</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox och dina emulatorer</string> + <string name="games_empty">Spel du installerar i Steam visas här efter sessionen.</string> + <string name="games_play_steam">Spela Steam</string> + <string name="games_launch_settings">Startinställningar</string> + <string name="games_launch">Starta</string> + <string name="games_files">Spelfiler</string> + <string name="games_prefix">Proton-prefix</string> + <string name="games_saves_looking">Letar efter sparfiler…</string> + <string name="games_saves_none">Ingen sparmapp hittades efter namn. Export tar hela användarmappen.</string> + <string name="games_saves_folders">%1$d sparmappar</string> + <string name="games_saves_summary">%1$s · %2$d filer · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">Hantera sparfiler</string> + <string name="games_saves">Sparfiler</string> + <string name="games_import_saves">Importera sparfiler…</string> + <string name="games_export_gamehub">Exportera som GameHub-zip…</string> + <string name="games_export_winlator">Exportera som Winlator-zip…</string> + <string name="games_open_save_folder">Öppna sparmappen</string> + <string name="games_never_played">Aldrig spelat</string> + <string name="games_runtime_busy_percent">Körmiljö upptagen · %1$d %%</string> + <string name="games_runtime_busy">Körmiljö upptagen</string> + <string name="games_runtime_first_play">Körmiljön installeras första gången du trycker på Play</string> + <string name="games_runtime_update">Uppdatering av körmiljön tillgänglig</string> + <string name="games_runtime_ready">● Körmiljö redo</string> + <string name="games_last_played">Senast spelad %1$s</string> + <string name="games_internal">Internt lagringsutrymme</string> + <string name="games_added">Tillagt spel</string> + <string name="games_custom">Anpassat spel</string> + <string name="games_store_storage">Spellagring</string> + <string name="gpu_auto_hint">Det bästa paret för den här GPU:n, hålls uppdaterat</string> + <string name="gpu_manual_hint">Välj ett par själv</string> + <string name="gpu_checking">Söker efter drivrutiner…</string> + <string name="gpu_none_listed">Inga drivrutiner listade än. Uppdatera för att söka efter dem.</string> + <string name="gpu_hide_other">Dölj drivrutiner för andra GPU:er</string> + <string name="gpu_show_other">Visa drivrutiner för andra GPU:er</string> + <string name="gpu_advanced">Avancerat</string> + <string name="gpu_set_with_display">Ställs in tillsammans med skärmdrivrutinen: %1$s innehåller båda.</string> + <string name="gpu_set_with_runtime">Ställs in tillsammans med körmiljödrivrutinen: %1$s innehåller båda.</string> + <string name="gpu_runtime_hint">Används av spel och skrivbordet.</string> + <string name="gpu_runtime_default">Standard för körmiljön</string> + <string name="gpu_display_hint">Ritar bildrutor på skärmen. Gäller efter att DroidDeck har startats om.</string> + <string name="gpu_auto_by_gpu">Auto – valt efter GPU</string> + <string name="gpu_manage">Ladda ned, importera eller ta bort…</string> + <string name="gpu_in_use_caps">I BRUK</string> + <string name="gpu_builtin">Drivrutinerna som är inbyggda i DroidDeck</string> + <string name="gpu_matched_pair">Körmiljö- och skärmdrivrutiner inställda tillsammans som ett matchat par.</string> + <string name="gpu_recommended_for">Rekommenderas för den här GPU:n: %1$s.</string> + <string name="gpu_refresh_hint">Uppdatera för att söka efter drivrutiner för den här GPU:n.</string> + <string name="gpu_recommended_caps">REKOMMENDERAS</string> + <string name="gpu_unavailable">Inte tillgänglig</string> + <string name="load_runtime">Linux-körmiljö</string> + <string name="load_runtime_detail">Förbereder Linux-körmiljön</string> + <string name="load_session">Session</string> + <string name="load_session_detail">Startar skärm och ljud</string> + <string name="load_steam">Steam-klient</string> + <string name="load_steam_detail">Kontrollerar Steam och dess kompatibilitetsverktyg</string> + <string name="load_opening_steam">Öppnar Steam</string> + <string name="load_opening_steam_detail">Väntar på Steams första bildruta</string> + <string name="load_desktop">Skrivbord</string> + <string name="load_desktop_detail">Förbereder skrivbordet</string> + <string name="load_opening">Öppnar</string> + <string name="load_opening_detail">Väntar på den första bildrutan</string> + <string name="load_starting_steam">Startar Steam</string> + <string name="load_ended">Sessionen avslutades</string> + <string name="load_step">Steg %1$d av %2$d</string> + <string name="load_steam_paused">Steam är pausat</string> + <string name="deck_tab_grips">Grepp</string> + <string name="deck_tab_left">Vänster platta</string> + <string name="deck_tab_right">Höger platta</string> + <string name="deck_tab_both">Båda</string> + <string name="deck_tab_gamepad">Handkontroll</string> + <string name="deck_surface_stick">Styrspak</string> + <string name="deck_surface_trackpad">Styrplatta</string> + <string name="deck_surface_switch">Växla mellan styrspak och styrplatta</string> + <string name="deck_more_controls">Fler Deck-kontroller</string> + <string name="deck_panels">PANELER</string> + <string name="deck_close">Stäng kontrollerna på andra skärmen</string> + <string name="deck_open_steam_menu">Öppna Steam-menyn</string> + <string name="deck_left_upper">Vänster · övre</string> + <string name="deck_left_lower">Vänster · nedre</string> + <string name="deck_right_upper">Höger · övre</string> + <string name="deck_right_lower">Höger · nedre</string> + <string name="deck_click">Klick</string> + <string name="deck_right_trackpad">Höger styrplatta</string> + <string name="deck_left_trackpad">Vänster styrplatta</string> + <string name="second_trackpad">Styrplatta</string> + <string name="second_show_keyboard">Visa tangentbord</string> + <string name="second_hide_keyboard">Dölj tangentbord</string> + <string name="second_terminal">Linux-terminal</string> + <string name="second_terminal_not_ready">Linux-sessionen är inte redo för en terminal än.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Spel</string> + <string name="rail_desktop">Skrivbord</string> + <string name="rail_store">Butik</string> + <string name="rail_components">Komponenter</string> + <string name="rail_apps">Appar</string> + <string name="rail_setup">Konfiguration</string> + <string name="rail_updates">Uppdateringar</string> + <string name="rail_percent">%1$d %%</string> + <string name="rail_resume_named">Återuppta %1$s</string> + <string name="roms_text">Visas som /root/ROMs i varje session. Det interna lagringsutrymmet finns på /root/Storage.</string> + <string name="roms_none">Ingen mapp vald</string> + <string name="roms_applies">Gäller från nästa session. SD-kort fungerar.</string> + <string name="roms_choose">Välj mapp</string> + <string name="roms_change">Byt mapp</string> + <string name="proton_lede">Installera en ARM64-version av Proton och välj den per spel i Steam → Egenskaper → Kompatibilitet.</string> + <string name="proton_stop_first">Stoppa den pågående sessionen för att installera eller ta bort ett kompatibilitetsverktyg.</string> + <string name="proton_available">Tillgängliga versioner</string> + <string name="proton_installed">Installerad %1$s</string> + <string name="proton_queued">Väntar sedan en tidigare begäran</string> + <string name="proton_not_installed">Inte installerad</string> + <string name="proton_update_available">Uppdatering tillgänglig → %1$s</string> + <string name="proton_remove_older">Ta bort gamla %1$s-versioner</string> + <string name="proton_remove_older_body">Tar bort %1$s. Spel som är inställda på dessa versioner måste bytas till en annan Proton.</string> + <string name="proton_remove_body">Spel som är inställda på den här versionen måste bytas till en annan Proton.</string> + <string name="proton_install_now">Installera nu</string> + <string name="proton_cancel_queued">Avbryt kön</string> + <string name="pckb_android">Android-tangentbord</string> + <string name="pckb_hide">Dölj ✕</string> + <string name="main_launch_named">Starta %1$s</string> + <string name="main_choose_screen">Välj en skärm. Sekundär: %1$s.</string> + <string name="main_secondary_gone">Den sekundära skärmen är inte längre tillgänglig.</string> + <string name="main_primary">Primär skärm</string> + <string name="main_secondary">Sekundär skärm</string> + <string name="widgets_prev_tab">Föregående flik</string> + <string name="widgets_next_tab">Nästa flik</string> + <string name="widgets_on">På</string> + <string name="widgets_all_n">Alla %1$d</string> + <string name="widgets_none">Inga</string> + <string name="widgets_n_of_m">%1$d av %2$d</string> + <string name="widgets_off">Av</string> + <string name="screen_bottom">Nedre skärm</string> + <string name="game_env_applies">Gäller från nästa spelstart. Ett spels profil åsidosätter den delade.</string> + <string name="game_env_other">Ett annat spel via app-ID…</string> + <string name="game_env_done">Klar</string> + <string name="game_env_default">Standard</string> + <string name="game_env_off">Av</string> + <string name="game_env_change">Ändra…</string> + <string name="game_env_change_title">Ändra variabel</string> + <string name="game_env_restore_default">Tillbaka till standard</string> + <string name="game_env_eyebrow">Proton-spel</string> + <string name="pip_title">Bild-i-bild</string> + <string name="pip_auto">Öppna bild-i-bild automatiskt</string> + <string name="unfolded_controls">Kontroller på den uppfällda skärmen</string> + <string name="unfolded_controls_hint">Uppfälld ligger spelet ovanför Steam Decks styrplattor och greppknappar. Hopfälld visas spelet i helskärm.</string> + <string name="pip_suspended">Pausad</string> + <string name="pip_not_ready">Bild-i-bild blir tillgängligt när sessionen är redo.</string> + <string name="pip_permission">Tillåt bild-i-bild för DroidDeck i Android-inställningarna.</string> + <string name="pip_unavailable">Android kunde inte öppna bild-i-bild.</string> + <string name="game_shortcut">Genväg</string> + <string name="game_shortcut_add">Lägg till på hemskärmen</string> + <string name="game_shortcut_choose">Välj spel</string> + <string name="game_shortcut_scanning">Söker igenom installerade spel…</string> + <string name="game_link_copy">Kopiera startlänk</string> + <string name="game_link_copied">Startlänken har kopierats</string> + <string name="game_shortcut_unsupported">Den här launchern kan inte lägga till genvägar. Använd Kopiera startlänk i stället.</string> + <string name="game_link_invalid">Ogiltig startlänk för spelet.</string> + <string name="game_link_missing">Det här spelet finns inte längre i ditt DroidDeck-bibliotek.</string> + <string name="game_link_session_busy">Stoppa den aktuella sessionen innan du startar ett Steam-spel.</string> + <string name="game_link_failed">Kunde inte skicka spelet till Steam. Försök starta det igen.</string> + <string name="setup_retry_removal">Försök ta bort igen</string> + <string name="runtime_removing">Tar bort</string> + <string name="runtime_removal_incomplete">Borttagningen är inte klar</string> + <string name="game_file_export">Exportera spelfil</string> + <string name="game_file_sync">Synka alla spelfiler till mapp</string> + <string name="game_file_stop_sync">Sluta synka spelfiler</string> + <string name="game_file_folder">Välj en mapp för frontend-spelfiler</string> + <string name="game_file_exported">Spelfiler sparade till %1$s</string> + <string name="game_frontend_files">Frontend-filer</string> + + <string name="main_wait_runtime">Vänta tills åtgärden på körmiljön är klar</string> + <string name="main_wait_proton_install">Vänta tills installationen av kompatibilitetsverktyget är klar</string> + <string name="main_wait_desktop_install">Vänta tills installationen av skrivbordsappen är klar</string> + <string name="main_wait_install_game">Vänta tills installationen är klar innan du startar ett spel</string> + <string name="main_saves_close_session">Stäng Steam-sessionen först, så att spelet inte skriver över importen</string> + <string name="main_saves_importing">Importerar till %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="one">Importerade %1$d fil från %2$s till %3$s</item> + <item quantity="other">Importerade %1$d filer från %2$s till %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">Importerade %1$d fil från %2$s till %3$s. Gamla sparfiler säkerhetskopierades till Download/DroidDeck/Saves/backups</item> + <item quantity="other">Importerade %1$d filer från %2$s till %3$s. Gamla sparfiler säkerhetskopierades till Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Välj en zip-fil med sparfiler för %1$s</string> + <string name="main_saves_exporting">Exporterar %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="one">Exporterade %1$d fil som %2$s: %3$s</item> + <item quantity="other">Exporterade %1$d filer som %2$s: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Välj var %1$s ska sparas (%2$s)</string> + <string name="main_action_failed">%1$s misslyckades: %2$s</string> + <string name="main_no_logs_yet">Inga sessionsloggar än: kör en session först.</string> + <string name="main_no_logs_to_clear">Inga sessionsloggar att rensa.</string> + <plurals name="main_logs_cleared"> + <item quantity="one">Rensade %1$d sessionsloggmapp.</item> + <item quantity="other">Rensade %1$d sessionsloggmappar.</item> + </plurals> + <string name="main_wadb_pair_failed">Parkopplingen för trådlös felsökning misslyckades</string> + <string name="main_wadb_command_failed">Kommandot för trådlös felsökning misslyckades</string> + <string name="main_wadb_unavailable">Trådlös felsökning är inte tillgänglig</string> + <string name="main_adb_copied">ADB-kommandot har kopierats</string> + <string name="main_pick_roms">Välj ROM-mappen</string> + <string name="main_non_adreno_title">Inte en Adreno-GPU</string> + <string name="main_non_adreno_text">Turnip stöder Adreno-GPU:er. På %1$s kan Steam visa en svart skärm. Nedladdning: %2$.0f MB.</string> + <string name="main_install_anyway">Installera trots det</string> + <string name="main_remove_runtime_title">Ta bort Linux-körmiljön</string> + <string name="main_remove_runtime_text">Det här tar bort körmiljön, Steam-klienten i den och alla spel som är installerade där.</string> + <string name="main_applied">Tillämpat: %1$s</string> + <string name="android_app_open_failed_primary">Kunde inte öppna %1$s på den primära skärmen</string> + <string name="android_app_open_failed_on">Kunde inte öppna %1$s på %2$s</string> + <string name="android_app_open_failed_display">Kunde inte öppna %1$s på skärm %2$d</string> + <string name="comp_busy_importing">Importerar</string> + <string name="comp_busy_swapping">Byter</string> + <string name="comp_busy_restoring">Återställer</string> + <string name="comp_busy_cancelling">Avbryter</string> + <string name="comp_busy_deleting">Tar bort</string> + <string name="comp_cannot_read">kan inte läsa filen</string> + <string name="comp_imported">Importerade %1$s</string> + <string name="comp_swap_cancelled">Det väntande bytet avbröts.</string> + <string name="comp_pick_wcp">Välj ett komponentpaket (-linux .wcp)</string> + <string name="comp_nightlies_unreachable">Nightlies kunde inte nås</string> + <string name="comp_stored">Sparade %1$s</string> + <string name="comp_download_failed">Nedladdningen misslyckades: %1$s</string> + <string name="drivers_pick_linux">Välj en drivrutin för Linux-körmiljön (-Linux-zip eller Android + Linux-paket)</string> + <string name="drivers_pick_android">Välj en skärmdrivrutin (AdrenoTools-zip eller Android + Linux-paket)</string> + <string name="drivers_pick_any">Välj en drivrutins-zip eller ett Android + Linux-paket</string> + <string name="main_pkg_problem">%1$s: %2$s</string> + <string name="main_removing_named">Tar bort %1$s…</string> + <string name="main_forgetting_named">Glömmer %1$s…</string> + <string name="main_pick_game_storage">Välj mappen för spellagring</string> + <string name="main_pick_decky_zip">Välj en Decky-plugin-zip</string> + <string name="main_storage_not_usable">Inte användbar: %1$s</string> + <string name="main_running_program">Program</string> + <string name="main_removing_runtime">Tar bort Linux-körmiljön</string> + <string name="main_settings_main_screen">Kunde inte öppna Inställningar på den nedre skärmen. Öppnade den på huvudskärmen i stället.</string> + <string name="main_open_dev_options">Öppna Utvecklaralternativ i Inställningar</string> + + <string name="upd_unknown_apps_failed">Det gick inte att öppna Androids inställning ”Installera okända appar”</string> + <string name="upd_couldnt_check">Kunde inte kontrollera: %1$s</string> + <string name="upd_no_download">Den här versionen har ingen nedladdning för den här kopian av DroidDeck</string> + <string name="upd_stop_first">Stoppa den pågående sessionen innan du uppdaterar</string> + <string name="upd_stage_downloading">Laddar ned</string> + <string name="upd_stage_installing">Installerar</string> + <string name="upd_stage_confirm">Väntar på att du bekräftar</string> + <string name="upd_failed">Uppdateringen misslyckades</string> + <string name="upd_not_offered">Android erbjöd inte att installera uppdateringen</string> + + <string name="decky_install_failed">Decky-installationen misslyckades: %1$s</string> + <string name="decky_plugin_starting">Startar import av plugin…</string> + <string name="decky_plugin_failed">Import av plugin misslyckades: %1$s</string> + <string name="decky_plugin_installed">Plugin installerad. Starta om Steam för att läsa in den.</string> + + <string name="drivers_not_checked">Inte kontrollerat än - tryck på uppdatera för att söka efter nya drivrutiner</string> + <string name="drivers_bundle_sets_display">Android + Linux: ställer in skärmdrivrutinen samtidigt</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: ställer in körmiljödrivrutinen samtidigt</string> + <string name="drivers_runtime_builtin">Turnip som är inbyggd i körmiljön</string> + <string name="drivers_system_vulkan">systemets Vulkan: ingen paketerad version för den här GPU:n</string> + <string name="drivers_bundled">%1$s (paketerad)</string> + <string name="drivers_manual">Manuell</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">Bara hälften är publicerad just nu</string> + <string name="drivers_pair_size">%1$.0f MB för båda</string> + <string name="drivers_none_to_set">Inga drivrutiner att ställa in: %1$s</string> + <string name="drivers_checking_latest">Söker efter de senaste drivrutinerna…</string> + <string name="drivers_up_to_date_check_failed">Senaste versionen (kunde inte kontrollera: %1$s)</string> + <string name="drivers_recommended_unpublished">De rekommenderade drivrutinerna är inte publicerade just nu</string> + <string name="drivers_downloading_pair">Laddar ned %1$s %2$s…</string> + <string name="drivers_updated_to">Uppdaterad till %1$s %2$s.</string> + <string name="drivers_updated_to_restart">Uppdaterad till %1$s %2$s. Skärmdrivrutinen tillämpas efter att DroidDeck har startats om.</string> + <string name="drivers_switched_to">Bytte till %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Bytte till %1$s %2$s. Skärmdrivrutinen tillämpas efter att DroidDeck har startats om.</string> + <string name="drivers_using">Använder %1$s %2$s.</string> + <string name="drivers_using_restart">Använder %1$s %2$s. Skärmdrivrutinen tillämpas efter att DroidDeck har startats om.</string> + <string name="drivers_not_a_driver">Inte en drivrutins-zip: varken en AdrenoTools-drivrutin, en -Linux-Turnip eller ett Android + Linux-paket</string> + <string name="drivers_import_failed">Importen misslyckades: %1$s</string> + <string name="drivers_imported_unnamed">Importerad drivrutin</string> + <string name="drivers_using_bundle">Använder %1$s som både körmiljö- och skärmdrivrutin.</string> + <string name="drivers_using_bundle_restart">Använder %1$s som både körmiljö- och skärmdrivrutin. Skärmdrivrutinen tillämpas efter att DroidDeck har startats om.</string> + <string name="drivers_deleted_bundle">Tog bort %1$s (båda drivrutinerna)</string> + <string name="drivers_deleted">Tog bort %1$s</string> + <string name="drivers_release_status">Senaste: %1$s · kontrollerad %2$s</string> + <string name="drivers_release_status_failed">Senaste: %1$s · %2$s gick inte att nå · kontrollerad %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">för %1$d min sedan</item> + <item quantity="other">för %1$d min sedan</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">för %1$d h sedan</item> + <item quantity="other">för %1$d h sedan</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">för %1$d dag sedan</item> + <item quantity="other">för %1$d dagar sedan</item> + </plurals> + <string name="drivers_checking_sources">Kontrollerar Banners-Turnip, WinNative och DroidDeck…</string> + <string name="drivers_check_failed">kontrollen misslyckades</string> + <string name="drivers_installed_pick_either">Installerad %1$s - välj den i endera listan för att ställa in båda drivrutinerna</string> + <string name="drivers_installed_pick">Installerad %1$s - välj den i menyn</string> + + <string name="session_wait_removal">Vänta tills borttagningen av körmiljön är klar</string> + <string name="session_title_game">Spel</string> + <string name="session_touch_auto_touchpad">pekplatta</string> + <string name="session_touch_auto_direct">direkt</string> + <string name="session_swap_failed">Bytet misslyckades: %1$s</string> + <string name="session_profile_save_failed">Kunde inte spara profilen: %1$s</string> + <string name="session_desktop_paused">Skrivbordet är pausat</string> + <string name="session_named_paused">%1$s är pausad</string> + <string name="session_program_paused">Programmet är pausat</string> + <string name="session_starting_desktop">Startar skrivbordet</string> + <string name="session_starting_named">Startar %1$s</string> + <string name="session_starting_program">Startar programmet</string> + <string name="session_no_logs">Inga loggar för den här sessionen.</string> + <string name="session_logs_archive_failed">Kunde inte skapa arkivet med sessionsloggar.</string> + <string name="session_logs_share_failed">Kunde inte dela sessionsloggarna.</string> + <string name="session_starting_session">Startar sessionen…</string> + <string name="session_runtime_downloading">laddar ned Linux-körmiljön</string> + <string name="session_runtime_downloading_mb">laddar ned Linux-körmiljön · %1$d av %2$d MB</string> + <string name="session_runtime_checking">kontrollerar Linux-körmiljön</string> + <string name="session_runtime_unpacking">packar upp Linux-körmiljön</string> + <string name="session_desktop_downloading">laddar ned skrivbordet</string> + <string name="session_desktop_downloading_mb">laddar ned skrivbordet · %1$d av %2$d MB</string> + <string name="session_desktop_checking">kontrollerar skrivbordet</string> + <string name="session_desktop_unpacking">packar upp skrivbordet</string> + <string name="session_proton_downloading">laddar ned Proton Experimental (ARM64)</string> + <string name="session_proton_downloading_mb">laddar ned Proton Experimental (ARM64) · %1$d av %2$d MB</string> + <string name="session_proton_checking">kontrollerar Proton Experimental (ARM64)</string> + <string name="session_proton_unpacking">packar upp Proton Experimental (ARM64)</string> + <string name="session_runtime_catalog_unreachable">Kunde inte nå körmiljökatalogen. Kontrollera anslutningen och tryck på Spela igen.</string> + <string name="session_runtime_install_failed">Linux-körmiljön installerades inte. Kontrollera körmiljön i Konfiguration och tryck på Spela igen.</string> + <string name="session_desktop_install_failed">Skrivbordet installerades inte (%1$s). Kontrollera anslutningen och tryck på Skrivbord igen.</string> + <string name="session_ended_named">%1$s stoppades oväntat. Dela loggarna i en felrapport, eller försök igen.</string> + <string name="session_ended_program">Programmet stoppades oväntat. Dela loggarna i en felrapport, eller försök igen.</string> + <string name="session_ended_desktop">Skrivbordet stoppades oväntat. Dela loggarna i en felrapport, eller försök igen.</string> + <string name="session_ended_detail">Avslutningsstatus %1$d · %2$s</string> + <string name="session_ended_no_log">ingen logg</string> + <plurals name="session_hint_enosys"> + <item quantity="one">Loggen visar %1$d ”Function not implemented”-fel: proots seccomp-acceleration misslyckas med en hjälpprocess på den här enheten. Prova %2$s → ”%3$s” (eller ”%4$s”) och starta igen.</item> + <item quantity="other">Loggen visar %1$d ”Function not implemented”-fel: proots seccomp-acceleration misslyckas med en hjälpprocess på den här enheten. Prova %2$s → ”%3$s” (eller ”%4$s”) och starta igen.</item> + </plurals> + + <string name="mode_decky_plugins">Plugins</string> + <string name="mode_decky_plugins_hint">Installera en plugin-zip för nästa Steam-session.</string> + <string name="mode_decky_plugin_downloading">Laddar ned…</string> + <string name="mode_decky_plugin_install_zip">Installera från zip</string> + <string name="store_cat_games">Spel</string> + <string name="store_cat_audio_video">Ljud & video</string> + <string name="store_cat_graphics">Grafik</string> + <string name="store_cat_internet">Internet</string> + <string name="store_cat_office">Kontor</string> + <string name="store_cat_developer">Utveckling</string> + <string name="store_cat_education">Utbildning</string> + <string name="store_cat_science">Vetenskap</string> + <string name="store_cat_system">System</string> + <string name="store_cat_utilities">Verktyg</string> + <string name="store_section_popular">Populärt</string> + <string name="store_section_trending">Trendande</string> + <string name="store_section_new">Nytt</string> + <string name="store_section_updated">Uppdaterat</string> + <string name="store_section_loading">läser in…</string> + <string name="store_flatpak_setup">Flatpak-konfiguration</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Kunde inte kontrollera Flathub efter uppdateringar</string> + <string name="content_rom_installed">installerat</string> + <string name="content_rom_folder">mapp</string> + <string name="theme_graphite">Grafit</string> + <string name="theme_graphite_detail">Grafitgrå ytor, med den blå färgen från logotypens klot för allt som är markerat, i fokus eller nedtryckt.</string> + <string name="theme_paper">Papper på svart</string> + <string name="theme_paper_detail">Vit är primärfärgen, ikonens blå är signalfärgen för linjer, ringar och punkter.</string> + <string name="theme_icon_blue">Ikonblå</string> + <string name="theme_icon_blue_detail">Svart bakgrund, vit text, den blå färgen för allt som är markerat, i fokus eller nedtryckt.</string> + <string name="theme_electric">Elektrisk marinblå</string> + <string name="theme_electric_detail">Den blå färgen klarare, på ytor som lutar åt marinblått i stället för grått.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Space</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <string name="game_sync_err_runtime">Installera körmiljön först</string> + <string name="game_sync_err_steam_library">Steam-biblioteket är inte tillgängligt</string> + <string name="game_sync_err_previous_sd">Det tidigare SD-biblioteket är inte tillgängligt</string> + <string name="game_sync_err_game_storage">Spellagringen är inte tillgänglig</string> + <string name="game_sync_err_added_games">Mappen för tillagda spel är inte tillgänglig</string> + <string name="game_sync_err_manifests">Steam-manifesten är inte tillgängliga</string> + <string name="content_system_many">Många system</string> + <string name="fm_drive_internal">Internt</string> + <string name="fm_drive_linux">Linux-system</string> + <string name="fm_drive_sd">SD-kort</string> + <string name="fm_drive_storage">Lagring</string> + <string name="fm_select">Markera</string> + <string name="fm_properties">Egenskaper</string> + <string name="fm_favorite_add">Lägg till i Favoriter</string> + <string name="fm_favorite_remove">Ta bort från Favoriter</string> + <string name="fm_rename">Byt namn</string> + <string name="fm_copy">Kopiera</string> + <string name="fm_cut">Klipp ut</string> + <string name="fm_actions">Åtgärder</string> + <string name="fm_favorites">Favoriter</string> + <string name="fm_pin_current">Fäst aktuell mapp</string> + <string name="fm_no_favorites">Inga favoriter än - fäst en mapp med dess ⋮-meny för att snabbt komma tillbaka hit.</string> + <string name="fm_unpin_favorite">Ta bort från favoriter</string> + <string name="fm_size_zero">0 byte</string> + <string name="fm_unit_bytes">byte</string> + <string name="fm_prop_location">Plats</string> + <string name="fm_prop_type">Typ</string> + <string name="fm_prop_size">Storlek</string> + <string name="fm_prop_modified">Ändrad</string> + <string name="fm_type_folder">Mapp</string> + <string name="fm_type_file">Fil</string> + <string name="fm_type_ext_file">%1$s-fil</string> + <string name="fm_read_only">Skrivskyddad</string> + <string name="fm_read_only_hint">Förhindrar att spel (och Wine) skriver över eller tar bort filen.</string> + <string name="fm_read_only_failed">Kunde inte ändra Skrivskyddad</string> + <string name="fm_hidden">Dold</string> + <string name="fm_hidden_unsupported">Den här lagringen kan inte spara attributet Dold.</string> + <string name="fm_hidden_hint">Markerar filen som dold i Windows (Wines dolda attribut).</string> + <string name="fm_hidden_failed">Kunde inte ändra Dold på den här lagringen</string> + <string name="fm_done">Klar</string> + <string name="fm_op_deleting">Tar bort...</string> + <string name="fm_delete_failed">Borttagning misslyckades</string> + <string name="fm_op_moving">Flyttar...</string> + <string name="fm_op_copying">Kopierar...</string> + <string name="fm_op_moving_item">Flyttar - %1$s</string> + <string name="fm_op_copying_item">Kopierar - %1$s</string> + <string name="fm_op_moving_batch">Flyttar %1$d/%2$d - %3$s</string> + <string name="fm_op_copying_batch">Kopierar %1$d/%2$d - %3$s</string> + <string name="fm_paste_failed">%1$d klara, %2$d misslyckades</string> + <string name="fm_paste_skipped">%1$d klara, %2$d hoppades över</string> + <plurals name="fm_paste_moved"> + <item quantity="one">%1$d objekt flyttat</item> + <item quantity="other">%1$d objekt flyttade</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">%1$d objekt kopierat</item> + <item quantity="other">%1$d objekt kopierade</item> + </plurals> + <string name="fm_already_exists">”%1$s” finns redan</string> + <string name="fm_op_renaming">Byter namn...</string> + <string name="fm_rename_failed">Namnbytet misslyckades</string> + <string name="fm_op_creating_folder">Skapar mapp...</string> + <string name="fm_create_folder_failed">Kunde inte skapa mappen</string> + <string name="fm_new_folder">Ny mapp</string> + <string name="fm_folder_name">Mappnamn</string> + <string name="fm_create">Skapa</string> + <string name="fm_new_name">Nytt namn</string> + <string name="fm_delete_title">Ta bort?</string> + <string name="fm_delete_text">Ta bort ”%1$s” permanent?</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">Ta bort %1$d objekt?</item> + <item quantity="other">Ta bort %1$d objekt?</item> + </plurals> + <string name="fm_cannot_undo">Det går inte att ångra.</string> + <plurals name="fm_and_more"> + <item quantity="one">…och %1$d till</item> + <item quantity="other">…och %1$d till</item> + </plurals> + <string name="fm_op_deleting_batch">Tar bort %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">%1$d kunde inte tas bort</item> + <item quantity="other">%1$d kunde inte tas bort</item> + </plurals> + <string name="fm_conflict_merge_hint">Sammanfoga lägger till och ersätter filer i den befintliga mappen.</string> + <string name="fm_conflict_overwrite_hint">Skriv över ersätter den befintliga filen.</string> + <string name="fm_conflict_apply_all">Tillämpa på alla konflikter</string> + <string name="fm_conflict_merge">Sammanfoga</string> + <string name="fm_conflict_overwrite">Skriv över</string> + <string name="fm_conflict_keep_both">Behåll båda</string> + <string name="fm_conflict_skip">Hoppa över</string> + <string name="fm_select_this_folder">Välj den här mappen</string> + <string name="fm_back">Tillbaka</string> + <string name="fm_drive_not_readable">%1$s är monterad men inte läsbar just nu</string> + <string name="fm_list_view">Listvy</string> + <string name="fm_grid_view">Rutnätsvy</string> + <string name="fm_search">Sök</string> + <string name="fm_sort">Sortera</string> + <string name="fm_sort_name">Namn</string> + <string name="fm_sort_date">Ändringsdatum</string> + <string name="fm_rows_comfortable">Bekväma rader</string> + <string name="fm_rows_compact">Kompakta rader</string> + <string name="fm_hide_hidden">Dölj dolda filer</string> + <string name="fm_show_hidden">Visa dolda filer</string> + <string name="fm_hide_favorites">Dölj favoriter</string> + <string name="fm_show_favorites">Visa favoriter</string> + <string name="fm_filter_folder">Filtrera den här mappen</string> + <string name="fm_free_space">%1$s ledigt</string> + <string name="fm_selected_count">%1$d markerade</string> + <string name="fm_select_none">Inga</string> + <string name="fm_select_all">Alla</string> + <string name="fm_paste">Klistra in</string> + <plurals name="fm_items"> + <item quantity="one">%1$d objekt</item> + <item quantity="other">%1$d objekt</item> + </plurals> + <string name="fm_paste_here">Klistra in %1$s här</string> + <string name="fm_paste_move_here">Klistra in %1$s (flytta) här</string> + <string name="fm_rail_title">Filer</string> + <string name="fm_rail_storage">LAGRING</string> + <string name="fm_rail_quick">SNABBT</string> + <string name="fm_rail_favorites">FAVORITER</string> + <string name="fm_rail_downloads">Hämtningar</string> + <string name="fm_rail_downloads_short">Hämtn.</string> + <string name="fm_rail_roms">ROM</string> + <string name="fm_rail_session_logs">Sessionsloggar</string> + <string name="fm_rail_pictures">Bilder</string> + <string name="fm_favorite_added">Lade till ”%1$s” i Favoriter</string> + <string name="fm_favorite_removed">Tog bort ”%1$s” från Favoriter</string> + <string name="fm_empty_directory">Tom mapp</string> + <string name="fm_up_one_level">Upp en nivå</string> + <string name="fm_choose_folder">Välj en mapp</string> + <string name="fm_choose_file">Välj en fil</string> + <string name="ctrl_tint_steam_blue">Steamblå</string> + <string name="ctrl_tint_sky">Himmelsblå</string> + <string name="ctrl_tint_white">Vit</string> + <string name="ctrl_tint_violet">Lila</string> + <string name="ctrl_tint_green">Grön</string> + <string name="ctrl_tint_amber">Amber</string> + <string name="ctrl_tint_red">Röd</string> + <string name="ctrl_tint_pink">Rosa</string> + <string name="ctrl_button_a">A-knappen</string> + <string name="ctrl_button_b">B-knappen</string> + <string name="ctrl_button_x">X-knappen</string> + <string name="ctrl_button_y">Y-knappen</string> + <string name="ctrl_button_lb">Vänster kantknapp</string> + <string name="ctrl_button_rb">Höger kantknapp</string> + <string name="ctrl_button_lt">Vänster avtryckare</string> + <string name="ctrl_button_rt">Höger avtryckare</string> + <string name="ctrl_button_view">Visa-knappen</string> + <string name="ctrl_button_menu">Meny-knappen</string> + <string name="ctrl_target_view">Visa</string> + <string name="ctrl_target_menu">Meny</string> + <string name="ctrl_target_up">D-pad upp</string> + <string name="ctrl_target_down">D-pad ned</string> + <string name="ctrl_target_left">D-pad vänster</string> + <string name="ctrl_target_right">D-pad höger</string> + <string name="ctrl_target_hidden">Dold</string> + <string name="ctrl_editor_drag">Dra för att flytta</string> + <string name="ctrl_editor_snap">Fäst</string> + <string name="ctrl_editor_save">Spara</string> + <string name="second_mode_none">Inget</string> + <string name="second_mode_keyboard_trackpad">Tangentbord + styrplatta</string> + <string name="second_mode_terminal">Terminal</string> + <string name="second_mode_deck_controls">Deck-grepp + styrplattor</string> + + <string name="rtinst_removing">Tar bort Linux-körmiljön</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">Tar bort Linux-körmiljön · %1$d post</item> + <item quantity="other">Tar bort Linux-körmiljön · %1$d poster</item> + </plurals> + <string name="rtinst_removed">Linux-körmiljön har tagits bort</string> + <string name="rtinst_removal_failed">Kunde inte slutföra borttagningen av Linux-körmiljön. Försök ta bort den igen i Konfiguration.</string> + <string name="rtinst_removing_leftovers">Tar bort rester av körmiljön</string> + <string name="rtinst_downloading">Laddar ned</string> + <string name="rtinst_verifying">Verifierar</string> + <string name="rtinst_extracting">Packar upp</string> + <string name="rtinst_channel_description">Visas medan Linux-körmiljön installeras eller tas bort</string> + <string name="rtinst_title_installing">Installerar Linux-körmiljön</string> + <string name="rtinst_title_removing">Tar bort Linux-körmiljön</string> + + <string name="deskpkg_runtime_missing">Linux-körmiljön är inte installerad</string> + <string name="deskpkg_no_checksum">Katalogen har ingen kontrollsumma för %1$s</string> + <string name="deskpkg_checksum_mismatch">Kontrollsumman stämmer inte - ingenting ändrades</string> + <string name="deskpkg_installing">Installerar %1$s</string> + <string name="deskpkg_place_failed">Kunde inte placera AppImage-filen</string> + <string name="deskpkg_extract_failed">Uppackningen misslyckades</string> + <string name="deskpkg_install_failed">Installationen misslyckades</string> + + <string name="flatpakmgr_setup_failed">Flatpak-konfigurationen misslyckades (avslutningskod %1$d)</string> + <string name="flatpakmgr_setup_unfinished">Flatpak-konfigurationen slutfördes inte</string> + <string name="flatpakmgr_busy">En annan butiksåtgärd pågår</string> + <string name="flatpakmgr_not_ready">Konfigurera Flatpak först</string> + <string name="flatpakmgr_starting">Startar</string> + <string name="flatpakmgr_exit_status">Flatpak avslutades med status %1$d</string> + <string name="flatpakmgr_installing">Installerar %1$s</string> + <string name="flatpakmgr_updating">Uppdaterar %1$s</string> + <string name="flatpakmgr_removing">Tar bort %1$s</string> + <string name="flatpakmgr_installing_step">Installerar %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_updating_step">Uppdaterar %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_removing_step">Tar bort %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_ref_translations">översättningar</string> + <string name="flatpakmgr_ref_graphics_drivers">grafikdrivrutiner</string> + <string name="flatpakmgr_ref_media_codecs">mediekodekar</string> + <string name="flatpakmgr_ref_runtime">%1$s-körmiljö</string> + <string name="flatpakmgr_ref_runtime_branch">%1$s-körmiljö %2$s</string> + + <string name="appimg_too_small">Filen är för liten för att vara en AppImage</string> + <string name="appimg_unreadable">Filen kan inte läsas: %1$s</string> + <string name="appimg_not_linux">Det här är inte en AppImage (det är inte ett Linux-program)</string> + <string name="appimg_wrong_arch">Den här AppImage-filen är byggd för varken ARM64 (aarch64) eller x86-datorer</string> + <string name="appimg_not_appimage">Det här är ett Linux-program men inte en AppImage</string> + <string name="appimg_wrong_type">Endast typ 2-AppImages kan importeras (den här är typ %1$d)</string> + <string name="appimg_no_space_import">Inte tillräckligt med ledigt utrymme: import kräver cirka %1$s</string> + <string name="appimg_no_space_update">Inte tillräckligt med ledigt utrymme: uppdatering kräver cirka %1$s</string> + <string name="appimg_import_failed">Importen misslyckades</string> + <string name="appimg_gone">Den AppImage-filen finns inte längre</string> + <string name="appimg_update_failed">Uppdateringen misslyckades</string> + <string name="appimg_extracting">Packar upp %1$s</string> + <string name="appimg_extract_failed">AppImage-filen kunde inte packas upp</string> + <string name="appimg_extract_failed_detail">AppImage-filen kunde inte packas upp: %1$s</string> + + <string name="deckymgr_stop_session_loader">Stoppa den aktiva sessionen innan du installerar Decky Loader</string> + <string name="deckymgr_download_failed">Nedladdningen av Decky Loader misslyckades</string> + <string name="deckymgr_size_mismatch">Storleken på Decky Loader matchade inte utgåvans metadata</string> + <string name="deckymgr_checksum_download_failed">Kunde inte ladda ned utgåvans kontrollsumma</string> + <string name="deckymgr_checksum_missing">Utgåvans kontrollsumma saknas</string> + <string name="deckymgr_checksum_invalid">Utgåvans kontrollsumma är ogiltig</string> + <string name="deckymgr_checksum_mismatch">Kontrollsumman för Decky Loader stämmer inte</string> + <string name="deckymgr_not_elf">Utgåvans fil är inte en kompatibel 64-bitars körbar Linux-fil</string> + <string name="deckymgr_session_started_loader">En session startade under nedladdningen. Stoppa den innan du installerar Decky Loader</string> + <string name="deckymgr_loader_not_executable">Kunde inte markera PluginLoader som körbar</string> + <string name="deckymgr_cef_failed">Kunde inte aktivera Steams CEF-fjärrfelsökning</string> + <string name="deckymgr_stage_failed">Kunde inte förbereda PluginLoader</string> + <string name="deckymgr_loader_preserve_failed">Kunde inte bevara den befintliga PluginLoader</string> + <string name="deckymgr_loader_activate_failed">Kunde inte aktivera den nya PluginLoader</string> + <string name="deckymgr_stop_session_plugin">Stoppa den aktiva sessionen innan du installerar en plugin</string> + <string name="deckymgr_loader_required">Installera Decky Loader innan du importerar en plugin</string> + <string name="deckymgr_zip_unreadable">Kunde inte läsa den valda zip-filen</string> + <string name="deckymgr_plugins_dir_failed">Kunde inte skapa mappen för Decky-plugins</string> + <string name="deckymgr_extracting_zip">Packar upp plugin-zip</string> + <string name="deckymgr_zip_one_folder">Plugin-zipen måste innehålla en plugin-mapp på toppnivå</string> + <string name="deckymgr_zip_missing_files">Plugin-zipen saknar plugin.json, package.json eller dist/index.js</string> + <string name="deckymgr_plugin_name_missing">Plugin-namnet saknas</string> + <string name="deckymgr_session_started_plugin">En session startade under installationen. Stoppa den innan du installerar en plugin</string> + <string name="deckymgr_plugin_installed">Plugin installerad · starta om Steam för att läsa in den</string> + <string name="deckymgr_plugin_install_failed">Installationen av plugin-zipen misslyckades</string> + <string name="deckymgr_zip_too_many">Plugin-zipen innehåller för många poster</string> + <string name="deckymgr_zip_invalid_path">Plugin-zipen innehåller en ogiltig sökväg</string> + <string name="deckymgr_zip_duplicate_paths">Plugin-zipen innehåller dubbletter av sökvägar</string> + <string name="deckymgr_zip_unsafe_path">Plugin-zipen innehåller en osäker sökväg</string> + <string name="deckymgr_folder_failed">Kunde inte skapa plugin-mappen</string> + <string name="deckymgr_zip_too_large">Plugin-zipen packas upp till mer än gränsen på 1 GiB</string> + <string name="deckymgr_bundled_not_executable">Kunde inte markera den medföljande plugin-binären som körbar</string> + <string name="deckymgr_remote_entry_invalid">Ogiltig remote_binary-post</string> + <string name="deckymgr_remote_name_invalid">Ogiltigt namn på fjärrbinär</string> + <string name="deckymgr_remote_url_https">URL:en för fjärrbinären måste använda HTTPS</string> + <string name="deckymgr_remote_hash_invalid">Fjärrbinären %1$s har en ogiltig SHA-256-hash</string> + <string name="deckymgr_bin_dir_failed">Kunde inte skapa bin-mappen för pluginen</string> + <string name="deckymgr_downloading_binary">Laddar ned plugin-binär · %1$s</string> + <string name="deckymgr_binary_download_failed">Kunde inte ladda ned plugin-binären %1$s</string> + <string name="deckymgr_binary_checksum_mismatch">Kontrollsumman för plugin-binären stämmer inte · %1$s</string> + <string name="deckymgr_binary_not_executable">Kunde inte markera plugin-binären som körbar · %1$s</string> + <string name="deckymgr_binary_replace_failed">Kunde inte ersätta plugin-binären · %1$s</string> + <string name="deckymgr_binary_install_failed">Kunde inte installera plugin-binären · %1$s</string> + <string name="deckymgr_folder_taken">Plugin-mappen tillhör en annan plugin</string> + <string name="deckymgr_preserve_failed">Kunde inte bevara den befintliga pluginen</string> + <string name="deckymgr_activate_failed">Kunde inte aktivera den importerade pluginen</string> + <string name="deckymgr_settings_failed">Kunde inte uppdatera inställningarna för Decky-plugin</string> + <string name="deckymgr_restore_failed">Kunde inte återställa den befintliga pluginen</string> + + <string name="adbfix_save_failed">Kunde inte spara identiteten för trådlös felsökning</string> + <string name="adbfix_command_failed">ADB-kommandot misslyckades (%1$d)</string> + <string name="adbfix_not_confirmed">Android bekräftade inte att gränsen för underprocesser ändrades: %1$s</string> + <string name="adbfix_no_pairing">Ingen parkoppling för trådlös felsökning är sparad</string> + <string name="adbfix_enter_address">Ange den aktuella IP-adressen och porten för trådlös felsökning</string> + + <string name="adbpair_timed_out">Tidsgränsen överskreds när appen väntade på popup-fönstret för parkoppling. Försök igen från DroidDeck.</string> + <string name="adbpair_discovery_failed_code">Kunde inte söka efter popup-fönstret för parkoppling (%1$d). Kontrollera att Wi-Fi är på.</string> + <string name="adbpair_discovery_failed">Kunde inte söka efter popup-fönstret för parkoppling: %1$s</string> + <string name="adbpair_six_digits">Ange alla 6 siffror i koden</string> + <string name="adbpair_pairing">Parkopplar…</string> + <string name="adbpair_code_rejected">Den koden fungerade inte. Öppna popup-fönstret för parkoppling igen och ange den nya koden.</string> + <string name="adbpair_connecting">Parkopplad. Ansluter…</string> + <string name="adbpair_port_not_found">Parkopplad, men porten för trådlös felsökning hittades inte. Låt trådlös felsökning vara på och försök igen.</string> + <string name="adbpair_applying">Tillämpar inställningen…</string> + <string name="adbpair_command_failed">Kommandot för trådlös felsökning misslyckades</string> + <string name="adbpair_channel_name">Konfiguration av trådlös felsökning</string> + <string name="adbpair_channel_description">Frågar efter parkopplingskoden för trådlös felsökning medan Inställningar är öppen</string> + <string name="adbpair_title_pair">Parkoppla trådlös felsökning</string> + <string name="adbpair_waiting_text">Slå på trådlös felsökning och tryck sedan på ”Parkoppla enhet med parkopplingskod”.</string> + <string name="adbpair_waiting_long">Slå på trådlös felsökning och tryck sedan på ”Parkoppla enhet med parkopplingskod”. Stanna kvar i Inställningar: koden skrivs in i den här aviseringen.</string> + <string name="adbpair_code_label">6-siffrig parkopplingskod</string> + <string name="adbpair_code_hint">Tryck på Ange kod och skriv de 6 siffrorna som visas i Inställningar.</string> + <string name="adbpair_title_code">Ange Wi-Fi-parkopplingskoden</string> + <string name="adbpair_title_retry">Försök med koden igen</string> + <string name="adbpair_enter_code">Ange kod</string> + <string name="adbpair_title_working">Konfigurerar Steam</string> + <string name="adbpair_title_done">Steam är redo</string> + <string name="adbpair_done_text">Tryck för att gå tillbaka till DroidDeck. Du kan stänga av trådlös felsökning nu.</string> + <string name="adbpair_title_failed">Konfigurationen av trådlös felsökning avbröts</string> + + <string name="selfinst_http">Nedladdningen svarade HTTP %1$d</string> + <string name="selfinst_size_mismatch">Nedladdningens storlek matchade inte den publicerade APK-filen och förkastades - försök igen</string> + <string name="selfinst_checksum_mismatch">Nedladdningen matchade inte sin kontrollsumma och förkastades - försök igen</string> + <string name="selfinst_wrong_package">Uppdateringen är för Android-paketet %1$s, inte %2$s</string> + <string name="selfinst_metadata_signer">Uppdateringens metadata anger inte DroidDecks signeringsnyckel för utgåvan</string> + <string name="selfinst_unreadable">Android kunde inte läsa den nedladdade APK-filen</string> + <string name="selfinst_apk_wrong_package">Den nedladdade APK-filen är för %1$s, inte %2$s</string> + <string name="selfinst_version_mismatch">Den nedladdade APK-filen anger versionCode %1$d, inte den publicerade %2$d</string> + <string name="selfinst_downgrade">Android installerar inte versionCode %1$d över den installerade %2$d</string> + <string name="selfinst_no_signing">Den nedladdade APK-filen har ingen signeringsinformation</string> + <string name="selfinst_wrong_signer">Den nedladdade APK-filen är inte signerad med DroidDecks nyckel för utgåvan</string> + <string name="appupd_http">Uppdateringskatalogen svarade HTTP %1$d</string> + + <string name="loadstate_starting">Startar sessionen…</string> + <string name="loadstate_elapsed">%1$d:%2$02d förflutit · arbetar fortfarande</string> + <string name="loadstate_steam_update">Laddar ned uppdateringen av Steam-klienten · %1$d %%</string> + <string name="loadstate_steam_client">Laddar ned Steam-klienten · %1$s</string> + + <string name="pextra_busy">En installation av ett kompatibilitetsverktyg pågår redan</string> + <string name="pextra_stop_session">Stoppa den aktiva sessionen innan du installerar kompatibilitetsverktyg</string> + <string name="pextra_no_release">Kunde inte hitta en ARM64-utgåva av %1$s</string> + <string name="pextra_no_space">Minst 4 GB ledigt utrymme plus nedladdningens storlek krävs för att installera %1$s</string> + <string name="pextra_downloading">Laddar ned %1$s %2$s</string> + <string name="pextra_retrying">Försöker ladda ned igen · försök %1$d av %2$d</string> + <string name="pextra_interrupted">Nedladdningen avbröts</string> + <string name="pextra_download_failed">Nedladdningen misslyckades. Den delvisa filen behålls så att ett nytt försök kan återupptas</string> + <string name="pextra_too_big">Nedladdningen översteg utgåvans storlek och togs bort</string> + <string name="pextra_incomplete">Nedladdningen var ofullständig. Den delvisa filen behålls för ett återupptagbart försök</string> + <string name="pextra_verifying">Verifierar nedladdningen</string> + <string name="pextra_checksum_unreadable">Kunde inte läsa utgåvans kontrollsumma. Försök igen</string> + <string name="pextra_no_checksum">%1$s %2$s publicerar ingen kontrollsumma. Inget installerades</string> + <string name="pextra_checksum_mismatch">Kontrollsumman stämmer inte. Nedladdningen togs bort</string> + <string name="pextra_session_started">En session startade under nedladdningen. Stoppa den innan du installerar kompatibilitetsverktyg</string> + <string name="pextra_installing">Installerar %1$s</string> + <string name="pextra_install_failed">Installationen av %1$s misslyckades (avslutningskod %2$d)</string> + <string name="pextra_could_not_install">Kunde inte installera %1$s</string> + <string name="esync_fetching">Hämtar droiddeck-esync-paket</string> + + <string name="cmgr_fex_linux_helpers">med Linux-hjälpprogram</string> + <string name="cmgr_fex_dlls_only">Endast DLL-filer</string> + <string name="cmgr_original_proton_updated">Original (Proton har uppdaterats sedan senaste bytet)</string> + <string name="cmgr_changed_outside">Ändrad utanför Komponenter</string> + <string name="cmgr_not_present">saknas</string> + <string name="cmgr_original_version">Original %1$s</string> + <string name="cmgr_original_from">Original från %1$s</string> + <string name="cmgr_swap_queued">%1$s %2$s läggs in i %3$s när spelet som körs på den stängs.</string> + <string name="cmgr_swap_done">%1$s %2$s finns nu i %3$s. Det börjar gälla nästa gång ett spel startar.</string> + <string name="cmgr_original_missing">Det originalpaketet är inte sparat</string> + <string name="cmgr_restore_queued">%1$s i %2$s återgår till originalet när spelet som körs på den stängs.</string> + <string name="cmgr_restore_done">%1$s i %2$s återställdes från sitt %3$s-original.</string> + <string name="cmgr_package_in_use">%1$s används i %2$s. Byt den Protonen till något annat först.</string> + <string name="cmgr_package_not_stored">%1$s är inte sparad</string> + <string name="cmgr_deleted">Tog bort %1$s.</string> + <string name="cmgr_original_kept">Den installerade versionens original behålls: det är vägen tillbaka.</string> + <string name="cmgr_not_stored">Inte sparad</string> + <string name="cmgr_deleted_original">Tog bort %1$s-originalet av %2$s.</string> + <string name="cmgr_nightlies_only">Nedladdningar kommer bara från Nightlies-utgåvorna</string> + <string name="cmgr_list_predates_checksums">Den här paketlistan är från innan kontrollsummor fanns - uppdatera den och försök igen</string> + <string name="cmgr_checksum_mismatch">Kontrollsumman stämmer inte</string> + + <string name="sprefs_upscaler_linear">Linjär</string> + <string name="sprefs_upscaler_nearest">Närmaste</string> + <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (kvalitet)</string> + <string name="sprefs_upscaler_sharpen">Endast skärpa</string> + <string name="sprefs_lod_bias_auto">Auto (matcha skalning)</string> + + <string name="look_off_desc">Inget tillämpat.</string> + <string name="look_game_clarity">Spelklarhet</string> + <string name="look_game_clarity_desc">Skärpa med en nypa kontrast - vardagsvalet.</string> + <string name="look_vivid">Levande</string> + <string name="look_vivid_desc">Färgen pressad hårt. Deband är på så att himlar inte strimmar.</string> + <string name="look_cinematic">Cinematisk</string> + <string name="look_cinematic_desc">Djupare kontrast, kallare färg, kanter jämnade ut.</string> + <string name="look_competitive">Tävling</string> + <string name="look_competitive_desc">Skuggor lyfta och tung skärpa - för att se motståndare snabbare.</string> + <string name="look_adaptive_sharpen">Adaptiv skärpa</string> + <string name="look_adaptive_sharpen_desc">Endast skärpa, färgen orörd.</string> + <string name="look_filmic">Filmisk</string> + <string name="look_filmic_desc">Filmlik kurva, lätt dämpad.</string> + <string name="look_arcade">Arkad</string> + <string name="look_arcade_desc">Högljudd och kraftfull.</string> + <string name="look_retro_crt">Retro-CRT</string> + <string name="look_retro_crt_desc">Simulerar både CRT-röret och den analoga signalen tillsammans.</string> + <string name="look_upscale_sharp">Skarp uppskalning</string> + <string name="look_upscale_sharp_desc">För körning under skärmens upplösning.</string> + <string name="look_pixel_clean">Pixelrent</string> + <string name="look_pixel_clean_desc">Skarpa pixlar för 2D och äldre titlar.</string> + <string name="look_anime_edge">Anime-kant</string> + <string name="look_anime_edge_desc">Tecknade konturer plus skärpa.</string> + + <string name="gpuinfo_not_adreno">Inte en Adreno-GPU</string> + <string name="gpuinfo_supported">Stöds</string> + <string name="gpuinfo_experimental">Experimentell: dess drivrutiner är testversioner</string> + <string name="gpuinfo_below_tested">Under testad maskinvara (Adreno 725 och nyare): det kan hända att det inte fungerar</string> + <string name="gpuinfo_unsupported">Stöds inte: DroidDeck behöver en Adreno-GPU (Snapdragon)</string> + + <string name="turnip_rate_limited">GitHubs hastighetsgräns nåddes - försök igen senare</string> + <string name="turnip_github_http">GitHub svarade HTTP %1$d</string> + <string name="turnip_download_http">nedladdningen svarade HTTP %1$d</string> + <string name="turnip_list_predates_checksums">Den här drivrutinslistan är från innan kontrollsummor fanns - uppdatera den och försök igen</string> + <string name="turnip_checksum_mismatch">Kontrollsumman stämmer inte - nedladdningen förkastades</string> + <string name="turnip_not_adrenotools">Inte en AdrenoTools-drivrutin: %1$s</string> + <string name="turnip_reject_no_meta">ingen meta.json</string> + <string name="turnip_reject_meta_unreadable">meta.json kan inte läsas (%1$s)</string> + <string name="turnip_reject_linux_driver">det här är en drivrutin för Linux-körmiljön (importera den under ”Linux-körmiljödrivrutin”)</string> + <string name="turnip_reject_wayland">det här är en spel-drivrutin för Wayland, som den här appen inte har någon användning för</string> + <string name="turnip_reject_no_library_name">meta.json anger inget libraryName, så ingenting kunde skickas till AdrenoTools</string> + <string name="turnip_reject_library_missing">meta.json anger %1$s, som inte finns i zip-filen</string> + <string name="turnip_reject_not_aarch64">%1$s är inte ett 64-bitars delat ELF-bibliotek för AArch64</string> + <string name="turnip_reject_glibc">%1$s länkar mot glibc - en ”-Linux”-Turnip, som hör hemma under ”Linux-körmiljödrivrutin”</string> + <string name="lvd_no_library">Ingen libvulkan_freedreno*.so i den här zip-filen. En Android-zip (AdrenoTools) eller -Wayland-Turnip-zip är inte en Linux-körmiljödrivrutin.</string> + <string name="lvd_not_aarch64">%1$s är inte ett 64-bitars delat ELF-bibliotek för AArch64.</string> + <string name="lvd_not_glibc">%1$s är inte en glibc-drivrutin - den länkar mot Androids libc. Linux-körmiljön behöver en ”-Linux”-zip; en vanlig eller ”-Wayland”-Turnip kan inte läsas in av Steam-klienten alls.</string> + <string name="bundle_no_manifest">Inte ett drivrutinspaket: ingen manifest.json som anger en android- och en linux-drivrutin</string> + <string name="bundle_entry_outside">Inte ett drivrutinspaket: %1$s pekar utanför det</string> + <string name="bundle_too_large">Inte ett drivrutinspaket: det packas upp till mer än %1$d MB</string> + + <string name="gstore_sd_card">SD-kort</string> + <string name="gstore_folder">Mapp</string> + + <string name="logshare_chooser">Dela sessionsloggar</string> + + <string name="hdr_needs_android_10">kräver Android 10 eller senare (skärmlager med en färgrymd)</string> + <string name="hdr_none">den här skärmen rapporterar inget HDR-stöd</string> + <string name="hdr_not_hdr10">den här skärmen stöder %1$s, inte HDR10</string> + + <string name="gpu_this_gpu">den här GPU:n</string> + <string name="phantom_title_on">Gränsen för underprocesser är på</string> + <string name="phantom_title_unset">Gränsen för underprocesser är inte inställd</string> + <string name="phantom_title_unreadable">Gränsen för underprocesser kunde inte kontrolleras</string> + <string name="phantom_title_off">Gränsen för underprocesser är av</string> + <string name="phantom_title_not_required">Gränsen för underprocesser krävs inte</string> + <string name="phantom_instructions_fix">%1$s Om det inte är möjligt ansluter du den här enheten till en dator med ADB och kör kommandot nedan. Kom tillbaka hit och kontrollera igen.</string> + <string name="phantom_instructions_off">Steam-sessioner kan starta.</string> + <string name="phantom_instructions_not_applicable">Den här Android-versionen använder inte den här gränsen.</string> + <string name="phantom_fix_toggle">Slå på ”Inaktivera begränsningar för underprocesser” i Utvecklaralternativ, och låt Utvecklaralternativ vara kvar på efteråt.</string> + <string name="phantom_fix_no_toggle">Den här Android-versionen har ingen växel för det. Använd trådlös felsökning för att ändra det från den här enheten.</string> + <string name="upd_block_no_apk">Den här versionen har ingen nedladdning för den här kopian av DroidDeck.</string> + <string name="upd_block_version_code">Android installerar inte den över den här versionen eftersom dess versionCode %1$d är lägre än den installerade %2$d.</string> + <string name="upd_fail_downgrade">Android installerar inte en äldre version över en nyare. Stanna på den här versionen tills kanalen hinner i kapp.</string> + <string name="upd_fail_signature">Den här kopian av DroidDeck är signerad på ett annat sätt (en lokal version?), så Android uppdaterar den inte på plats.</string> + <string name="upd_fail_storage">Det finns inte tillräckligt med ledigt utrymme för uppdateringen.</string> + <string name="upd_fail_aborted">Uppdateringen avbröts.</string> + <string name="upd_fail_with_message">Uppdateringen installerades inte: %1$s</string> + <string name="upd_fail_generic">Uppdateringen installerades inte.</string> + <string name="saves_layout_zip">%1$s-zip</string> + <string name="saves_zip">zip</string> + <string name="saves_not_launched">%1$s har inte startats än</string> + <string name="saves_none_found">Inga sparfiler hittades för %1$s</string> + <string name="saves_not_save_zip">%1$s har ingen drive_c-mapp, så det är inte en sparfils-zip från GameHub eller Winlator</string> + <string name="gstore_cannot_create">kan inte skapa mappar i %1$s</string> + <string name="gstore_cannot_write">kan inte skriva i %1$s</string> + <string name="gstore_cannot_write_detail">kan inte skriva i %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">Mappen är inte tillgänglig</string> + <string name="gfiles_name_taken">En annan fil använder redan %1$s</string> + <string name="gfiles_art_unavailable">Mappen för omslagsbilder är inte tillgänglig</string> + <string name="gfiles_unrelated_inventory">Mappen har en orelaterad synkinventering</string> + <string name="gfiles_remove_failed">Kunde inte ta bort %1$s</string> + <string name="gfiles_outside_folder">Filen hamnar utanför exportmappen</string> + <string name="cmgr_fex_unnamed">FEX (version utan versionsnamn)</string> + <string name="cmgr_no_profile">ingen profile.json i %1$s</string> + <string name="cmgr_not_component_package">%1$s är inte ett Linux-komponentpaket (typ %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s ledigt</string> + <string name="mode_storage_auto_short">Automatiskt</string> + <string name="appupd_catalog_rejected">Uppdateringskatalogen klarade inte sina kontroller (%1$s)</string> + <string name="main_adb_clip_label">DroidDecks inställning för underprocesser</string> + <string name="setup_signed_in_offline_unnamed">Inloggad · offlineläge</string> + <string name="setup_signed_in_unnamed">Inloggad</string> + <string name="driver_tag_bundled">PAKETERAD</string> + <string name="driver_tag_downloaded">NEDLADDAD</string> + <string name="driver_tag_imported">IMPORTERAD</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Sessionsskärm</string> + <string name="display_resolution">Upplösning</string> + <string name="display_game_fit">16:9-spelanpassning</string> + <string name="display_game_fit_hint">Hur 16:9-spel anpassas till den här skärmen. Nästa session.</string> + <string name="display_game_fit_unavailable">Välj Matcha skärmen eller en anpassad storlek som är smalare än 16:9.</string> + <string name="display_preserve">Behåll proportionerna</string> + <string name="display_stretch">Sträck ut till skärmen</string> + <string name="display_image_scaling">Bildskalning</string> + <string name="display_filter">Skalningsfilter</string> + <string name="display_filter_hint">Delas av Steam och skrivbordet.</string> + <string name="display_filter_live">Tillämpas direkt. Delas av Steam och skrivbordet.</string> + <string name="display_filter_note">FSR, GSR och NIS tillämpas när sessionens bild förstoras. Endast skärpa fungerar även utan förstoring.</string> + <string name="display_sharpness">Filterskärpa</string> + <string name="display_frame_rate">Bildfrekvens</string> + <string name="display_look_filter">%1$s Ställer in skalning till %2$s.</string> + <string name="display_match_screen">Matcha skärmen (%1$d×%2$d)</string> + <string name="display_follow_screen">Följ skärmen (ändrar storlek vid vikning)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Visa enhetsinformation</string> + <string name="device_info_hide">Dölj enhetsinformation</string> + <string name="device_info_loading">Läser av enheten…</string> + <string name="device_info_copy">Kopiera</string> + <string name="device_info_copy_hint">Kopiera denna information till en felrapport.</string> + <string name="device_info_copied">Kopierat.</string> + <string name="force_ssbs_title">Tvinga SSBS</string> + <string name="force_ssbs_hint">Håller kvar en CPU-hastighetsinställning som Wine annars stänger av. Kan göra Windows-spel snabbare på Snapdragon 8 Elite; liten skillnad på äldre chip. Gäller från nästa spelstart.</string> + + <string name="rail_stores">Butiker</string> + <string name="source_steam">Steam</string> + <string name="source_added">Egna</string> + <string name="setup_stores">Butiker</string> + <string name="setup_stores_open_on">Öppna en butik på</string> + <string name="setup_stores_speed">Nedladdningshastighet</string> + <string name="setup_stores_engine">Butiksmotor</string> + <string name="setup_stores_engine_chip_ready">● Motor redo</string> + <string name="setup_stores_engine_chip_missing">Inte byggd</string> + <string name="stores_tier_balanced">Balanserad</string> + <string name="stores_tier_fast">Snabb</string> + <string name="stores_tier_max">Max</string> + <string name="stores_title">Butiker</string> + <string name="stores_settings">Butiksinställningar</string> + <string name="stores_settings_eyebrow">Inställningar</string> + <string name="stores_chip_signed_out">(inte inloggad)</string> + <string name="stores_tab_store">Butik</string> + <string name="stores_tab_installed">Installerade</string> + <string name="stores_tab_library">Bibliotek</string> + <string name="stores_tab_all">Alla</string> + <string name="stores_tab_downloads">Nedladdningar</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Logga in</string> + <string name="stores_signout_named">%1$s · logga ut</string> + <string name="stores_signout_title">Logga ut från %1$s?</string> + <string name="stores_signout_note">Installerade spel finns kvar. Biblioteket hämtas igen när du loggar in på nytt.</string> + <string name="stores_signout_stay">Fortsätt vara inloggad</string> + <string name="stores_signout_go">Logga ut</string> + <string name="stores_search_catalog">Sök i %1$s-katalogen…</string> + <string name="stores_search_installed">Sök installerade spel…</string> + <string name="stores_search_library">Sök i ditt bibliotek…</string> + <string name="stores_search_open">Sök</string> + <string name="stores_installed_empty">Inget installerat</string> + <string name="stores_library_loading">Läser in</string> + <string name="stores_nothing_matches">Inga spel</string> + <string name="stores_shelves_loading">Läser in butiken…</string> + <string name="stores_amazon_no_catalog">Ingen katalog</string> + <string name="stores_shelf_new">Nytt</string> + <string name="stores_shelf_deals">Erbjudanden</string> + <string name="stores_shelf_free">Gratisspel</string> + <string name="stores_shelf_trending">Populärt nu</string> + <string name="stores_shelf_library">Ditt bibliotek</string> + <string name="stores_play">Spela</string> + <string name="stores_install">Installera</string> + <string name="stores_resume_install">Återuppta installation</string> + <string name="stores_dl_interrupted">Avbruten</string> + <string name="stores_install_size">Installation · %1$s</string> + <string name="stores_get_free">Hämta gratis</string> + <string name="stores_view_on">Visa på %1$s</string> + <string name="stores_buy_price">Köp · %1$s</string> + <string name="stores_installed_chip">Installerat</string> + <string name="stores_in_library">I biblioteket</string> + <string name="stores_free">Gratis</string> + <string name="stores_resume">Återuppta</string> + <string name="stores_refresh">Uppdatera</string> + <string name="stores_show_mature">Visa innehåll för vuxna</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Offline</string> + <string name="epic_card_none">Av</string> + <string name="epic_eos">Epic-inloggning (EOS)</string> + <string name="epic_offline">Starta offline</string> + <string name="epic_resolve">Lös Epic-inloggningen</string> + <string name="store_cloud_saves">Molnsparfiler</string> + <string name="cloud_upload">Ladda upp till molnet</string> + <string name="cloud_download">Hämta från molnet</string> + <string name="cloud_none">Inga molnsparfiler</string> + <string name="cloud_conflict">Molnkonflikt</string> + <string name="cloud_conflict_files">Molnkonflikt · %1$d filer</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Behåll molnet</string> + <string name="cloud_keep_local">Behåll lokal</string> + <string name="cloud_checking">Kontrollerar molnet…</string> + <string name="cloud_last_sync">Senast synkad %1$s</string> + <string name="cloud_never">Inte synkad än</string> + <string name="cloud_done">%1$d filer</string> + <string name="stores_eyebrow_installed">installerat</string> + <string name="stores_eyebrow_library">i ditt bibliotek</string> + <string name="stores_eyebrow_store">butik</string> + <string name="achievements_title">Prestationer</string> + <string name="achievements_count">%1$d av %2$d upplåsta</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Upplåsta · %1$d</string> + <string name="achievements_group_locked">Låsta · %1$d</string> + <string name="achievements_group_hidden">Dolda · %1$d</string> + <string name="achievements_hidden">Dold prestation</string> + <string name="achievements_hidden_detail">Detaljer visas när den är upplåst.</string> + <string name="achievements_all">Alla prestationer upplåsta</string> + <string name="achievements_reading">Läser in…</string> + <string name="achievements_signed_out">Logga in på %1$s för att se dina prestationer.</string> + <string name="achievements_unreachable">Kunde inte nå %1$s. Kontrollera igen när du är online.</string> + <string name="achievements_none">%1$s listar inga prestationer för det här spelet.</string> + <string name="achievements_show_all">Visa alla %1$d</string> + <string name="achievements_dialog_title">Prestationer · %1$s</string> + <string name="stores_in_steam">✓ I Steam</string> + <string name="stores_uninstall">Avinstallera</string> + <string name="stores_uninstalling">Avinstallerar…</string> + <string name="stores_uninstall_title">Avinstallera %1$s?</string> + <string name="stores_uninstall_body_size">Frigör %1$s och tar bort det från Steam.</string> + <string name="stores_uninstall_body">Tar bort dess filer och Steam-genväg.</string> + <string name="stores_launch_not_registered">Steam upptäcker spelet nästa gång det startar; starta det därifrån, eller starta Steam nu.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Steams val per spel</string> + <string name="stores_card_audio">Ljud</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Komponenter</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Motor</string> + <string name="stores_card_engine_value">%1$s · nativ</string> + <string name="stores_card_engine_java">%1$s · inbyggd</string> + <string name="stores_how_it_runs">Så körs det</string> + <string name="stores_kv_fetched">Hämtat från</string> + <string name="stores_kv_fetched_value">%1$s, med ditt konto</string> + <string name="stores_kv_installed_to">Installerat till</string> + <string name="stores_kv_installed_to_value">Spellagring (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Startas av</string> + <string name="stores_kv_launched_by_value">Steam och Proton, samma väg som Steam-spel</string> + <string name="stores_kv_launched_by_epic">Steam och Proton · Epic-inloggning per start, EOS-overlay av</string> + <string name="stores_kv_launched_by_amazon">Steam och Proton · Amazon Games Services-variabler inställda</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">läggs till i Steam vid installation (Deck-gränssnitt + Steam Input)</string> + <string name="stores_downloads_empty">Inga nedladdningar</string> + <string name="stores_dl_paused">Pausad</string> + <string name="stores_dl_queued">I kö</string> + <string name="stores_dl_queued_at">I kö · #%1$d</string> + <string name="stores_dl_cancelled">Avbruten</string> + <string name="stores_dl_failed">Misslyckades</string> + <string name="stores_dl_failed_reason">Misslyckades · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Pausa</string> + <string name="stores_dl_resume">Återuppta</string> + <string name="stores_dl_cancel">Avbryt</string> + <string name="stores_dl_retry">Försök igen</string> + <string name="stores_dl_clear">Rensa</string> + <string name="stores_dl_clear_all">Rensa avslutade</string> + <string name="stores_stage_manifest">Manifest</string> + <string name="stores_stage_download">Nedladdning</string> + <string name="stores_stage_verify">Verifiering</string> + <string name="stores_stage_install">Installation</string> + <string name="stores_stage_done">Klart</string> + <string name="stores_stage_checking">Kontrollerar</string> + <string name="stores_stage_downloading">Laddar ned</string> + <string name="stores_dl_cancel_confirm">Ta bort nedladdning?</string> + <string name="stores_dl_failed_kept">Misslyckades · %1$s behålls</string> + <string name="stores_stage_verifying">Verifierar</string> + <string name="stores_stage_installing">Installerar</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d filer</string> + <string name="stores_download_manager">Nedladdningshanterare</string> + <string name="stores_parallel">Samtidiga nedladdningar</string> + <string name="stores_install_where">Installera till</string> + <string name="stores_install_title">Installera %1$s:</string> + <string name="stores_target_internal">Internminne</string> + <string name="stores_target_free">%1$s ledigt</string> + <string name="stores_notification_channel">Butiksnedladdningar</string> + <string name="stores_notification_channel_hint">Visar nedladdningsförlopp för spel och håller nedladdningar igång i bakgrunden</string> + <string name="stores_notification_one">Laddar ned</string> + <string name="stores_notification_many">%1$d nedladdningar</string> + <string name="stores_notification_waiting">Förbereder…</string> + <string name="stores_notification_done_channel">Avslutade butiksnedladdningar</string> + <string name="stores_notification_installed">%1$s installerat</string> + <string name="stores_notification_failed">Nedladdningen av %1$s misslyckades</string> + <string name="stores_notification_delete">Ta bort</string> + <string name="stores_notification_keep">Behåll</string> + <string name="stores_login_title">Logga in på %1$s</string> + <string name="stores_login_provider_title">Logga in med din leverantör</string> + <string name="stores_login_reload">Läs in på nytt</string> + <string name="stores_login_close">Stäng</string> + <string name="stores_login_retry">Försök igen</string> + <string name="stores_login_finishing">Slutför inloggningen…</string> + <string name="stores_login_error_network">Inloggningssidan kunde inte läsas in: %1$s</string> + <string name="stores_login_error_idp_blocked">Den här leverantören vägrar logga in från en inbäddad sida. Logga in med ditt butikslösenord istället.</string> + <string name="stores_login_error_http">Inloggningssidan svarade med ett fel (HTTP %1$d).</string> + <string name="stores_login_error_ssl">Anslutningen till inloggningssidan är inte säker.</string> + <string name="stores_login_error_verification">Inloggningen kunde inte verifieras. Försök igen.</string> + <string name="stores_login_error_generic">Inloggningen misslyckades. Försök igen.</string> + <string name="common_close">Stäng</string> + <string name="common_back">Tillbaka</string> + <string name="common_edit">Redigera</string> + <string name="games_add_all">Lägg till alla spel i den här mappen</string> + <string name="games_added_none">Inget nytt i den här mappen</string> + <string name="edit_name">Namn</string> + <string name="edit_target">Mål</string> + <string name="edit_start_in">Starta i</string> + <string name="edit_launch_options">Startalternativ</string> + <string name="edit_artwork">Bildmaterial</string> + <string name="edit_other_exe">Annan exe…</string> + <string name="edit_remove">Ta bort</string> + <string name="art_cover">Omslag</string> + <string name="art_background">Bakgrund</string> + <string name="art_logo">Logotyp</string> + <string name="art_icon">Ikon</string> + <string name="art_src_folder">Spelmapp</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">Din fil</string> + <string name="art_src_exe">Från exe-filen</string> + <string name="art_src_auto">Automatiskt</string> + <string name="art_pick_image">Välj bild…</string> + <string name="art_pick_title">Välj en bild: %1$s</string> + <string name="art_reset">Återställ till automatiskt</string> + <plurals name="games_added_n"> + <item quantity="one">%1$d spel tillagt</item> + <item quantity="other">%1$d spel tillagda</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="one">%1$d redan i Spel</item> + <item quantity="other">%1$d redan i Spel</item> + </plurals> + <string name="hop_back">B · Tillbaka till %1$s</string> + <string name="hop_from_games">spelet</string> + <string name="hop_from_play">Spelinställningar</string> + <string name="sheet_graphics_drivers">Grafikdrivrutiner</string> + <string name="comp_this_proton">Denna Proton</string> + <string name="comp_every_proton">Alla Proton</string> + <string name="comp_for_every_proton">För alla Proton</string> + <string name="comp_available_count">Tillgängliga · %1$d</string> + <string name="comp_checked">Kontrollerade %1$s</string> + <string name="comp_check_all">Sök efter nya komponenter och drivrutiner</string> + <string name="comp_get">Hämta</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton och FEX nedan hör till denna Proton</string> + <string name="comp_device_tested">%1$s · testad</string> + <string name="comp_device_untested">%1$s · otestad</string> + <string name="comp_device_unsupported">%1$s · stöds inte</string> + <string name="gpu_auto_keeps">Håller det rekommenderade paret uppdaterat</string> + <string name="gpu_bar_auto">Auto · %1$s</string> + <string name="gpu_separately">Runtime- och visningsdrivrutiner separat</string> + <string name="gpu_use_title">Använda %1$s?</string> + <string name="proton_remove_title">Ta bort %1$s?</string> + <string name="setup_tab_diagnostics">Diagnostik</string> + <string name="setup_ready">Redo att spela</string> + <string name="setup_not_ready">Inte redo att spela än</string> + <string name="setup_ready_runtime">runtime %1$s</string> + <string name="setup_ready_limit_off">processgräns av</string> + <string name="setup_ready_signed_in">inloggad som %1$s</string> + <string name="setup_needs_you">När något behöver dig</string> + <string name="setup_group_look">Utseende</string> + <string name="setup_group_device">Den här enheten</string> + <string name="setup_group_art">Biblioteksbilder</string> + <string name="flathub_eyebrow">LINUX-APPAR</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Skrivbordsappar för Linux-skrivbordet, öppnas från Skrivbordsläge.</string> + <string name="flathub_setup">Konfigurera Flathub</string> + <string name="flathub_remove_title">Ta bort %1$s?</string> + <string name="flathub_remove_note">Dess inställningar och filer behålls.</string> + <string name="flathub_keep">Behåll</string> + <string name="desktop_size">Storlek på disk</string> + <string name="desktop_uninstall_title">Avinstallera skrivbordet?</string> + <string name="desktop_all_systems">Alla</string> + <string name="gog_achievements">GOG-prestationer</string> + <string name="gog_achievements_hint">Synkroniserar prestationer för GOG-spel som stöds. Gäller från nästa start.</string> + <string name="gog_achievements_off">Prestationer av</string> + <string name="gog_achievements_on">Prestationer på</string> + <string name="gog_achievements_signin">Logga in på GOG igen</string> + <string name="gog_achievements_uploaded">Prestationer uppladdade</string> + <string name="gog_achievements_pending">Uppladdning av prestation väntar</string> + <string name="gog_achievements_enable_store">Aktivera GOG-prestationer i butiksinställningarna</string> + <string name="gog_achievements_unavailable">Prestationer inte tillgängliga; kontrollera sessionsloggarna</string> + <string name="achievement_unlocked">Prestation upplåst</string> + <string name="touch_paste">Klistra in</string> + <string name="touch_dpad">Styrkors</string> + <string name="touch_left_stick">Vänster styrspak</string> + <string name="touch_right_stick">Höger styrspak</string> + <string name="touch_menu">Meny</string> + <string name="touch_keyboard">Tangentbord</string> + <string name="touch_dpad_up">Styrkors upp</string> + <string name="touch_dpad_down">Styrkors ner</string> + <string name="touch_dpad_left">Styrkors vänster</string> + <string name="touch_dpad_right">Styrkors höger</string> + <string name="touch_title">Pekskärmskontroller</string> + <string name="touch_controller">Pekskärmskontroll</string> + <string name="touch_mouse">Mus</string> + <string name="touch_on">På</string> + <string name="touch_off">Av</string> + <string name="touch_speed_slow">Långsam</string> + <string name="touch_speed_normal">Normal</string> + <string name="touch_speed_fast">Snabb</string> + <string name="touch_edit_layout">Redigera layout</string> + <string name="touch_droiddeck_menu">DroidDeck-meny</string> + <string name="touch_drag_hint">Dra kontroller till skärmen</string> + <string name="touch_arrange">Ordna</string> + <string name="touch_discard">Kasta</string> + <string name="touch_saving">Sparar…</string> + <string name="touch_icon">Ikon</string> + <string name="touch_narrower">B−</string> + <string name="touch_wider">B+</string> + <string name="touch_shorter">H−</string> + <string name="touch_taller">H+</string> + <string name="touch_hide">Dölj</string> + <string name="touch_color_opacity">Färg och opacitet…</string> + <string name="touch_copy_layout">Kopiera denna layout</string> + <string name="touch_layout_copied">Layouten kopierad; klistra in den i pekskärmslayouten för valfritt spel</string> + <string name="touch_paste_layout">Klistra in kopierad layout</string> + <string name="touch_reset_layout">Återställ till Steams standard</string> + <string name="touch_no_binding">Den här kontrollen har ingen bindning i Steam att ge en ikon</string> + <string name="touch_icon_for">Ikon för %1$s</string> + <string name="touch_layout_color">Färg på pekskärmslayout</string> + <string name="touch_done">Klar</string> + <string name="touch_save_failed">Det gick inte att spara pekskärmslayouten</string> + <string name="touch_saved">Pekskärmslayout sparad till Steam</string> + <string name="touch_app_id">App %1$d</string> + <string name="touch_label_hint">Etikett (valfritt)</string> + <string name="touch_icons_missing">Steams ikoner hittades inte</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d ikon från Steam</item> + <item quantity="other">%1$d ikoner från Steam</item> + </plurals> + <string name="touch_icon_color">Ikonfärg</string> + <string name="touch_button_color">Knappfärg</string> + <string name="touch_no_icon">Ingen ikon</string> + + <string name="stores_sync_sign_in">Kontrollerar inloggning…</string> + <string name="stores_sync_library">Hämtar ditt bibliotek…</string> + <string name="stores_sync_game_list">Hämtar spellista…</string> + <string name="stores_sync_details">Läser in speldetaljer… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">Synkroniserar %1$d spel…</item> + <item quantity="other">Synkroniserar %1$d spel…</item> + </plurals> + <string name="stores_sync_progress">Synkroniserar… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Hämtar omslagsbilder…</string> + <string name="stores_sync_failed">Synkroniseringen misslyckades</string> + <string name="stores_sync_no_games">Inga spel hittades i ditt %1$s-bibliotek</string> + <string name="stores_sync_amazon_no_answer">Amazon Games svarade inte för ditt bibliotek: uppdatera, eller logga in igen</string> + <string name="stores_sync_gog_unreachable">Det gick inte att nå GOG</string> + <string name="stores_sync_gog_unreadable">Det gick inte att läsa ditt GOG-bibliotek</string> + <string name="stores_registering">Registrerar hos Steam…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Cardboard-läge</string> + <string name="cardboard_mode_hint">Bilder för vänster och höger öga till ett VR-headset för mobilen.</string> + <string name="cardboard_source">Bild</string> + <string name="cardboard_source_screen">Virtuell skärm</string> + <string name="cardboard_source_sbs">Sida vid sida-3D</string> + <string name="cardboard_viewer">Glasögon</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Ingen lins</string> + <string name="cardboard_ipd">Ögonavstånd</string> + <string name="cardboard_screen_width">Skärmbredd</string> + <string name="cardboard_screen_distance">Skärmavstånd</string> + <string name="cardboard_head_tracking">Huvudspårning</string> + <string name="cardboard_no_sensor">Den här enheten saknar rotationssensor.</string> + <string name="cardboard_recenter">Centrera vyn</string> + <string name="cardboard_recenter_button">Centrera</string> + <string name="cardboard_steamvr">SteamVR (experimentellt)</string> + <string name="cardboard_steamvr_install">Installera</string> + <string name="cardboard_steamvr_install_hint">Laddar ned Valves SteamVR för ARM64 (571 MB).</string> + <string name="cardboard_steamvr_installing">Laddar ned och packar upp SteamVR…</string> + <string name="cardboard_steamvr_start">Starta</string> + <string name="cardboard_steamvr_ready_hint">Kör SteamVR med ditt huvud som headset; bilden växlar till sida vid sida-3D.</string> + <string name="cardboard_steamvr_stop">Stoppa</string> + <string name="cardboard_steamvr_running_hint">SteamVR körs.</string> + <string name="cardboard_steamvr_unavailable">Inte tillgängligt i den här sessionen.</string> + <string name="cardboard_steamvr_failed">SteamVR svarade inte; kontrollera sessionsloggen.</string> + <string name="drawer_component_downloads">Ladda ned komponenter</string> + <string name="drawer_component_downloads_hint">Nedladdningar delas med sidan Komponenter. Välj en nedladdad version ovan för att använda den nästa gång ett spel startas.</string> + <string name="drawer_component_choose_download">Välj en version att ladda ned</string> + <string name="stores_fact_developer">Utvecklare</string> + <string name="stores_fact_publisher">Utgivare</string> + <string name="stores_fact_released">Släppt</string> + <string name="stores_hltb_main">Huvudspel</string> + <string name="stores_hltb_extras">+ Extra</string> + <string name="stores_hltb_completionist">Komplettering</string> + <string name="stores_hltb_hours">%1$s h</string> + <string name="stores_hltb_minutes">%1$d min</string> +</resources> diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml new file mode 100644 index 000000000..082c55548 --- /dev/null +++ b/app/src/main/res/values-th/strings.xml @@ -0,0 +1,2210 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>การเริ่มใช้งานครั้งแรกจะดาวน์โหลด Steam ซึ่งอาจใช้เวลาสักครู่</item> + <item>ตามค่าเริ่มต้น การออกจากแอปจะหยุด Steam ไว้ชั่วคราว แล้วกลับมาทำงานต่อจากจุดเดิมเมื่อคุณเปิดแอปอีกครั้ง</item> + <item>คอนโทรลเลอร์ของคุณจะปรากฏเป็นจอย Xbox 360 หากไม่เช่นนั้นให้ใช้ปุ่มควบคุมบนหน้าจอ</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>การเริ่มใช้งานครั้งแรกจะตั้งค่าเดสก์ท็อป ซึ่งอาจใช้เวลาสักครู่</item> + <item>ตามค่าเริ่มต้น การออกจากแอปจะหยุดเซสชันไว้ชั่วคราว แล้วกลับมาทำงานต่อจากจุดเดิมเมื่อคุณเปิดแอปอีกครั้ง</item> + <item>การสัมผัสหน้าจอทำงานเหมือนทัชแพด ลากเพื่อเลื่อนตัวชี้ แตะเพื่อคลิก</item> + <item>คอนโทรลเลอร์ของคุณจะปรากฏเป็นจอย Xbox 360 หากไม่เช่นนั้นให้ใช้ปุ่มควบคุมบนหน้าจอ</item> + </string-array> + <string name="session_channel">เซสชัน</string> + <string name="session_channel_description">แสดงขณะมีเซสชัน Steam หรือเดสก์ท็อปเปิดอยู่</string> + <string name="session_notification">Steam กำลังทำงาน</string> + <string name="session_notification_downloads">กำลังดาวน์โหลดใน Steam</string> + <string name="session_notification_paused">หยุดเซสชันไว้ชั่วคราว - แตะดำเนินการต่อเมื่อพร้อม</string> + <string name="resume_session">ดำเนินการต่อ</string> + <string name="suspend_now">หยุดชั่วคราวตอนนี้</string> + <string name="stop_session">หยุดเซสชัน</string> + <string name="frame_gen_title">การสร้างเฟรม</string> + <string name="frame_gen_hint">เลือกโหมดการสร้างเฟรม</string> + <string name="frame_gen_off">ปิด</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG แบบปรับอัตโนมัติ %1$d</string> + <string name="lsfg_needs_steam">ต้องมี Lossless Scaling บน Steam</string> + <string name="lsfg_needs_install">ติดตั้ง Lossless Scaling ใน Steam หรือนำเข้า Lossless.dll</string> + <string name="lsfg_import">นำเข้า Lossless.dll…</string> + <string name="lsfg_using_steam">ใช้ชุดที่มาจาก Steam</string> + <string name="lsfg_using_import">ใช้ชุดที่นำเข้ามา</string> + <string name="lsfg_pick_title">เลือก Lossless.dll</string> + <string name="lsfg_imported">นำเข้า Lossless.dll แล้ว</string> + <string name="lsfg_import_unchanged">Lossless.dll นี้ถูกใช้งานอยู่แล้ว</string> + <string name="lsfg_import_not_owned">บัญชี Steam นี้ไม่ได้เป็นเจ้าของ Lossless Scaling</string> + <string name="lsfg_import_invalid">ไฟล์นี้ไม่ใช่ Lossless.dll ของ Lossless Scaling</string> + <string name="lsfg_import_failed">ไม่สามารถเตรียม LSFG จาก Lossless.dll นี้ได้</string> + <string name="lsfg_problem_missing">LSFG ต้องใช้ Lossless Scaling จาก Steam หรือ Lossless.dll ที่นำเข้ามา</string> + <string name="lsfg_problem_failed">LSFG ไม่สามารถเตรียมเชดเดอร์จาก Lossless.dll ได้</string> + <string name="hud_fg_unsupported">%1$s: ไม่รองรับ</string> + <string name="hud_fg_failed">%1$s: เอนจินล้มเหลว</string> + <string name="hud_fg_needs_lossless">%1$s: ต้องใช้ Lossless Scaling</string> + <string name="hud_fg_starting">%1$s %2$s · กำลังเริ่ม</string> + <string name="second_library_import_hint">เพิ่มไลบรารี Steam และสแกนหาเกม Windows เกมที่เป็นเจ้าของอยู่แล้วจะใช้รายการ Steam เดิม ส่วนที่เหลือจะกลายเป็นช็อตคัตแบบไม่ใช่ Steam</string> + <string name="game_env_title">สภาพแวดล้อมของเกม</string> + <string name="game_env_hint">ตัวแปรสภาพแวดล้อมสำหรับเกม Proton</string> + <string name="game_env_edit">แก้ไข</string> + <string name="game_env_shared">เกม Proton ทั้งหมด</string> + <string name="game_env_scope">โปรไฟล์</string> + <string name="game_env_app_id">รหัสแอป Steam</string> + <string name="game_env_open">เปิด</string> + <string name="game_env_profile">เกม %1$s</string> + <string name="game_env_add">เพิ่มตัวแปร</string> + <string name="game_env_name">ชื่อตัวแปร</string> + <string name="game_env_value">ค่า</string> + <string name="game_env_save">บันทึก</string> + <string name="game_env_remove">ลบ</string> + <string name="game_env_restore">รับค่าสืบทอดอีกครั้ง</string> + <string name="game_env_reset">ล้างการปรับแต่งสภาพแวดล้อม</string> + <string name="game_env_unset">ยกเลิกสำหรับโปรไฟล์นี้</string> + <string name="game_env_empty">ไม่มีตัวแปรที่ตั้งไว้</string> + <string name="game_env_inherited">สืบทอดมา</string> + <string name="game_env_custom">ตัวแปรกำหนดเอง</string> + <string name="game_env_error">โหลดหรือบันทึกไม่ได้ ตรวจสอบพื้นที่จัดเก็บว่าง แล้วลองใหม่</string> + <string name="game_env_invalid">ใช้ชื่อที่ถูกต้องและค่าที่มีความยาวไม่เกิน 8192 ตัวอักษร</string> + <string name="game_env_unused">รันไทม์ Proton มาตรฐานไม่อ่านตัวแปรนี้</string> + <string name="game_env_loading">กำลังโหลด…</string> + <string name="game_env_feature">การปรับแต่งระดับคุณสมบัติ D3D12 12_2 รายงานความสามารถ DirectX 12 Ultimate คุณสมบัติที่ไม่รองรับอาจทำงานล้มเหลว เว้นค่าไว้เพื่อให้ตรวจหาโดยอัตโนมัติ</string> + <string name="game_env_shader">การปรับแต่งโมเดลเชดเดอร์ D3D12 6_9 ต้องใช้ VKD3D รุ่นใหม่พอและคุณสมบัติ GPU ที่ตรงกัน เว้นค่าไว้เพื่อให้ตรวจหาโดยอัตโนมัติ</string> + <string name="game_env_cache">เปิดใช้แคชเชดเดอร์ของ Mesa เพื่อลดการคอมไพล์ซ้ำ false จะเปิดใช้แคช</string> + <string name="game_env_cache_size">ขนาดสูงสุดของแคชเชดเดอร์ Mesa เช่น 1G แคชที่ใหญ่ขึ้นจะใช้พื้นที่จัดเก็บมากขึ้น</string> + <string name="game_env_vkd3d_config">ตัวเลือก VKD3D สำหรับ D3D12 nodxr ปิดการเรย์เทรซิง ใช้เฉพาะกับเกมที่จำเป็นต้องปิด</string> + <string name="game_env_proton_log">ตั้งเป็น 1 เพื่อบันทึกล็อกวินิจฉัยของ Proton การบันทึกล็อกอาจลดประสิทธิภาพและใช้พื้นที่จัดเก็บ</string> + <string name="game_env_wined3d">ตั้งเป็น 1 เพื่อใช้ WineD3D แบบ OpenGL แทน DXVK สำหรับทดสอบความเข้ากันได้ของ D3D9–11</string> + <string name="game_env_no_ntsync">ตั้งเป็น 1 เพื่อให้เกมนี้ไม่ใช้ ntsync (droiddeck-ntsync หรือไดรเวอร์เคอร์เนล) โดยจะใช้ droiddeck-esync เมื่อมีแพ็กที่เหมาะสม มิฉะนั้นใช้ droiddeck-fsync</string> + <string name="game_env_no_esync">ตั้งเป็น 1 เพื่อปิด droiddeck-esync สำหรับเกมนี้ โดยจะใช้ droiddeck-fsync แทน หรือ droiddeck-ntsync เมื่อ fsync ปิดอยู่ด้วย</string> + <string name="game_env_no_fsync">ตั้งเป็น 1 เพื่อให้เกมนี้ไม่ใช้ droiddeck-fsync โดยจะใช้ droiddeck-esync เมื่อมีแพ็กที่เหมาะสม มิฉะนั้นใช้ droiddeck-ntsync</string> + <string name="game_env_fsync_first">ตั้งเป็น 1 เพื่อให้เกมนี้ทำงานบน droiddeck-fsync หรือ 0 เพื่อให้ droiddeck-esync มาก่อนเมื่อมีแพ็กที่เข้ากับ Proton นี้</string> + <string name="game_env_xalia">ตั้งเป็น 0 เพื่อปิดตัวช่วยควบคุมด้วยคอนโทรลเลอร์ของ Proton สำหรับเกมนี้</string> + <string name="game_env_hud">โอเวอร์เลย์ DXVK สำหรับ D3D9–11 fps จะแสดงอัตราเฟรม หากไม่ตั้งค่าจะซ่อนไว้</string> + <string name="game_env_dxvk_config">ตั้งค่า DXVK คั่นด้วยเซมิโคลอน รุ่นใหม่ใช้ dxvk.maxFrameRate สำหรับจำกัดเฟรมเรต ควรใช้ตัวจำกัดเฟรมของเซสชันสำหรับการเล่นทั่วไป</string> + <string name="game_env_vkd3d_limit">ขีดจำกัดอัตราเฟรม D3D12 0 คือไม่จำกัด</string> + <string name="game_env_glthread">การทำเธรด OpenGL ของ Mesa true อาจช่วยเกม OpenGL ที่ติดขัดจาก CPU ทดสอบดูตามแต่ละเกม</string> + <string name="sync_backend_title">การซิงค์ของ Wine</string> + <string name="sync_backend_hint">วิธีที่เกมประสานเธรดให้ตรงกัน จะมีผลในเซสชันถัดไป</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">ค่าตั้งล่วงหน้าของ FEX</string> + <string name="fex_next_launch">จะมีผลในการเปิดเกมครั้งถัดไป</string> + <string name="game_settings_title">เกม</string> + <string name="fex_default_label">ค่าเริ่มต้นของ FEX</string> + <string name="fex_default_detail">ใช้ค่าเริ่มต้นของ FEX</string> + <string name="fex_stability_label">เสถียรภาพ</string> + <string name="fex_stability_detail">ตั้งค่าที่ปลอดภัยที่สุดแต่ช้าที่สุด</string> + <string name="fex_compatibility_label">ความเข้ากันได้</string> + <string name="fex_compatibility_detail">TSO เต็มรูปแบบพร้อมมัลติบล็อก</string> + <string name="fex_intermediate_label">ระดับกลาง</string> + <string name="fex_intermediate_detail">TSO แบบสมดุลพร้อมลดความแม่นยำ x87</string> + <string name="fex_performance_label">ประสิทธิภาพ</string> + <string name="fex_performance_detail">ปิด TSO ทั้งหมด พร้อมมัลติบล็อก</string> + <string name="fex_performance_tso_label">ประสิทธิภาพ + TSO</string> + <string name="fex_performance_tso_detail">ค่าตั้งประสิทธิภาพพร้อมเปิด TSO หลัก</string> + <string name="fex_extreme_label">สูงสุด</string> + <string name="fex_extreme_detail">ปิดการตรวจสอบโค้ดที่ปรับเปลี่ยนตัวเอง เกมที่ใช้ JIT และ DRM อาจทำงานผิดพลาด</string> + <string name="fex_extreme_tso_label">สูงสุด + TSO</string> + <string name="fex_extreme_tso_detail">ค่าตั้งระดับสูงสุดพร้อมเปิด TSO หลัก</string> + <string name="fex_extreme_gn_label">สูงสุด-gn</string> + <string name="fex_extreme_gn_detail">ประสิทธิภาพพร้อมคำแนะนำ TSC และข้อมูลเมตา ควรลองก่อนสูงสุด</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">ประสิทธิภาพพร้อมตรวจสอบโค้ดเต็มรูปแบบและซ่อนบิตไฮเปอร์ไวเซอร์</string> + <string name="game_env_custom_value">ค่ากำหนดเอง</string> + <string name="game_env_select_value">เลือกค่า</string> + <string name="user_apps_section">โปรแกรมจำลองและแอป</string> + <string name="user_apps_add">เพิ่ม</string> + <string name="user_apps_add_detail">แอปหรือเกมของคุณเอง</string> + <string name="user_apps_kind_script">สคริปต์</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d%%</string> + <string name="user_apps_starting">กำลังเริ่ม…</string> + <string name="user_apps_added">เพิ่ม %1$s แล้ว</string> + <string name="user_apps_removed">ลบ %1$s แล้ว</string> + <string name="user_apps_back">เดสก์ท็อป</string> + <string name="user_apps_gone">แอปนั้นหายไปแล้ว เลือกใหม่จากเดสก์ท็อป</string> + <string name="user_apps_open">เปิด %1$s</string> + <string name="user_apps_remove">ลบ</string> + <string name="user_apps_remove_confirm">กดอีกครั้งเพื่อลบ</string> + <string name="user_apps_stop_session">หยุดเซสชันก่อนเพื่อลบ</string> + <string name="user_apps_script_linked">เรียกใช้ %1$s จากโฟลเดอร์ของมัน โดยเชื่อมโยงเข้าสู่เซสชันในตำแหน่งเดิม การลบแอปจะไม่กระทบโฟลเดอร์นั้น</string> + <string name="user_apps_script_copied">เรียกใช้ %1$s จากสำเนาโฟลเดอร์ของมันภายใน DroidDeck การลบแอปจะลบสำเนานี้ ไม่ใช่ต้นฉบับ</string> + <string name="user_apps_runtime_required">ติดตั้งรันไทม์ Linux ก่อน</string> + <string name="user_apps_file_gone">ไฟล์หายไปแล้ว</string> + <string name="user_apps_no_space">พื้นที่ว่างไม่พอ: การคัดลอกโฟลเดอร์ต้องใช้ %1$s</string> + <string name="user_apps_copying">กำลังคัดลอก %1$s</string> + <string name="user_apps_failed">เพิ่มแอปไม่สำเร็จ</string> + <string name="user_apps_github_checking">กำลังค้นหารุ่นที่เผยแพร่ของ %1$s</string> + <string name="user_apps_github_unreachable">ไม่สามารถอ่านรุ่นที่เผยแพร่ของ %1$s ได้</string> + <string name="user_apps_github_no_appimage">รุ่นที่เผยแพร่ของ %1$s ไม่มี AppImage แบบ ARM64 หรือ x86_64</string> + <string name="user_apps_downloading">กำลังดาวน์โหลด %1$s</string> + <string name="user_apps_download_failed">ดาวน์โหลดไม่สำเร็จ</string> + <string name="user_apps_update_failed">อัปเดตแอปไม่สำเร็จ</string> + <string name="user_apps_icon_failed">ไม่สามารถอ่านไอคอนนั้นได้</string> + <string name="user_apps_saved">บันทึก %1$s แล้ว</string> + <string name="user_apps_edit">แก้ไข</string> + <string name="user_apps_check_updates">ตรวจสอบการอัปเดต</string> + <string name="user_apps_checking_updates">กำลังตรวจสอบการอัปเดต…</string> + <string name="user_apps_update_to">อัปเดตเป็น %1$s</string> + <string name="user_apps_up_to_date">เป็นรุ่นล่าสุดแล้ว · %1$s</string> + <string name="user_apps_updated">อัปเดต %1$s แล้ว</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">เพิ่มแอป</string> + <string name="add_app_tab_script">สคริปต์</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">ไฟล์ .sh จะรันจากโฟลเดอร์ของตัวเอง แบบเต็มจอภายใต้ gamescope</string> + <string name="add_app_appimage_hint">AppImage แบบ ARM64 (aarch64) หรือแบบ x86_64 ที่รันผ่าน FEX จะถูกแตกไฟล์เข้าสู่รันไทม์ Linux เพียงครั้งเดียว</string> + <string name="add_app_github_hint">ติดตั้ง AppImage แบบ ARM64 จากรุ่นที่เผยแพร่ล่าสุดของที่เก็บ หรือแบบ x86_64 ผ่าน FEX</string> + <string name="app_fex_title">ใช้ FEX</string> + <string name="app_fex_auto">อัตโนมัติ</string> + <string name="app_fex_on">เปิด</string> + <string name="app_fex_off">ปิด</string> + <string name="app_fex_hint">FEX รันโปรแกรม Linux x86 บนอุปกรณ์ ARM64 นี้ อัตโนมัติจะใช้งานเมื่อโปรแกรมถูกสร้างมาสำหรับ x86</string> + <string name="app_fex_x86">%1$s ผ่าน FEX</string> + <string name="app_fex_not_ready">โปรแกรมนี้สร้างมาสำหรับพีซี %1$s และรันผ่าน FEX ซึ่งยังไม่ได้ตั้งค่า เปิด Steam สักครั้งขณะลงชื่อเข้าใช้: DroidDeck จะดาวน์โหลด FEX และ Steam Linux Runtime ที่จำเป็น จากนั้นโปรแกรมนี้จะเปิดขึ้น</string> + <string name="session_end_program">โปรแกรมนี้</string> + <string name="session_end_fex_missing">%1$s สร้างมาสำหรับพีซี x86 และรันผ่าน FEX ซึ่งยังไม่ได้ติดตั้ง เริ่ม Steam สักครั้งขณะลงชื่อเข้าใช้และให้ดาวน์โหลด FEX และ Steam Linux Runtime จากนั้นเริ่มโปรแกรมนี้อีกครั้ง</string> + <string name="add_app_flatpak_hint">รหัสแอป Flathub หรือหน้า Flathub ของมัน จะตั้งค่า Flatpak ก่อนหากยังไม่ได้ตั้งค่า</string> + <string name="add_app_file">ไฟล์</string> + <string name="add_app_no_file">ยังไม่เลือกไฟล์</string> + <string name="add_app_choose_file">เลือกไฟล์</string> + <string name="add_app_change_file">เปลี่ยน</string> + <string name="add_app_pick_script">เลือกสคริปต์</string> + <string name="add_app_pick_appimage">เลือก AppImage</string> + <string name="add_app_pick_icon">เลือกไอคอน</string> + <string name="add_app_repository">ที่เก็บ</string> + <string name="add_app_repository_placeholder">owner/repo หรือลิงก์ github.com</string> + <string name="add_app_repository_invalid">ไม่ใช่ที่เก็บ GitHub</string> + <string name="add_app_flatpak_id">รหัสแอป</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">ไม่ใช่รหัสแอป Flathub</string> + <string name="add_app_flatpak_bundle_hint">หรือไฟล์ .flatpak สำหรับแอปที่ไม่มีใน Flathub</string> + <string name="add_app_pick_flatpak">เลือกไฟล์ .flatpak</string> + <string name="add_app_source">แหล่งที่มา</string> + <string name="add_app_appearance">บนเดสก์ท็อปของคุณ</string> + <string name="add_app_preview_name">แอปใหม่</string> + <string name="add_app_name">ชื่อ</string> + <string name="add_app_name_placeholder">จากตัวแอป</string> + <string name="add_app_icon">ไอคอน</string> + <string name="add_app_icon_default">ไอคอนเริ่มต้น</string> + <string name="add_app_choose_icon">เลือกไอคอน</string> + <string name="add_app_clear_icon">ล้าง</string> + <string name="add_app_icon_suggestions">จากที่เก็บ</string> + <string name="add_app_icon_searching">กำลังค้นหาไอคอน…</string> + <string name="add_app_icon_reset">ค่าเริ่มต้น</string> + <string name="edit_app_title">แก้ไข %1$s</string> + <string name="edit_app_save">บันทึก</string> + <string name="add_app_checking_folder">กำลังตรวจสอบโฟลเดอร์…</string> + <string name="add_app_folder_linked">โฟลเดอร์ของมันเชื่อมโยงเข้าสู่เซสชัน: %1$s</string> + <string name="add_app_folder_copied">โฟลเดอร์ของมันมีโปรแกรม Linux ซึ่งพื้นที่จัดเก็บที่ใช้ร่วมกันไม่สามารถรันได้ จึงถูกคัดลอกเข้าสู่ DroidDeck (%1$s)</string> + <string name="add_app_confirm">เพิ่ม</string> + <string name="add_app_cancel">ยกเลิก</string> + <string name="add_app_runtime_required">ติดตั้งรันไทม์ Linux จากการตั้งค่าก่อน</string> + <string name="add_app_busy">รอให้ %1$s เสร็จก่อน</string> + <string name="steam_compat_label">เข้ากันได้</string> + <string name="proton_compatible_note">บิลด์ที่มีเครื่องหมาย “%1$s” ในเมนู Compatibility ของ Steam มีการแก้ไข prefix ของ DroidDeck และสามารถเริ่มเกมบนอุปกรณ์นี้ได้ ค่าเริ่มต้นคือ Proton Experimental ARM64 (%1$s) หรือ Proton 11.0 ARM64 (%1$s) เมื่อติดตั้งบิลด์ใหม่กว่านั้นแล้ว รายการ Proton ของ Valve เองที่ไม่มีเครื่องหมายนี้ไม่สามารถเริ่มเกมบนอุปกรณ์นี้ได้</string> + <string name="proton_installed_compatible">ติดตั้งแล้ว %1$s · %2$s</string> + <string name="common_cancel">ยกเลิก</string> + <string name="common_auto">อัตโนมัติ</string> + <string name="common_never">ไม่เลย</string> + <string name="common_always">เสมอ</string> + <string name="common_uninstall">ถอนการติดตั้ง</string> + <string name="common_check">ตรวจสอบ</string> + <string name="common_update">อัปเดต</string> + <string name="common_checking">กำลังตรวจสอบ…</string> + <string name="common_working">กำลังดำเนินการ…</string> + <string name="common_forget">ลืม</string> + <string name="common_add_ellipsis">เพิ่ม…</string> + <string name="common_use">ใช้</string> + <string name="common_applies_next_session">จะมีผลในเซสชันถัดไป</string> + <string name="mode_steam_title">เซสชัน Steam</string> + <string name="mode_desktop_title">เซสชันเดสก์ท็อป</string> + <string name="mode_display">หน้าจอ</string> + <string name="mode_resolution">ความละเอียด</string> + <string name="mode_res_720">สูงสุด 720p</string> + <string name="mode_res_900">สูงสุด 900p</string> + <string name="mode_res_1080">สูงสุด 1080p</string> + <string name="mode_res_native">ความละเอียดจริง</string> + <string name="mode_res_default">%1$s (ค่าเริ่มต้น)</string> + <string name="mode_res_custom_value">กำหนดเอง · %1$d×%2$d</string> + <string name="mode_res_custom">กำหนดเอง…</string> + <string name="mode_res_note">720p อาจทำให้เมนูตอบสนองไวขึ้น</string> + <string name="mode_ratio">อัตราส่วนหน้าจอ</string> + <string name="mode_ratio_custom">กำหนดโดยความละเอียดกำหนดเอง</string> + <string name="mode_ratio_auto">อัตโนมัติใช้อย่างน้อย 16:9</string> + <string name="mode_fps">ขีดจำกัดเฟรม</string> + <string name="mode_fps_note">จำกัดทั้งเซสชัน หน้าจอจะเปลี่ยนไปใช้อัตรารีเฟรชที่หารขีดจำกัดได้พอดี</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">เอาต์พุต HDR10</string> + <string name="mode_hdr_unavailable">ไม่พร้อมใช้งาน: %1$s</string> + <string name="mode_hdr_restart">รีสตาร์ทแอปเพื่อให้มีผล</string> + <string name="mode_drivers">ไดรเวอร์</string> + <string name="mode_gpu_drivers">ไดรเวอร์ GPU</string> + <string name="mode_gpu_drivers_hint">ใช้ร่วมกันระหว่าง Steam และเดสก์ท็อป ตั้งค่าได้ที่หน้า Components</string> + <string name="mode_touch_controls">การสัมผัสและการควบคุม</string> + <string name="mode_touch">การสัมผัส</string> + <string name="mode_touch_touchpad">ทัชแพด</string> + <string name="mode_touch_direct">โดยตรง</string> + <string name="mode_touch_note">อัตโนมัติ: ทัชแพดในเดสก์ท็อป โดยตรงใน Steam</string> + <string name="mode_osc">ปุ่มควบคุมบนหน้าจอ</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">อัตโนมัติจะแสดงปุ่มควบคุมเมื่อไม่มีคอนโทรลเลอร์เชื่อมต่ออยู่</string> + <string name="osc_steam_touch">การสัมผัสของ Steam (ทดลอง)</string> + <string name="osc_style_next_session">ปุ่มสัมผัสของ Steam จะเริ่มใช้งานในเซสชัน Steam ถัดไป</string> + <string name="osc_steam_layout">รูปแบบปุ่มสัมผัสของ Steam</string> + <string name="osc_steam_layout_hint">ย้าย ปรับขนาด หรือซ่อนปุ่มสัมผัสของเกมนี้ บันทึกไปยัง Steam</string> + <string name="mode_controller">คอนโทรลเลอร์</string> + <string name="mode_controller_hint">วิธีที่ Steam มองเห็นคอนโทรลเลอร์ของคุณ จะมีผลในเซสชันถัดไป</string> + <string name="mode_controller_deck">คอนโทรลเลอร์ Steam Deck</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">คอนโทรลเลอร์ Xbox 360</string> + <string name="mode_controller_note">Deck: ปุ่ม Quick Access และไจโร Steam Controller: เหมือนกัน พร้อมเซนเซอร์แรงจับที่ปุ่มด้านหลัง DualShock 4 และ DualSense: ไจโร และแทร็กแพดทำหน้าที่เป็นทัชแพด Xbox 360: เปิด Quick Access ด้วย Guide+A</string> + <string name="mode_back">กลับ</string> + <string name="back_menu_then_qam">เมนู → QAM</string> + <string name="back_qam_then_menu">QAM → เมนู</string> + <string name="mode_session">เซสชัน</string> + <string name="mode_suspend">การทำงานในพื้นหลัง</string> + <string name="mode_suspend_hint">เมื่อซ่อนแอปหรือปิดหน้าจอ</string> + <string name="mode_suspend_manual">กำหนดเอง</string> + <string name="mode_suspend_native">ดั้งเดิม</string> + <string name="mode_suspend_steam_note">อัตโนมัติจะหยุดชั่วคราวและดำเนินการต่อเมื่อแสดงผล ดั้งเดิมเตรียม Steam เข้าสู่โหมดพักก่อน กำหนดเองรอการกดดำเนินการต่อ ไม่มีวันทำงานต่อเนื่องตลอด</string> + <string name="mode_background_downloads">ให้การดาวน์โหลดทำงานต่อไป</string> + <string name="mode_background_downloads_hint">เลื่อนการหยุดพื้นหลังชั่วคราวออกไปขณะ Steam กำลังดาวน์โหลดอยู่</string> + <string name="mode_suspend_note">อัตโนมัติจะหยุดชั่วคราวเมื่ออยู่ในพื้นหลังและดำเนินการต่อเมื่อแสดงผล กำหนดเองรอการกดดำเนินการต่อ ไม่มีวันทำงานต่อเนื่องตลอด</string> + <string name="mode_startup">การเริ่มทำงาน</string> + <string name="mode_steam_startup">เริ่ม Steam พร้อมกับ DroidDeck</string> + <string name="mode_steam_startup_hint">เปิดเซสชัน Steam เมื่อ DroidDeck เริ่มทำงาน</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">กำลังค้นหารุ่นที่เผยแพร่…</string> + <string name="mode_decky_ready">พร้อมติดตั้ง %1$s</string> + <string name="mode_decky_none">ไม่พบบิลด์ที่เข้ากันได้ ลองใหม่อีกครั้งในหลัง</string> + <string name="mode_decky_installed">ติดตั้งแล้ว · %1$s</string> + <string name="mode_decky_update">มีอัปเดต · %1$s</string> + <string name="mode_decky_current">เป็นรุ่นล่าสุดแล้ว · %1$s</string> + <string name="mode_decky_loader">ตัวโหลด</string> + <string name="mode_decky_install_latest">ติดตั้งรุ่นล่าสุด</string> + <string name="mode_decky_on">เริ่มพร้อมกับ Steam เมื่อเปิดใช้งาน แอปอื่นบนอุปกรณ์นี้อาจสามารถควบคุม Steam ได้</string> + <string name="mode_decky_off">ปิดอยู่ พอร์ตดีบักระยะไกลของ Steam ยังคงปิดอยู่</string> + <string name="mode_decky_remove_title">ถอนการติดตั้ง Decky Loader?</string> + <string name="mode_decky_remove_text">ลบตัวโหลดออก พลักอินและการตั้งค่าของคุณจะยังคงอยู่</string> + <string name="mode_client">ไคลเอนต์</string> + <string name="mode_mangoapp">โอเวอร์เลย์แสดงประสิทธิภาพ</string> + <string name="mode_mangoapp_hint">ปิดหากเกมขัดข้องหรือจอดำในโหมด Deck จะมีผลในเซสชันถัดไป</string> + <string name="mode_steam_repair">ซ่อมแซมไคลเอนต์ Steam</string> + <string name="mode_steam_repair_hint">ดาวน์โหลดไคลเอนต์ Steam ใหม่อีกครั้งในครั้งถัดไปที่ Steam เริ่มทำงาน ใช้เมื่อ Steam ค้างอยู่ที่หน้าจอโหลด เกม การลงชื่อเข้าใช้ และการตั้งค่ายังคงอยู่</string> + <string name="mode_steam_repair_queued">ไคลเอนต์ Steam จะถูกดาวน์โหลดใหม่อีกครั้งในครั้งถัดไปที่ Steam เริ่มทำงาน</string> + <string name="mode_steam_repair_button">ซ่อมแซม</string> + <string name="mode_added_art">ภาพปก</string> + <string name="mode_sgdb_key">คีย์ API ของ SteamGridDB</string> + <string name="mode_sgdb_key_yours">คีย์ของคุณ</string> + <string name="mode_sgdb_edit">แก้ไข…</string> + <string name="mode_added_art_hint">เติมภาพปกที่ขาดไปจาก Steam จากนั้นจาก SteamGridDB ไฟล์ cover.jpg, hero.jpg, logo.png หรือ icon.png ของคุณเองในโฟลเดอร์เกมจะมีสิทธิ์ก่อน</string> + <string name="summary_remove_game">ลบ %1$s</string> + <string name="mode_added_remove_confirm">ลบหรือไม่?</string> + <string name="games_add">เพิ่มเกม</string> + <string name="games_add_title">เพิ่มเกม:</string> + <string name="games_pick_exe_title">เลือกไฟล์ exe:</string> + <string name="games_add_unreachable">ตำแหน่งนี้ไม่พร้อมใช้งานในเซสชัน</string> + <string name="mode_added_exe_check">ตรวจสอบไฟล์ exe นี้</string> + <string name="wincomp_title">คอมโพเนนต์ของ Windows</string> + <string name="wincomp_pad_hint">LB / RB สลับระหว่างส่วนต่าง ๆ</string> + <string name="wincomp_card_none">ไม่มี</string> + <string name="wincomp_applies">DLL ของ Windows ที่ Proton ไม่มีให้ จากรายการคอมโพเนนต์ เปิดใช้รายการที่เกมนี้ต้องการ: รายการเหล่านั้นจะถูกใส่ไว้ในโฟลเดอร์ Windows ของเกมเอง ไม่ใช่ไฟล์ของเกม และจะคงอยู่ไม่ว่าเกมจะรันบน Proton รุ่นใด</string> + <string name="wincomp_offline">ไม่สามารถเข้าถึงรายการคอมโพเนนต์ได้ คอมโพเนนต์ที่ดาวน์โหลดไว้แล้วยังสามารถสลับได้</string> + <string name="wincomp_recommended">แนะนำสำหรับเกมนี้</string> + <string name="wincomp_checking">กำลังตรวจสอบว่าเกมนี้ต้องการอะไร…</string> + <string name="wincomp_when_online">พร้อมใช้งานเมื่อออนไลน์</string> + <plurals name="wincomp_turning_on"> + <item quantity="other">กำลังเปิดใช้คอมโพเนนต์ %1$d รายการ…</item> + </plurals> + <string name="wincomp_auto_section">เปิดใช้สำหรับเกมนี้</string> + <string name="wincomp_auto_steam">Steam ติดตั้ง %1$s ร่วมกับ %2$s (แอป %3$s)</string> + <string name="wincomp_auto_gog">จากรายการของ GOG สำหรับเกมนี้</string> + <string name="wincomp_auto_epic">จากข้อกำหนดเบื้องต้นของ Epic สำหรับเกมนี้</string> + <string name="wincomp_auto_amazon">จากขั้นตอนติดตั้งของ Amazon สำหรับเกมนี้</string> + <string name="wincomp_steam_by_name">Steam ติดตั้ง %1$s ร่วมกับแอป %2$s โดยจับคู่ตามชื่อ</string> + <string name="wincomp_suggestions">ยังมีการแนะนำเพิ่ม</string> + <string name="wincomp_suggestion_reason">แนะนำโดย %1$s; ไม่ใช่คอมโพเนนต์ในที่นี้</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">เกมนี้มี %1$s มาด้วย</string> + <string name="wincomp_found_shipped">เกมนี้มี %1$s (ส่วนมากใช้งานได้ตามเดิม)</string> + <string name="wincomp_already_here">มีอยู่ในเกมนี้แล้ว</string> + <string name="wincomp_no_recommendation">ไม่มีสิ่งใดในโฟลเดอร์เกมที่ชี้ไปยังคอมโพเนนต์</string> + <string name="wincomp_active">ติดตั้งสำหรับเกมนี้ (%1$d)</string> + <string name="wincomp_all">คอมโพเนนต์ทั้งหมด (%1$d)</string> + <string name="wincomp_installers">ต้องใช้ตัวติดตั้งของ Windows (%1$d)</string> + <string name="wincomp_installers_note">รายการเหล่านี้เป็นโปรแกรมติดตั้งของตัวเอง ไม่มีแพ็กเกจภายในให้แยกส่วน และยังไม่มีการบันทึกสิ่งที่ติดตั้ง ส่วนใหญ่เป็นรันไทม์รุ่นเก่าที่ Mono ภายใน Proton ทำหน้าที่แทนอยู่แล้ว การบันทึกจะถูกเพิ่มเมื่อจัดทำแล้ว</string> + <string name="wincomp_show">แสดง</string> + <string name="wincomp_hide">ซ่อน</string> + <string name="wincomp_needs_installer">รันโปรแกรมติดตั้งของตัวเอง (ยังไม่มีการบันทึก)</string> + <string name="wincomp_unsupported">ยังไม่รองรับในที่นี้</string> + <string name="wincomp_loading">กำลังโหลดคอมโพเนนต์…</string> + <string name="wincomp_failed">ติดตั้งไม่ได้: %1$s</string> + <string name="wincomp_downloaded">ดาวน์โหลดแล้ว</string> + <string name="wincomp_downloading">กำลังดาวน์โหลด %1$s</string> + <string name="wincomp_installing">กำลังติดตั้ง: %1$s</string> + <string name="wincomp_reverting">กำลังปิดใช้…</string> + <string name="wincomp_download_size">ดาวน์โหลด %1$s</string> + <string name="wincomp_next_launch">จะมีผลในครั้งถัดไปที่เริ่มเกม</string> + <string name="mode_stretch169">ยืดเกม 16:9</string> + <string name="mode_stretch169_hint">บนหน้าจอที่เป็นสี่เหลี่ยมจัตุรัสกว่า 16:9 จะยืดเกมให้เต็มจอแทนการแสดงแถบดำ จะมีผลในเซสชันถัดไป</string> + <string name="mode_audio">เสียง</string> + <string name="mode_directaudio">DirectAudio สำหรับเกม</string> + <string name="mode_directaudio_hint">ข้าม PulseAudio เพื่อลดความหน่วง</string> + <string name="mode_mic">ไมโครโฟน</string> + <string name="mode_mic_hint">สำหรับการแชทด้วยเสียง</string> + <string name="mode_network">เครือข่าย</string> + <string name="mode_wifi_discovery">การค้นหา Wi-Fi</string> + <string name="mode_wifi_discovery_hint">แสดงชื่อ Wi-Fi และเครือข่ายใกล้เคียงใน Steam</string> + <string name="mode_wifi_location_off">บริการตำแหน่งปิดอยู่</string> + <string name="mode_wifi_permission_denied">ไม่ได้รับสิทธิ์ตำแหน่ง</string> + <string name="mode_wifi_open_settings">เปิดการตั้งค่า</string> + <string name="mode_wifi_continue">ดำเนินการต่อ</string> + <string name="mode_wifi_explain_title">เปิดการค้นหา Wi-Fi?</string> + <string name="mode_wifi_explain_text">Android กำหนดให้ต้องมีสิทธิ์ตำแหน่งเพื่ออ่านชื่อ Wi-Fi และค้นหาเครือข่ายใกล้เคียง DroidDeck ใช้สิทธิ์นี้เพื่อแสดงเครือข่ายเหล่านั้นใน Steam เราไม่ติดตามตำแหน่งของคุณ การเข้าถึงอินเทอร์เน็ตทำงานได้โดยไม่ต้องมีสิทธิ์นี้</string> + <string name="mode_storage">พื้นที่จัดเก็บเกม</string> + <string name="mode_storage_auto">อัตโนมัติ - การ์ด SD เมื่อมีอยู่ รวมกับโฟลเดอร์ Games ของคุณ</string> + <string name="mode_storage_auto_none">อัตโนมัติ - โฟลเดอร์ Games ของคุณ รวมกับการ์ด SD เมื่อมีอยู่ (ยังไม่มีในขณะนี้)</string> + <string name="mode_storage_internal">พื้นที่ภายในเท่านั้น</string> + <string name="mode_storage_folder">โฟลเดอร์: %1$s</string> + <string name="mode_second_library">ไลบรารีที่สอง</string> + <string name="mode_storage_note">เกมที่สตรีมข้อมูลจาก SD หรือพื้นที่จัดเก็บที่ใช้ร่วมกันอาจกระตุก</string> + <string name="mode_storage_diagnostics">การวินิจฉัยพื้นที่จัดเก็บ</string> + <string name="mode_storage_diagnostics_hint">บันทึกผลการจัดสรรและเวลาพื้นที่จัดเก็บที่สุ่มตรวจไว้ในล็อกที่แชร์ อาจทำให้การติดตั้งช้าลงขณะเปิดใช้งาน จะมีผลในเซสชันถัดไป</string> + <string name="mode_choose_folder">เลือกโฟลเดอร์…</string> + <string name="mode_custom_res_title">ความละเอียดกำหนดเอง</string> + <string name="mode_custom_res_text">แทนที่ขีดจำกัดและอัตราส่วน ขนาดที่ไม่ตรงกับหน้าจอจะมีแถบดำ</string> + <string name="mode_width">ความกว้าง</string> + <string name="mode_height">ความสูง</string> + <string name="mode_custom_res_range">ระหว่าง 320×240 ถึง 3840×2160</string> + <plurals name="mode_added_count"> + <item quantity="other">%1$d เกม</item> + </plurals> + <string name="common_ok">ตกลง</string> + <string name="common_delete">ลบ</string> + <string name="common_restore">คืนค่า</string> + <string name="common_download">ดาวน์โหลด</string> + <string name="common_delete_named">ลบ %1$s</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">ไดรเวอร์รันไทม์</string> + <string name="comp_runtime_driver_hint">ใช้โดย Steam เกม และเดสก์ท็อป จะมีผลในเซสชันถัดไป</string> + <string name="comp_import_turnip">นำเข้า Turnip zip…</string> + <string name="comp_display_driver">ไดรเวอร์แสดงผล</string> + <string name="comp_display_driver_hint">วาดเฟรมบนหน้าจอในทั้งสองโหมด รีสตาร์ทแอปเพื่อให้มีผล</string> + <string name="comp_import_adrenotools">นำเข้า AdrenoTools zip…</string> + <string name="comp_never">ไม่เคย</string> + <string name="comp_nightlies_checked">ตรวจสอบ Nightlies ครั้งล่าสุด %1$s</string> + <string name="comp_title">Components</string> + <string name="comp_about">เกี่ยวกับ Components</string> + <string name="comp_check_drivers">ตรวจสอบไดรเวอร์ใหม่</string> + <string name="comp_check_nightlies">ตรวจสอบแพ็กเกจใหม่ใน Nightlies</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">Proton ที่เลือกไว้ที่นี่จะรันทุกเกมที่ยังไม่ได้เลือก Proton ของตัวเองใน Steam และการเปลี่ยนแปลงใน Steam ก็จะแสดงที่นี่ด้วย Steam จะแทนที่ Proton ของ Valve เมื่ออัปเดต ต้นฉบับจะถูกเก็บไว้ตามแต่ละบิลด์</string> + <string name="comp_scope">การตั้งค่า</string> + <string name="comp_profile_configure">ตั้งค่าโปรไฟล์</string> + <string name="comp_profile">โปรไฟล์</string> + <string name="comp_scope_general">โปรไฟล์เริ่มต้น</string> + <string name="comp_scope_general_detail">ใช้โดยอัตโนมัติกับเกมที่ไม่มีการปรับแต่งเฉพาะ</string> + <string name="comp_profile_game_detail">การตั้งค่าที่บันทึกไว้จะใช้โดยอัตโนมัติเมื่อ %1$s เปิดขึ้น</string> + <string name="comp_game_hint">เลือกเฉพาะการตั้งค่าที่เกมนี้ต้องการ ส่วนที่เหลือจะยังใช้โปรไฟล์เริ่มต้นต่อไป</string> + <string name="comp_game_fex">ค่าตั้งล่วงหน้าของ FEX</string> + <string name="comp_game_fex_inherited">ใช้ค่าตั้งล่วงหน้าจากโปรไฟล์เริ่มต้น</string> + <string name="comp_game_fex_override">บันทึกค่าตั้งล่วงหน้ากำหนดเองสำหรับเกมนี้แล้ว</string> + <string name="comp_game_fex_custom">ตัวแปร FEX กำหนดเอง</string> + <string name="comp_game_use_general">ใช้ค่าเริ่มต้น</string> + <string name="comp_game_compatibility">ความเข้ากันได้ของเกม</string> + <string name="comp_game_environment_hint">ตัวเลือกสภาพแวดล้อมของ Proton, DXVK, VKD3D และการซิงค์สำหรับเกมนี้</string> + <string name="comp_game_proton_inherited">ใช้ Proton จากโปรไฟล์เริ่มต้น</string> + <string name="comp_game_proton_override">บันทึก Proton กำหนดเองสำหรับเกมนี้ผ่านการจับคู่ความเข้ากันได้ของ Steam แล้ว</string> + <string name="comp_no_proton_short">ไม่มี Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">ใช้แพ็กเกจของโปรไฟล์เริ่มต้นจาก %1$s เว้นแต่เกมนี้เลือกแพ็กเกจอื่น</string> + <string name="comp_game_component_general">ใช้แพ็กเกจคอมโพเนนต์ของโปรไฟล์เริ่มต้น</string> + <string name="comp_game_component_override">ใช้ %1$s เฉพาะสำหรับเกมนี้เท่านั้น</string> + <string name="comp_game_runtime_options">ตัวเลือกรันไทม์</string> + <string name="comp_game_environment_other_hint">ตัวเลือกสภาพแวดล้อมอื่น ๆ ของ Proton, DXVK, VKD3D-Proton และการซิงค์เฉพาะเกม</string> + <string name="comp_game_saved_next">บันทึกโปรไฟล์ของ %1$s ไว้สำหรับการเปิดเกมครั้งถัดไป</string> + <string name="game_no_steam_title">เริ่มโดยไม่ใช้ Steam</string> + <string name="game_no_steam_detail">สำหรับเกมที่ปิดตัวทันที ใช้เริ่มเกมโดยไม่ตั้งค่า Steam ของ Proton ฟีเจอร์ของ Steam เช่นโอเวอร์เลย์จะถูกปิดสำหรับเกมนี้</string> + <string name="game_no_steam_on">เปิด</string> + <string name="comp_game_proton_live">บันทึกการเลือก Proton ของ %1$s แล้ว และพร้อมใช้งานใน Steam ตอนนี้</string> + <string name="comp_game_proton_next">บันทึกการเลือก Proton ของ %1$s แล้ว รีสตาร์ท Steam ก่อนเปิดเกม</string> + <string name="comp_game_proton_not_runnable">%1$s ไม่สามารถใช้ Proton ที่เลือกบนอุปกรณ์นี้ได้</string> + <string name="comp_game_load_failed">ไม่สามารถโหลดโปรไฟล์ของ %1$s ได้: %2$s</string> + <string name="comp_game_save_failed">ไม่สามารถบันทึกโปรไฟล์เกมได้: %1$s</string> + <string name="comp_game_reset">รีเซ็ตโปรไฟล์เกม</string> + <string name="comp_game_reset_title">รีเซ็ตโปรไฟล์ของ %1$s?</string> + <string name="comp_game_reset_body">ลบการปรับแต่ง Proton, FEX, แพ็กเกจคอมโพเนนต์, สภาพแวดล้อม, การกรองพื้นผิว และคอมโพเนนต์ Windows ของเกมนี้หรือไม่? เกมจะใช้โปรไฟล์เริ่มต้นแทน</string> + <string name="comp_game_reset_done">%1$s ใช้โปรไฟล์เริ่มต้นแล้วตอนนี้</string> + <string name="comp_game_reset_failed">ไม่สามารถรีเซ็ตโปรไฟล์เกมได้ทั้งหมด: %1$s</string> + <string name="comp_default_live">%1$s รันทุกเกมที่ยังไม่ได้เลือก Proton ของตัวเองใน Steam แล้วตอนนี้</string> + <string name="comp_default_next">%1$s จะรันทุกเกมที่ยังไม่ได้เลือก Proton ของตัวเอง ตั้งแต่ครั้งถัดไปที่ Steam เริ่มทำงาน</string> + <string name="comp_default_not_runnable">%1$s ไม่สามารถเริ่มเกมบนอุปกรณ์นี้ได้ จึงไม่สามารถเป็นค่าเริ่มต้นได้</string> + <string name="comp_default_failed">ไม่สามารถบันทึก Proton เริ่มต้นได้: %1$s</string> + <string name="comp_swapped">%1$s · สลับแล้ว %2$d รายการ</string> + <string name="comp_all_original">%1$s · เป็นต้นฉบับทั้งหมด</string> + <string name="comp_game_running_suffix">%1$s · เกมกำลังทำงาน</string> + <string name="comp_esync_pack">%1$s · แพ็ก droiddeck-esync</string> + <string name="comp_esync_wanted">%1$s · ยังไม่มีแพ็ก droiddeck-esync</string> + <string name="comp_esync_builtin">%1$s · มี esync อยู่ในตัวแล้ว ไม่ต้องใช้แพ็ก</string> + <string name="comp_gpu_drivers">ไดรเวอร์ GPU</string> + <string name="comp_reapplied">%1$s · นำมาใช้ใหม่เมื่อเปิด %2$s</string> + <string name="comp_next_queued">ถัดไป: %1$s · หลังจากปิดเกม</string> + <string name="comp_in_use">กำลังใช้ %1$s: %2$s</string> + <string name="comp_waits">รอจนกว่าเกมจะปิด</string> + <string name="comp_applies_next_game">จะมีผลในครั้งถัดไปที่เกมเริ่มทำงาน</string> + <string name="comp_import_zip">นำเข้า .zip</string> + <string name="comp_reading">กำลังอ่าน Proton…</string> + <string name="comp_no_proton">ยังไม่มี Proton ติดตั้งอยู่ เริ่ม Steam สักครั้งเพื่อดาวน์โหลด Proton ARM64 ของมัน หรือเพิ่ม GE-Proton, CachyOS หรือ DroidDeck-Proton จากการตั้งค่า</string> + <string name="comp_original">ต้นฉบับ · %1$s</string> + <string name="comp_this_build">%1$s · บิลด์นี้</string> + <string name="comp_earlier_build">%1$s · บิลด์ก่อนหน้า</string> + <string name="comp_installed_count">ติดตั้งแล้ว · %1$d</string> + <string name="comp_nothing_stored">ยังไม่มีอะไรบันทึกไว้สำหรับ %1$s ไฟล์ดั้งเดิมของ Proton นี้ถูกบันทึกเป็นต้นฉบับ</string> + <string name="comp_waits_long">มีเกมกำลังทำงานบน Proton นี้ ดังนั้นจะรอจนกว่าเกมจะปิด</string> + <string name="comp_applies_long">จะมีผลในครั้งถัดไปที่เกมเริ่มทำงาน</string> + <string name="comp_swap_title">สลับ %1$s?</string> + <string name="comp_swap_body">ใส่ %1$s เข้าไปใน %2$s หรือไม่? ไฟล์ต้นฉบับจะยังถูกบันทึกไว้ %3$s</string> + <string name="comp_restore_title">คืนค่า %1$s?</string> + <string name="comp_restore_body">นำต้นฉบับของ %1$s กลับมาไว้ใน %2$s หรือไม่? %3$s</string> + <string name="comp_delete_package_body">ลบแพ็กเกจออกจากแอป คุณสามารถดาวน์โหลดใหม่ได้อีก</string> + <string name="comp_delete_original_title">ลบต้นฉบับนี้?</string> + <string name="comp_delete_original_body">ต้นฉบับ %1$s นี้มาจากบิลด์ก่อนหน้าของ %2$s ต้นฉบับของบิลด์ปัจจุบันจะถูกเก็บไว้เสมอ</string> + <string name="comp_nightlies_count">ใน Nightlies · %1$d</string> + <string name="comp_refresh_to_list">รีเฟรชเพื่อแสดงรายการแพ็กเกจใน Nightlies</string> + <string name="comp_nothing_new">ไม่มีอะไรใหม่ตั้งแต่การตรวจสอบครั้งล่าสุด</string> + <string name="comp_import_wcp">นำเข้า .wcp</string> + <string name="comp_restore_original">คืนค่าต้นฉบับของ Proton นี้</string> + <string name="comp_restore_original_body">นำต้นฉบับ %2$s ของ %1$s กลับมาหรือไม่?</string> + <string name="comp_swap">สลับ</string> + <string name="comp_about_text">FEX, DXVK และ VKD3D-Proton สำหรับ Proton แต่ละตัวที่ Steam ใช้รันเกม\n\nเลือกแถวที่ติดตั้งไว้เพื่อสลับเข้าใช้งาน จะมีผลในครั้งถัดไปที่เกมเริ่มทำงาน หรือหลังจากเกมที่กำลังทำงานปิดลง\n\nตัวเลือกของคุณจะถูกตรวจสอบใหม่ก่อนการเริ่มเกมทุกครั้ง และนำกลับมาใช้หากมีสิ่งใดเปลี่ยนไป ไฟล์ต้นฉบับของ Proton แต่ละตัวจะถูกเก็บไว้ ดังนั้นการอัปเดตของ Steam จะไม่ทำให้สูญหาย</string> + <string name="comp_tag_original">ต้นฉบับ</string> + <string name="comp_tag_in_use">ใช้งานอยู่</string> + <string name="comp_tag_new">ใหม่</string> + <string name="comp_tag_stored">บันทึกไว้</string> + <string name="perf_title">ประสิทธิภาพ</string> + <string name="perf_lede">คอร์ CPU การแก้ไขเซสชัน และชื่อโฮสต์ จะมีผลในเซสชันถัดไป</string> + <string name="perf_client_cores_group">คอร์สำหรับไคลเอนต์ Steam</string> + <string name="perf_override">กำหนดคอร์เองแทนการเลือกของ Steam</string> + <string name="perf_override_hint">ตรึง Steam ตัวช่วย UI และ gamescope ไว้ที่คอร์ด้านล่าง</string> + <string name="perf_client_cores">คอร์ไคลเอนต์</string> + <string name="perf_client_cores_on">คอร์สำหรับ Steam</string> + <string name="perf_client_cores_off">เปิดการกำหนดเองเพื่อเลือก</string> + <string name="perf_game_cores_group">คอร์สำหรับเกม</string> + <string name="perf_game_cores">คอร์เกม</string> + <string name="perf_game_cores_hint">คอร์สำหรับเกม เมื่อเลือกทั้งหมด Android จะจัดสรรได้อย่างอิสระ</string> + <string name="perf_client_ui">อินเทอร์เฟซไคลเอนต์</string> + <string name="perf_glthread">GL แบบเธรด</string> + <string name="perf_glthread_hint">อาจทำให้เมนู Steam ตอบสนองไวขึ้น</string> + <string name="perf_zink">Zink: descriptor แบบเลซี่</string> + <string name="perf_zink_hint">แนะนำสำหรับไดรเวอร์ที่ไม่มี descriptor buffer</string> + <string name="perf_noglerror">ข้ามการตรวจสอบข้อผิดพลาด GL</string> + <string name="perf_noglerror_hint">ปิดการตรวจสอบความถูกต้องของ GL ในทุกการเรียก</string> + <string name="perf_gsrealtime">gamescope: คิว GPU แบบเรียลไทม์</string> + <string name="perf_gsrealtime_hint">ให้ความสำคัญกับงาน GPU ของตัวประกอบภาพก่อน อาจทำให้การเรียงเฟรมราบรื่นขึ้นโดยแลกกับประสิทธิภาพของเกมบางส่วน</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">ตรึง GPU ไว้ที่ความเร็วสัญญาณนาฬิกาสูงสุด</string> + <string name="perf_gpuclock_hint">สำหรับ Adreno เท่านั้น สะดุดน้อยลง แต่ใช้แบตเตอรี่และเกิดความร้อนมากขึ้น</string> + <string name="perf_fixes">การแก้ไขเซสชัน</string> + <string name="perf_sysmem">Turnip: การเรนเดอร์ sysmem</string> + <string name="perf_sysmem_hint">เร็วขึ้นสำหรับ Steam และเกมส่วนใหญ่ จำเป็นสำหรับ Adreno 710/720/722</string> + <string name="perf_xalia">ข้ามตัวช่วย xalia ของ Steam</string> + <string name="perf_fastsync">droiddeck-ntsync (ทดลอง)</string> + <string name="perf_fastsync_hint">ตัวแทนในระดับยูสเซอร์สเปซของไดรเวอร์ ntsync ของ Linux สำหรับการทดสอบ เมื่อเปิดใช้ Proton จะใช้สิ่งนี้แทน droiddeck-esync และ droiddeck-fsync ใช้ได้กับ Proton Experimental ARM64, GE-Proton และ proton-cachyos จะมีผลในเซสชันถัดไป</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync ก่อน</string> + <string name="perf_fsync_hint">รัน fsync ของ Proton เองบนไฟล์ Proton มาตรฐาน โดยมี futex_waitv ที่จัดเตรียมในระดับยูสเซอร์สเปซ จึงไม่ต้องใช้แพ็กและยังคงทำงานได้ทันทีที่ Steam อัปเดต Proton เมื่อเปิดใช้ เกมจะใช้สิ่งนี้ก่อน droiddeck-esync ยกเว้นบางส่วนที่ Proton ปิด fsync ไว้ จะมีผลในเซสชันถัดไป</string> + <string name="perf_esync_hint">เธรด Windows ของเกมรอกันผ่าน eventfd และหน่วยความจำที่ใช้ร่วมกัน แทนการเดินทางไปกลับผ่าน wineserver ใช้แพ็ก droiddeck-esync ที่สร้างไว้สำหรับบิลด์ Proton นั้นโดยเฉพาะ แพ็กจะถูกดึงมาโดยอัตโนมัติ จนกว่าจะมีแพ็กสำหรับบิลด์ Proton ใหม่ droiddeck-fsync จะทำหน้าที่แทน จะมีผลในเซสชันถัดไป</string> + <string name="perf_xalia_hint">ปิดตัวช่วยควบคุมด้วยคอนโทรลเลอร์ของ Proton ซึ่งกินพลัง CPU ในทุกเกม</string> + <string name="perf_seccomp">รัน proot โดยไม่ใช้ seccomp</string> + <string name="perf_seccomp_hint">อาจแก้ข้อผิดพลาด syscall ที่ขาดหายบนบางเคอร์เนล อาจลดประสิทธิภาพ</string> + <string name="perf_fastpath">เส้นทางลัดของ proot</string> + <string name="perf_fastpath_needs_seccomp">ต้องใช้ตัวกรอง seccomp ของ proot</string> + <string name="perf_fastpath_hint">ค้นหาไฟล์เร็วขึ้นและสะดุดน้อยลง ปิดหากโปรแกรมหาไฟล์ของตัวเองไม่พบ</string> + <string name="perf_identity">ข้อมูลประจำตัวของเซสชัน</string> + <string name="perf_hostname">ชื่อโฮสต์</string> + <string name="perf_hostname_hint">ชื่อที่ Steam มองเห็นสำหรับอุปกรณ์นี้ เว้นว่างไว้เพื่อคืนค่าเป็น %1$s</string> + <string name="perf_hostname_invalid">ใช้ได้เฉพาะตัวอักษร ตัวเลข และยัติภังค์ตรงกลาง ไม่เกิน 63 ตัวอักษร</string> + <string name="perf_phantom_warning">Android ถูกตั้งให้ปิดเซสชันนี้</string> + <string name="driver_installed">ติดตั้งแล้ว</string> + <string name="driver_available">พร้อมดาวน์โหลด</string> + <string name="driver_nothing_new">ไม่มีอะไรใหม่ตั้งแต่การตรวจสอบครั้งล่าสุด</string> + <string name="driver_restore_builtin">คืนค่าไดรเวอร์ที่มีในตัว</string> + <string name="driver_delete_builtin">มีอยู่ในตัวแอป: การลบนี้จะลบไฟล์ที่แตกไว้ออกและซ่อนมัน คืนค่าไดรเวอร์ที่มีในตัวจะนำกลับมา</string> + <string name="driver_delete_bundle">ครึ่งหนึ่งของแพ็ก Android + Linux: ทั้งสองครึ่งจะถูกลบ ไดรเวอร์ที่ใช้งานอยู่จะกลับไปใช้ค่าเริ่มต้น</string> + <string name="driver_delete_plain">ไฟล์ของมันจะถูกลบออกจากแอป</string> + <string name="driver_delete_in_use">ไฟล์ของมันถูกลบออก เนื่องจากกำลังใช้งานอยู่ ค่าเริ่มต้นจะเข้ามาแทนที่</string> + <string name="drawer_page_display">หน้าจอ</string> + <string name="drawer_page_effects">เอฟเฟกต์</string> + <string name="drawer_page_controls">การควบคุม</string> + <string name="drawer_page_components">Components</string> + <string name="drawer_page_controller">คอนโทรลเลอร์</string> + <string name="drawer_page_settings">การตั้งค่า</string> + <string name="drawer_page_desc">หน้า %1$s</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">เดสก์ท็อป</string> + <string name="drawer_steam_menu">เมนู Steam</string> + <string name="drawer_quick_access">Quick Access</string> + <string name="drawer_open_qam">เปิดเมนู Quick Access</string> + <string name="drawer_prev_page">หน้าก่อนหน้า</string> + <string name="drawer_next_page">หน้าถัดไป</string> + <string name="drawer_hud">HUD ประสิทธิภาพ</string> + <string name="drawer_scaling">โหมดการปรับขนาด</string> + <string name="drawer_scaling_sharpness">ความคมชัดของการปรับขนาด</string> + <string name="drawer_effects">เอฟเฟกต์หน้าจอ</string> + <string name="drawer_effects_hint">แบบเรียลไทม์ ใช้เวลา GPU เล็กน้อยขณะเปิดเอฟเฟกต์ใด ๆ อยู่</string> + <string name="drawer_look">ค่าที่กำหนดไว้ล่วงหน้า</string> + <string name="drawer_look_custom">กำหนดเอง</string> + <string name="drawer_effects_advanced">ขั้นสูง</string> + <string name="widgets_expanded">ขยายแล้ว</string> + <string name="widgets_collapsed">ย่อแล้ว</string> + <string name="drawer_cas">ความคมชัด (CAS)</string> + <string name="drawer_cas_level">ระดับความคมชัด</string> + <string name="drawer_fake_hdr">HDR จำลอง</string> + <string name="drawer_deband">ลดลายแถบสี</string> + <string name="drawer_deband_strength">ความเข้มของการลดลายแถบสี</string> + <string name="drawer_brightness">ความสว่าง</string> + <string name="drawer_contrast">ความเข้มสี</string> + <string name="drawer_gamma">แกมมา</string> + <string name="drawer_saturation">ความอิ่มตัวของสี</string> + <string name="drawer_fxaa">FXAA (ลบขอบหยัก)</string> + <string name="drawer_toon">เส้นขอบแบบการ์ตูน</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">การกรองพื้นผิว</string> + <string name="drawer_texture_hint">เกม DirectX 9–11 มีผลตั้งแต่ครั้งถัดไปที่เปิดเกม</string> + <string name="drawer_anisotropy">การกรองแบบ Anisotropic</string> + <string name="drawer_texture_sharpness">ความคมชัดของพื้นผิว</string> + <string name="drawer_texture_sharpness_note">ค่าไบแอส mip ติดลบ: พื้นผิวคมขึ้น มีประกายวูบวาบบ้าง อัตโนมัติจะปรับตามระดับที่เซสชันถูกขยายให้พอดีหน้าจอ</string> + <string name="drawer_touch_auto">อัตโนมัติ (%1$s)</string> + <string name="drawer_keyboard">แป้นพิมพ์</string> + <string name="drawer_pc_keyboard">แป้นพิมพ์พีซี</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">หน้าจอที่สอง</string> + <string name="drawer_second_screen_mode">โหมด</string> + <string name="drawer_second_screen_shows">แสดง</string> + <string name="drawer_android_apps">แอป Android</string> + <string name="drawer_launch_app">เปิดแอป</string> + <string name="common_hide">ซ่อน</string> + <string name="common_show">แสดง</string> + <string name="drawer_no_apps">ไม่มีแอปที่เปิดได้</string> + <string name="drawer_session_behavior">การทำงานของเซสชัน</string> + <string name="drawer_suspend_hint">มีผลตั้งแต่ตอนนี้และกับเซสชันในอนาคตในโหมดนี้</string> + <string name="drawer_next_session">เซสชันถัดไป</string> + <string name="drawer_support">การสนับสนุน</string> + <string name="drawer_logs">ล็อกเซสชัน</string> + <string name="drawer_logs_hint">แนบไปกับรายงานข้อบกพร่อง</string> + <string name="drawer_share_logs">แชร์ล็อก</string> + <string name="drawer_background">พื้นหลัง</string> + <string name="drawer_stop">หยุด</string> + <string name="drawer_stop_title">หยุดเซสชัน?</string> + <string name="drawer_running_now">กำลังทำงานอยู่ตอนนี้</string> + <string name="drawer_no_game">ไม่มีเกมกำลังทำงานบน Proton</string> + <string name="drawer_selected_profile">โปรไฟล์เกม</string> + <string name="drawer_selected_profile_none">รายละเอียดโปรไฟล์ยังไม่พร้อมใช้งาน</string> + <string name="drawer_profile_refresh_failed_initial">โหลดโปรไฟล์เกมไม่ได้ กำลังลองใหม่โดยอัตโนมัติ</string> + <string name="drawer_profile_refresh_failed_stale">รีเฟรชโปรไฟล์เกมไม่ได้ แสดงข้อมูลล่าสุดที่มีอยู่ กำลังลองใหม่โดยอัตโนมัติ</string> + <string name="drawer_profile_inspect_hint">ดูและแก้ไขการตั้งค่าที่บันทึกไว้สำหรับการเปิดครั้งถัดไป</string> + <string name="drawer_profile_default">ใช้โปรไฟล์เริ่มต้น</string> + <string name="drawer_profile_custom">โปรไฟล์กำหนดเอง</string> + <string name="drawer_component_inherited">ใช้ %1$s จากโปรไฟล์เริ่มต้น</string> + <string name="drawer_game_components_note">การเปลี่ยนแปลงจะถูกบันทึกในโปรไฟล์ของ %1$s และมีผลในครั้งถัดไปที่เกมเปิดขึ้น</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="other">การปรับแต่งเกม %1$d รายการ</item> + </plurals> + <string name="drawer_no_proton">ยังไม่มี Proton ติดตั้งอยู่</string> + <string name="drawer_game_running">เกมกำลังทำงาน</string> + <string name="drawer_reapplied">นำมาใช้ใหม่เมื่อเปิด %1$s</string> + <string name="drawer_original">ต้นฉบับ</string> + <string name="drawer_components_note">นำเข้าและลบได้ที่หน้า Components</string> + <string name="setup_check_done">เสร็จแล้ว</string> + <string name="setup_check_warn">ต้องดำเนินการ</string> + <string name="setup_check_busy">กำลังดำเนินการ</string> + <string name="setup_limit_adb_failed">ตรวจสอบที่อยู่ IP และพอร์ตของ Wireless debugging หรือจับคู่ใหม่หาก Android ลบอุปกรณ์นี้ไปแล้ว</string> + <string name="setup_install">ติดตั้ง</string> + <string name="setup_manage">จัดการ</string> + <string name="setup_tab_overview">ภาพรวม</string> + <string name="setup_tab_controller">คอนโทรลเลอร์</string> + <string name="setup_tab_session">เซสชัน</string> + <string name="setup_tab_launcher">ตัวเปิดแอป</string> + <string name="setup_title">การตั้งค่า</string> + <string name="setup_all_set">● พร้อมทั้งหมด</string> + <string name="setup_n_ready">พร้อม %1$d จาก %2$d</string> + <string name="setup_system_check">ตรวจสอบระบบ</string> + <string name="setup_gpu_supported">รองรับอุปกรณ์นี้</string> + <string name="setup_gpu_untested">GPU ที่ยังไม่ผ่านการทดสอบ</string> + <string name="setup_gpu_unsupported">ไม่รองรับ GPU นี้</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">Steam ต้องใช้ไดรเวอร์ Adreno; %1$s อาจแสดงจอดำ</string> + <string name="setup_runtime">รันไทม์ Linux</string> + <string name="setup_runtime_progress">%1$s · %2$d%%</string> + <string name="setup_runtime_missing">ยังไม่ติดตั้ง · ประมาณ 3 GB จะติดตั้งเมื่อกด Play ครั้งแรก</string> + <string name="setup_installed">ติดตั้งแล้ว</string> + <string name="setup_runtime_update">%1$s · มีอัปเดต</string> + <string name="setup_runtime_current">%1$s · เป็นรุ่นล่าสุดแล้ว</string> + <string name="setup_limit">ขีดจำกัดโพรเซสลูก</string> + <string name="setup_limit_on">เปิดอยู่ · Android อาจปิด Steam การปิดใช้งานใช้เวลาประมาณหนึ่งนาที</string> + <string name="setup_limit_unset">ยังไม่ตั้งค่า · Android อาจปิด Steam การปิดใช้งานใช้เวลาประมาณหนึ่งนาที</string> + <string name="setup_limit_unknown">ไม่สามารถตรวจสอบได้ · การปิดใช้งานใช้เวลาประมาณหนึ่งนาที</string> + <string name="setup_fix_it">แก้ไข</string> + <string name="setup_details">รายละเอียด</string> + <string name="setup_dev_options">ตัวเลือกสำหรับนักพัฒนา</string> + <string name="setup_use_wireless">ใช้ Wireless debugging</string> + <string name="setup_check_again">ตรวจสอบอีกครั้ง</string> + <string name="setup_limit_turn_on">เปิดขีดจำกัด</string> + <string name="setup_account">บัญชี Steam</string> + <string name="setup_signed_in_offline">ลงชื่อเข้าใช้เป็น %1$s · โหมดออฟไลน์</string> + <string name="setup_signed_in">ลงชื่อเข้าใช้เป็น %1$s</string> + <string name="setup_sign_in">กด Play แล้วลงชื่อเข้าใช้ Steam</string> + <string name="setup_tools">เครื่องมือ</string> + <string name="setup_controller">คอนโทรลเลอร์</string> + <string name="setup_controller_unavailable">การตั้งค่าคอนโทรลเลอร์ไม่พร้อมใช้งาน</string> + <string name="setup_session">เซสชัน</string> + <string name="setup_logs">ล็อกเซสชัน</string> + <string name="setup_logs_hint">บันทึกไว้หลังจบแต่ละเซสชัน</string> + <string name="setup_agent_commands">เครื่องมือดีบัก</string> + <string name="setup_latest_logs">ล็อกเซสชันล่าสุด</string> + <string name="setup_saved_logs">ล็อกเซสชันที่บันทึกไว้</string> + <string name="setup_saved_logs_hint">เก็บ %1$d รายการล่าสุด</string> + <string name="setup_clear_logs">ล้างล็อก</string> + <string name="setup_offline">โหมดออฟไลน์</string> + <string name="setup_sign_in_first">ลงชื่อเข้าใช้ Steam ก่อน</string> + <string name="setup_launcher">ตัวเปิดแอป</string> + <string name="setup_language">ภาษา</string> + <string name="setup_language_hint">Steam ก็ใช้ภาษานี้ด้วย ตั้งแต่ครั้งถัดไปที่เริ่มทำงาน</string> + <string name="setup_language_system">ค่าเริ่มต้นของระบบ (%1$s)</string> + <string name="setup_language_system_short">ค่าเริ่มต้นของระบบ</string> + <string name="setup_app_scale">ขนาดแอป</string> + <string name="setup_app_scale_hint">ข้อความและการควบคุมของ DroidDeck</string> + <string name="setup_app_scale_default">%1$d%% (ค่าเริ่มต้น)</string> + <string name="setup_theme">ธีม</string> + <string name="setup_theme_hint">รูปลักษณ์ของตัวเปิดแอป</string> + <string name="setup_home">ใช้เป็นหน้าจอหลัก</string> + <string name="setup_home_on">DroidDeck สามารถเป็นแอปหน้าจอหลักได้</string> + <string name="setup_home_off">DroidDeck จะไม่ถูกเสนอให้เป็นแอปหน้าจอหลัก</string> + <string name="setup_orientation">การวางแนว</string> + <string name="setup_orientation_auto">แนวนอนอัตโนมัติ</string> + <string name="setup_orientation_landscape">แนวนอน</string> + <string name="setup_orientation_reverse">แนวนอนกลับด้าน</string> + <string name="setup_fullscreen">เต็มจอ</string> + <string name="setup_fullscreen_on">ซ่อนแถบสถานะและแถบนำทางของ Android</string> + <string name="setup_fullscreen_off">แสดงแถบสถานะและแถบนำทางของ Android</string> + <string name="setup_default_home">แอปหน้าจอหลักเริ่มต้น</string> + <string name="setup_choose_home">เลือกแอปหน้าจอหลัก</string> + <string name="setup_choose">เลือก</string> + <string name="setup_linux_apps">แอป Linux (ทดลอง)</string> + <string name="setup_tool_files">ไฟล์</string> + <string name="setup_tool_files_hint">เรียกดูและจัดการไฟล์</string> + <string name="setup_tool_protons">รุ่นของ Proton</string> + <string name="setup_tool_protons_hint">ติดตั้งบิลด์ Proton แบบ ARM64</string> + <string name="setup_tool_performance">ประสิทธิภาพ</string> + <string name="setup_tool_performance_hint">การกำหนดคอร์ CPU</string> + <string name="setup_tool_roms">โฟลเดอร์ ROM</string> + <string name="setup_tool_roms_hint">ตำแหน่งที่เก็บเกมของโปรแกรมจำลอง</string> + <string name="setup_card_components">Components</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">การควบคุม</string> + <string name="setup_card_controls_hint">การกำหนดปุ่ม</string> + <string name="store_tab_discover">ค้นพบ</string> + <string name="store_tab_search">ค้นหา</string> + <string name="store_tab_installed">ติดตั้งแล้ว</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">สโตร์</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak พร้อมใช้งาน</string> + <string name="store_needs_runtime">ติดตั้งรันไทม์ Linux จากการตั้งค่าก่อน</string> + <string name="store_setup_title">ตั้งค่า Flatpak</string> + <string name="store_setup_text">เพิ่ม Flatpak และเชื่อมต่อกับ Flathub (ประมาณ 20 MB) แอปแรกจะดาวน์โหลดรันไทม์ของตัวเองด้วย</string> + <string name="store_setting_up">กำลังตั้งค่า…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">การอัปเดต</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d%%</string> + <string name="store_starting">กำลังเริ่ม…</string> + <string name="store_categories">หมวดหมู่</string> + <string name="store_unreachable">ไม่สามารถเชื่อมต่อ Flathub ได้ ตรวจสอบการเชื่อมต่อแล้วลองใหม่</string> + <string name="store_try_again">ลองใหม่</string> + <string name="store_search_label">ค้นหาใน Flathub</string> + <string name="store_search_placeholder">Firefox, เกม, โปรแกรมจำลอง…</string> + <string name="store_search">ค้นหา</string> + <string name="store_all">ทั้งหมด</string> + <string name="store_searching">กำลังค้นหา…</string> + <string name="store_search_hint">ค้นหาตามชื่อ หรือเลือกหมวดหมู่ แสดงเฉพาะแอป ARM64</string> + <string name="store_no_results">ไม่มีแอป ARM64 บน Flathub ที่ตรงกับคำค้นนี้</string> + <string name="store_results">ผลลัพธ์</string> + <string name="store_check_updates">ตรวจสอบการอัปเดต</string> + <string name="store_update_all">อัปเดตทั้งหมด (%1$d)</string> + <string name="store_all_current">● ทุกอย่างเป็นรุ่นล่าสุดแล้ว</string> + <string name="store_no_apps">ยังไม่มีแอป ค้นหาได้จากค้นพบหรือค้นหา</string> + <string name="store_setup_first_note">ตั้งค่า Flatpak ก่อนเพื่อติดตั้งแอป</string> + <string name="store_update_available_prefix">มีอัปเดต · %1$s</string> + <string name="store_open">เปิด</string> + <string name="store_details">รายละเอียด</string> + <string name="store_remove">ลบ</string> + <string name="store_press_again">กดอีกครั้งเพื่อลบ</string> + <string name="store_installing">กำลังติดตั้ง…</string> + <string name="store_install">ติดตั้ง</string> + <string name="store_setup_first">ตั้งค่า Flatpak ก่อน</string> + <string name="store_download_size">ดาวน์โหลด %1$s</string> + <string name="store_installed_chip">● ติดตั้งแล้ว</string> + <string name="store_details_failed">ไม่สามารถโหลดหน้า Flathub ของแอปนี้ได้</string> + <string name="store_loading">กำลังโหลดจาก Flathub…</string> + <string name="store_no_arm">ไม่มีบิลด์ ARM64 บน Flathub จึงติดตั้งที่นี่ไม่ได้</string> + <string name="store_screenshots">ภาพหน้าจอ</string> + <string name="store_about">เกี่ยวกับ</string> + <string name="store_app_id">รหัสแอป</string> + <string name="store_license">สัญญาอนุญาต</string> + <string name="store_runtime">รันไทม์</string> + <string name="store_installed_size">ขนาดที่ติดตั้ง</string> + <string name="store_installed_size_value">%1$s (รวมรันไทม์ที่ใช้ร่วมกัน)</string> + <string name="store_website">เว็บไซต์</string> + <string name="store_verified">ยืนยันแล้ว</string> + <string name="upd_title">การอัปเดต</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">ตรวจสอบล่าสุด %1$s</string> + <string name="upd_not_checked">ยังไม่ตรวจสอบ</string> + <string name="upd_perm_title">ให้ DroidDeck อัปเดตตัวเองได้</string> + <string name="upd_perm_text">ในหน้าจอถัดไป เปิด “อนุญาตจากแหล่งนี้” แล้วกลับมาที่นี่ การอัปเดตจะดำเนินต่อไปเอง</string> + <string name="upd_open_settings">เปิดการตั้งค่า</string> + <string name="upd_not_now">ไม่ใช่ตอนนี้</string> + <string name="upd_signed_status">ลงนามต่างกัน</string> + <string name="upd_signed_headline">อัปเดตทับไม่ได้</string> + <string name="upd_signed_detail">สำเนานี้ไม่ได้ลงนามด้วยคีย์รุ่นวางจำหน่ายของ DroidDeck จึงไม่สามารถติดตั้งอัปเดตทับได้ ถอนการติดตั้งเพื่อเปลี่ยน</string> + <string name="upd_available">มีอัปเดต</string> + <string name="upd_blocked_status">ติดตั้งไม่ได้</string> + <string name="upd_switch">พร้อมเปลี่ยน</string> + <string name="upd_ahead_status">นำหน้า Stable</string> + <string name="upd_ahead_headline">คุณนำหน้า Stable อยู่</string> + <string name="upd_ahead_detail">บิลด์นี้ใหม่กว่า Stable รุ่นล่าสุด คุณจะเปลี่ยนไปใช้ Stable เมื่อมีการเผยแพร่รุ่นถัดไป</string> + <string name="upd_gone_status">การทดสอบสิ้นสุดแล้ว</string> + <string name="upd_gone_headline">การทดสอบนี้สิ้นสุดแล้ว</string> + <string name="upd_gone_detail">การแก้ไขของมันถูกรวมเข้าหรือถูกยกเลิกไปแล้ว ติดตาม Preview เพื่อรับการแก้ไขล่าสุด</string> + <string name="upd_current_status">เป็นรุ่นล่าสุดแล้ว</string> + <string name="upd_current_headline">คุณมี %1$s รุ่นล่าสุดแล้ว</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">อัปเดต%1$s</string> + <string name="upd_install_button">ติดตั้ง%1$s</string> + <string name="upd_install_anyway">ติดตั้ง Stable %1$s อยู่ดี</string> + <string name="upd_follow_preview">ติดตาม Preview</string> + <string name="upd_stop_session">หยุดเซสชันที่กำลังทำงานอยู่เพื่ออัปเดต</string> + <string name="upd_less">น้อยลง</string> + <string name="upd_more">เพิ่มเติม</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">บิลด์ Preview ใหม่</string> + <string name="upd_new_test">บิลด์ทดสอบ PR #%1$d</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">ช่องทางอัปเดต</string> + <string name="upd_stable">Stable</string> + <string name="upd_stable_hint">รุ่นที่ผ่านการทดสอบแล้ว เหมาะกับคนส่วนใหญ่</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Preview</string> + <string name="upd_preview_hint">การแก้ไขล่าสุด ก่อนถึง Stable</string> + <string name="upd_preview_current">เป็นรุ่นล่าสุดแล้ว</string> + <string name="upd_tests">บิลด์ทดสอบ</string> + <string name="upd_tests_hint">ลองการแก้ไขก่อนเผยแพร่จริง</string> + <string name="upd_tests_none">ไม่มีในขณะนี้</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">ไม่ได้สร้างไว้สำหรับสำเนานี้ของ DroidDeck</string> + <string name="upd_updated">อัปเดตแล้ว %1$s</string> + <string name="upd_test_name">การทดสอบ PR #%1$d</string> + <string name="upd_while_ago">สักพักที่แล้ว</string> + <string name="upd_just_now">เมื่อสักครู่</string> + <plurals name="upd_tests_count"> + <item quantity="other">%1$d รายการให้ลอง</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="other">การเปลี่ยนแปลงตั้งแต่บิลด์ของคุณ · ล้าหลัง %1$d บิลด์</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="other">การเปลี่ยนแปลงล่าสุด · %1$d บิลด์</item> + </plurals> + <string name="gate_title">มีการตั้งค่า Android หนึ่งอย่างที่ต้องเปลี่ยน</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Android หยุดแอปที่เริ่มโพรเซสพื้นหลังจำนวนมาก และ Steam เริ่มหลายสิบโพรเซส จนกว่าจะเปลี่ยนค่านี้ เกมจะปิดตัวโดยไม่มีข้อผิดพลาด</string> + <string name="gate_fix_for_me">แก้ให้ฉันเลย</string> + <string name="gate_toggle_steps">เปิด “Disable child process restrictions” แล้วกลับมาที่นี่ เปิดตัวเลือกสำหรับนักพัฒนาไว้เสมอ ไม่เช่นนั้นสวิตช์จะปิดลงอีก</string> + <string name="gate_open_dev">เปิดตัวเลือกสำหรับนักพัฒนา</string> + <string name="gate_no_toggle">Android รุ่นนี้ไม่มีสวิตช์สำหรับสิ่งนี้ ดังนั้น DroidDeck จะเปลี่ยนค่าผ่าน Wireless debugging ใช้เวลาประมาณหนึ่งนาที ไม่ต้องใช้คอมพิวเตอร์</string> + <string name="gate_needs_wifi">Wireless debugging ต้องใช้ Wi-Fi เครือข่ายใดก็ได้ ไม่จำเป็นต้องมีอินเทอร์เน็ต</string> + <string name="gate_needs_notifications">รหัสจับคู่จะแสดงในการแจ้งเตือน อนุญาตการแจ้งเตือนสำหรับ DroidDeck หรือใช้วิธีอื่น</string> + <string name="gate_allow_notifications">อนุญาตการแจ้งเตือน</string> + <string name="gate_step1">เปิดการตั้งค่า → ตัวเลือกสำหรับนักพัฒนา → Wireless debugging แล้วเปิดใช้งาน</string> + <string name="gate_step2">แตะ “Pair device with pairing code”</string> + <string name="gate_step3">พิมพ์รหัสลงในการแจ้งเตือนของ DroidDeck</string> + <string name="gate_waiting">กำลังรอป๊อปอัปการจับคู่…</string> + <string name="gate_code_needed">พบป๊อปอัปแล้ว กรอกรหัสในการแจ้งเตือน</string> + <string name="gate_status">สถานะ</string> + <string name="gate_polling">กำลังตรวจสอบทุก 2 วินาที หน้านี้จะปิดเมื่อปิดใช้งานแล้ว</string> + <string name="gate_other_ways">วิธีอื่น</string> + <string name="gate_other_toggle">Wireless debugging, ที่อยู่แบบกำหนดเอง, ADB จากคอมพิวเตอร์</string> + <string name="gate_other_no_toggle">ที่อยู่แบบกำหนดเอง, ADB จากคอมพิวเตอร์, ตัวเลือกสำหรับนักพัฒนา</string> + <string name="gate_use_wireless_instead">ใช้ Wireless debugging แทน</string> + <string name="gate_some_roms">บาง ROM เพิ่มสวิตช์นี้มาเองเลย ลองหา “child process” ในตัวเลือกสำหรับนักพัฒนา</string> + <string name="gate_manual_hint">หากการแจ้งเตือนไม่ทำงาน ให้กรอกที่อยู่และรหัสจากป๊อปอัปด้วยตัวเอง</string> + <string name="gate_enter_manually">กรอกที่อยู่ด้วยตัวเอง</string> + <string name="gate_from_computer">จากคอมพิวเตอร์ที่มี ADB:</string> + <string name="gate_copy_command">คัดลอกคำสั่ง</string> + <string name="gate_choose_display">เลือกหน้าจอที่จะเปิดการตั้งค่า Android</string> + <string name="gate_main_screen">หน้าจอหลัก</string> + <string name="wadb_finding_port">จับคู่แล้ว กำลังค้นหาพอร์ตของ Wireless debugging…</string> + <string name="wadb_enter_port">จับคู่แล้ว กรอกพอร์ตจากการตั้งค่า</string> + <string name="wadb_applying">พบอุปกรณ์แล้ว กำลังปรับใช้การตั้งค่า…</string> + <string name="wadb_apply_failed">ไม่สามารถปรับใช้โดยอัตโนมัติได้ ตรวจสอบที่อยู่ด้านล่างแล้วลองใหม่ %1$s</string> + <string name="wadb_title_wireless">Wireless debugging</string> + <string name="wadb_title_done">อัปเดตขีดจำกัดโพรเซสลูกแล้ว</string> + <string name="wadb_title">เปลี่ยนขีดจำกัดโพรเซสลูก</string> + <string name="wadb_lede_compact0">หากไม่พบตัวเลือกนี้ ให้จับคู่ผ่าน Wireless debugging</string> + <string name="wadb_lede_compact1">จับคู่แล้ว ตรวจสอบที่อยู่ IP และพอร์ตใน Wireless debugging</string> + <string name="wadb_lede_done">อัปเดตขีดจำกัดโพรเซสลูกแล้ว</string> + <string name="wadb_lede0">ใช้ขั้นตอนที่ 2 หากตัวเลือกสำหรับนักพัฒนาไม่มีการตั้งค่าโพรเซสลูก เปิดตัวเลือกสำหรับนักพัฒนา → Wireless debugging → Pair device with pairing code</string> + <string name="wadb_lede1">จับคู่แล้ว ใช้ที่อยู่ด้านล่าง หรืออัปเดตจาก Wireless debugging → IP address & Port</string> + <string name="wadb_lede_done_long">อัปเดตขีดจำกัดโพรเซสลูกผ่าน Wireless debugging แล้ว</string> + <string name="wadb_confirmed_on">Android ยืนยันแล้วว่าขีดจำกัดเปิดอยู่</string> + <string name="wadb_confirmed_off">Android ยืนยันแล้วว่าขีดจำกัดปิดอยู่</string> + <string name="wadb_pair_group">2 · จับคู่ Wireless debugging</string> + <string name="wadb_pair_address">ที่อยู่ IP และพอร์ตของป๊อปอัปการจับคู่</string> + <string name="wadb_pin">รหัสจับคู่ (PIN)</string> + <string name="wadb_pin_placeholder">6 หลัก</string> + <string name="wadb_pair">จับคู่</string> + <string name="wadb_apply_group">2 · ปรับใช้ผ่าน Wireless debugging</string> + <string name="wadb_connect_address">ที่อยู่ IP และพอร์ตสำหรับเชื่อมต่อ</string> + <string name="wadb_pair_again">จับคู่อีกครั้ง</string> + <string name="wadb_turn_off">ปิดขีดจำกัด</string> + <string name="wadb_other_ways">วิธีอื่นในการเปลี่ยนค่านี้</string> + <string name="wadb_dev_try_first">1 · ตัวเลือกสำหรับนักพัฒนา (ลองก่อน)</string> + <string name="wadb_restrict_on">เปิด “Restrict child processes”</string> + <string name="wadb_restrict_off">ปิด “Restrict child processes”</string> + <string name="wadb_computer_last">3 · ADB จากคอมพิวเตอร์ (ทางเลือกสุดท้าย)</string> + <string name="wadb_only_if">ใช้เฉพาะเมื่อ Wireless debugging ใช้งานไม่ได้</string> + <string name="wadb_computer_group">3 · ADB จากคอมพิวเตอร์ (ทางเลือกสุดท้าย)</string> + <string name="ctrl_osc_hint">เมื่อปุ่มสัมผัสจะปรากฏในเซสชัน</string> + <string name="ctrl_color">สี</string> + <string name="ctrl_color_hint">สีของปุ่มบนหน้าจอ</string> + <string name="ctrl_custom">กำหนดเอง</string> + <string name="ctrl_opacity">ความโปร่งใส</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">ขนาดปุ่ม</string> + <string name="ctrl_size_hint" formatted="false">100% คือขนาดมาตรฐาน</string> + <string name="ctrl_stick_click">การกดสติ๊ก</string> + <string name="ctrl_stick_click_hint">แตะสองครั้งแล้วกดสติ๊กค้างไว้สำหรับ L3 หรือ R3</string> + <string name="ctrl_adaptive">สติ๊กแบบปรับอัตโนมัติ</string> + <string name="ctrl_rumble">การสั่นของเกม</string> + <string name="ctrl_steam_button">ปุ่ม Steam</string> + <string name="ctrl_qam_button">ปุ่ม QAM</string> + <string name="ctrl_keyboard_button">ปุ่มแป้นพิมพ์</string> + <string name="ctrl_select_steam">กด Select เพื่อเปิดเมนู Steam</string> + <string name="ctrl_select_steam_hint">แทน View/Select</string> + <string name="setup_animations">ภาพเคลื่อนไหว</string> + <string name="ctrl_adaptive_hint">สติ๊กจะปรากฏที่ตำแหน่งที่คุณแตะและซ่อนเมื่อปล่อย</string> + <string name="ctrl_layout">การจัดวาง</string> + <string name="ctrl_layout_custom">บันทึกตำแหน่งกำหนดเองไว้แล้ว</string> + <string name="ctrl_layout_default">ปรับให้พอดีกับหน้าจอนี้</string> + <string name="ctrl_edit">แก้ไข</string> + <string name="ctrl_reset">รีเซ็ต</string> + <string name="ctrl_mapping">การกำหนดปุ่ม</string> + <string name="ctrl_mapping_default">ทุกปุ่มส่งอินพุตของตัวเอง</string> + <string name="ctrl_mapping_count">กำหนดใหม่ %1$d จาก %2$d ปุ่ม</string> + <string name="ctrl_configure">ตั้งค่า</string> + <string name="ctrl_reset_all">รีเซ็ตคอนโทรลเลอร์</string> + <string name="ctrl_reset_all_hint">คืนค่าการตั้งค่าคอนโทรลเลอร์ทั้งหมดและการจัดวาง</string> + <string name="ctrl_mapping_lede">สิ่งที่ปุ่มบนหน้าจอแต่ละปุ่มส่งไปยังเกม ซ่อนจะเอาปุ่มนั้นออก</string> + <string name="ctrl_onscreen_buttons">ปุ่มบนหน้าจอ</string> + <string name="ctrl_defaults">ค่าเริ่มต้น</string> + <string name="ctrl_reset_mapping">รีเซ็ตการกำหนดปุ่ม</string> + <string name="content_android_apps">แอป Android</string> + <string name="content_apps_count">%1$d แอป</string> + <string name="content_no_android_apps">ไม่พบแอป Android ที่เปิดได้</string> + <string name="content_desktop_installed">● ติดตั้งเดสก์ท็อปแล้ว</string> + <string name="content_desktop_first_open">ติดตั้งเมื่อเปิดครั้งแรก</string> + <string name="content_available">พร้อมติดตั้ง</string> + <string name="content_not_installed">ยังไม่ติดตั้ง</string> + <string name="content_not_installed_suffix">%1$s · ยังไม่ติดตั้ง</string> + <string name="content_open_named">เปิด %1$s</string> + <string name="content_games">เกม</string> + <string name="content_choose_roms">เลือกโฟลเดอร์ ROM</string> + <string name="content_retroarch_roms">ไปที่ /root/ROMs ใน RetroArch</string> + <string name="content_add_roms">เพิ่มเกม %1$s ลงใน ROMs/%2$s</string> + <string name="content_install_named">ติดตั้ง %1$s</string> + <string name="content_stop_to_install">หยุดเซสชันเพื่อติดตั้ง</string> + <string name="content_runtime_required">ต้องมีรันไทม์</string> + <string name="content_loading_details">กำลังโหลดรายละเอียดการติดตั้ง…</string> + <string name="content_details_unavailable">รายละเอียดการติดตั้งไม่พร้อมใช้งาน ลองใหม่ในหลัง</string> + <string name="content_needs_runtime">ติดตั้งรันไทม์ Linux จากการตั้งค่าก่อน</string> + <string name="content_stop_first">หยุดเซสชันที่กำลังทำงานเพื่อติดตั้งแอปเดสก์ท็อป</string> + <string name="content_install_into">ติดตั้ง %1$s ลงในเดสก์ท็อป Linux</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">การซ่อนจะเอาออกจากเดสก์ท็อป ไฟล์ของมันยังอยู่ในรันไทม์ Linux</string> + <string name="content_rom_gone">เกมนั้นไม่มีอยู่ในโฟลเดอร์ ROM แล้ว</string> + <string name="content_desktop_system">เดสก์ท็อป · %1$s</string> + <string name="content_launch_in">เปิดใน %1$s</string> + <string name="content_also_in">มีอยู่ใน %1$s ด้วย</string> + <string name="content_page_gone">หน้านั้นหายไปแล้ว เลือกส่วนทางด้านซ้าย</string> + <string name="content_open_desktop">เปิดเดสก์ท็อป</string> + <string name="content_install_open_desktop">ติดตั้งและเปิดเดสก์ท็อป</string> + <string name="desktop_remove_title">ถอนการติดตั้งเดสก์ท็อป</string> + <string name="desktop_remove_text">ลบเดสก์ท็อปและแอปที่มาพร้อมกัน เกม แอปที่เพิ่มเข้ามา การตั้งค่า และไลบรารีที่ใช้ร่วมกันจะยังคงอยู่</string> + <string name="desktop_removing">กำลังลบเดสก์ท็อป</string> + <string name="desktop_removed">ลบเดสก์ท็อปแล้ว</string> + <string name="desktop_cleaning_legacy">กำลังลบไฟล์ LXQt เก่า</string> + <string name="desktop_removal_unknown">อัปเดต DroidDeck ก่อนลบเดสก์ท็อปรุ่นนี้</string> + <string name="content_linux_desktop">เดสก์ท็อป Linux</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox และโปรแกรมจำลองของคุณ</string> + <string name="games_empty">เกมที่คุณติดตั้งใน Steam จะปรากฏที่นี่หลังจากจบเซสชัน</string> + <string name="games_play_steam">เล่น Steam</string> + <string name="games_launch_settings">การตั้งค่าการเปิดเกม</string> + <string name="games_launch">เปิดเกม</string> + <string name="games_files">ไฟล์เกม</string> + <string name="games_prefix">Prefix ของ Proton</string> + <string name="games_saves_looking">กำลังค้นหาเซฟ…</string> + <string name="games_saves_none">ไม่พบโฟลเดอร์เซฟที่ตรงชื่อ การส่งออกจะนำทั้งโฟลเดอร์ผู้ใช้ไป</string> + <string name="games_saves_folders">%1$d โฟลเดอร์เซฟ</string> + <string name="games_saves_summary">%1$s · %2$d ไฟล์ · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">จัดการเซฟ</string> + <string name="games_saves">เซฟของเกม</string> + <string name="games_import_saves">นำเข้าเซฟ…</string> + <string name="games_export_gamehub">ส่งออกเป็น GameHub zip…</string> + <string name="games_export_winlator">ส่งออกเป็น Winlator zip…</string> + <string name="games_open_save_folder">เปิดโฟลเดอร์เซฟ</string> + <string name="games_never_played">ยังไม่เคยเล่น</string> + <string name="games_runtime_busy_percent">รันไทม์กำลังทำงาน · %1$d%%</string> + <string name="games_runtime_busy">รันไทม์กำลังทำงาน</string> + <string name="games_runtime_first_play">รันไทม์จะติดตั้งเมื่อกด Play ครั้งแรก</string> + <string name="games_runtime_update">มีอัปเดตรันไทม์</string> + <string name="games_runtime_ready">● รันไทม์พร้อมใช้งาน</string> + <string name="games_last_played">เล่นล่าสุด %1$s</string> + <string name="games_internal">พื้นที่จัดเก็บภายใน</string> + <string name="games_added">เกมที่เพิ่ม</string> + <string name="games_custom">เกมกำหนดเอง</string> + <string name="games_store_storage">พื้นที่จัดเก็บเกม</string> + <string name="gpu_auto_hint">คู่ที่ดีที่สุดสำหรับ GPU นี้ อัปเดตให้ล่าสุดเสมอ</string> + <string name="gpu_manual_hint">เลือกคู่ด้วยตัวเอง</string> + <string name="gpu_checking">กำลังตรวจสอบไดรเวอร์…</string> + <string name="gpu_none_listed">ยังไม่มีไดรเวอร์ในรายการ รีเฟรชเพื่อค้นหา</string> + <string name="gpu_hide_other">ซ่อนไดรเวอร์สำหรับ GPU อื่น</string> + <string name="gpu_show_other">แสดงไดรเวอร์สำหรับ GPU อื่น</string> + <string name="gpu_advanced">ขั้นสูง</string> + <string name="gpu_set_with_display">ตั้งค่าร่วมกับไดรเวอร์แสดงผล: %1$s รวมทั้งสองอย่าง</string> + <string name="gpu_set_with_runtime">ตั้งค่าร่วมกับไดรเวอร์รันไทม์: %1$s รวมทั้งสองอย่าง</string> + <string name="gpu_runtime_hint">ใช้โดยเกมและเดสก์ท็อป</string> + <string name="gpu_runtime_default">ค่าเริ่มต้นของรันไทม์</string> + <string name="gpu_display_hint">วาดเฟรมบนหน้าจอ จะมีผลหลัง DroidDeck รีสตาร์ท</string> + <string name="gpu_auto_by_gpu">อัตโนมัติ - เลือกตาม GPU</string> + <string name="gpu_manage">ดาวน์โหลด นำเข้า หรือลบ…</string> + <string name="gpu_in_use_caps">กำลังใช้งาน</string> + <string name="gpu_builtin">ไดรเวอร์ที่มีในตัว DroidDeck</string> + <string name="gpu_matched_pair">ไดรเวอร์รันไทม์และแสดงผลตั้งค่าร่วมกันเป็นคู่ที่เข้ากัน</string> + <string name="gpu_recommended_for">แนะนำสำหรับ GPU นี้: %1$s</string> + <string name="gpu_refresh_hint">รีเฟรชเพื่อค้นหาไดรเวอร์สำหรับ GPU นี้</string> + <string name="gpu_recommended_caps">แนะนำ</string> + <string name="gpu_unavailable">ไม่พร้อมใช้งาน</string> + <string name="load_runtime">รันไทม์ Linux</string> + <string name="load_runtime_detail">กำลังเตรียมรันไทม์ Linux</string> + <string name="load_session">เซสชัน</string> + <string name="load_session_detail">กำลังเริ่มการแสดงผลและเสียง</string> + <string name="load_steam">ไคลเอนต์ Steam</string> + <string name="load_steam_detail">กำลังตรวจสอบ Steam และเครื่องมือความเข้ากันได้</string> + <string name="load_opening_steam">กำลังเปิด Steam</string> + <string name="load_opening_steam_detail">กำลังรอเฟรมแรกของ Steam</string> + <string name="load_desktop">เดสก์ท็อป</string> + <string name="load_desktop_detail">กำลังเตรียมเดสก์ท็อป</string> + <string name="load_opening">กำลังเปิด</string> + <string name="load_opening_detail">กำลังรอเฟรมแรก</string> + <string name="load_starting_steam">กำลังเริ่ม Steam</string> + <string name="load_ended">เซสชันสิ้นสุดแล้ว</string> + <string name="load_step">ขั้นตอนที่ %1$d จาก %2$d</string> + <string name="load_steam_paused">Steam หยุดชั่วคราว</string> + <string name="deck_tab_grips">ปุ่มด้านหลัง</string> + <string name="deck_tab_left">แพดซ้าย</string> + <string name="deck_tab_right">แพดขวา</string> + <string name="deck_tab_both">ทั้งสองข้าง</string> + <string name="deck_tab_gamepad">เกมแพด</string> + <string name="deck_surface_stick">สติ๊ก</string> + <string name="deck_surface_trackpad">แทร็กแพด</string> + <string name="deck_surface_switch">สลับระหว่างสติ๊กและแทร็กแพด</string> + <string name="deck_more_controls">การควบคุม Deck เพิ่มเติม</string> + <string name="deck_panels">แผง</string> + <string name="deck_close">ปิดการควบคุมหน้าจอที่สอง</string> + <string name="deck_open_steam_menu">เปิดเมนู Steam</string> + <string name="deck_left_upper">ซ้าย · บน</string> + <string name="deck_left_lower">ซ้าย · ล่าง</string> + <string name="deck_right_upper">ขวา · บน</string> + <string name="deck_right_lower">ขวา · ล่าง</string> + <string name="deck_click">คลิก</string> + <string name="deck_right_trackpad">แทร็กแพดขวา</string> + <string name="deck_left_trackpad">แทร็กแพดซ้าย</string> + <string name="second_trackpad">แทร็กแพด</string> + <string name="second_show_keyboard">แสดงแป้นพิมพ์</string> + <string name="second_hide_keyboard">ซ่อนแป้นพิมพ์</string> + <string name="second_terminal">เทอร์มินัล Linux</string> + <string name="second_terminal_not_ready">เซสชัน Linux ยังไม่พร้อมสำหรับเทอร์มินัล</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">เกม</string> + <string name="rail_desktop">เดสก์ท็อป</string> + <string name="rail_store">สโตร์</string> + <string name="rail_components">Components</string> + <string name="rail_apps">แอป</string> + <string name="rail_setup">การตั้งค่า</string> + <string name="rail_updates">การอัปเดต</string> + <string name="rail_percent">%1$d%%</string> + <string name="rail_resume_named">ดำเนินการต่อ %1$s</string> + <string name="roms_text">แสดงเป็น /root/ROMs ในแต่ละเซสชัน พื้นที่จัดเก็บภายในอยู่ที่ /root/Storage</string> + <string name="roms_none">ยังไม่เลือกโฟลเดอร์</string> + <string name="roms_applies">มีผลในเซสชันถัดไป ใช้การ์ด SD ได้</string> + <string name="roms_choose">เลือกโฟลเดอร์</string> + <string name="roms_change">เปลี่ยนโฟลเดอร์</string> + <string name="proton_lede">ติดตั้งบิลด์ Proton แบบ ARM64 แล้วเลือกให้แต่ละเกมใน Steam → Properties → Compatibility</string> + <string name="proton_stop_first">หยุดเซสชันที่กำลังทำงานเพื่อติดตั้งหรือลบเครื่องมือความเข้ากันได้</string> + <string name="proton_available">บิลด์ที่พร้อมใช้งาน</string> + <string name="proton_installed">ติดตั้ง %1$s แล้ว</string> + <string name="proton_queued">รอดำเนินการจากคำขอก่อนหน้า</string> + <string name="proton_not_installed">ยังไม่ติดตั้ง</string> + <string name="proton_update_available">มีอัปเดต → %1$s</string> + <string name="proton_remove_older">ลบรุ่นเก่าของ %1$s</string> + <string name="proton_remove_older_body">ลบ %1$s เกมที่ตั้งไว้ให้ใช้รุ่นเหล่านี้ต้องเปลี่ยนไปใช้ Proton อื่น</string> + <string name="proton_remove_body">เกมที่ตั้งไว้ให้ใช้รุ่นนี้ต้องเปลี่ยนไปใช้ Proton อื่น</string> + <string name="proton_install_now">ติดตั้งตอนนี้</string> + <string name="proton_cancel_queued">ยกเลิกรายการที่รออยู่</string> + <string name="pckb_android">แป้นพิมพ์ Android</string> + <string name="pckb_hide">ซ่อน ✕</string> + <string name="main_launch_named">เปิด %1$s</string> + <string name="main_choose_screen">เลือกหน้าจอ หน้าจอที่สอง: %1$s</string> + <string name="main_secondary_gone">หน้าจอที่สองไม่พร้อมใช้งานแล้ว</string> + <string name="main_primary">หน้าจอหลัก</string> + <string name="main_secondary">หน้าจอที่สอง</string> + <string name="widgets_prev_tab">แท็บก่อนหน้า</string> + <string name="widgets_next_tab">แท็บถัดไป</string> + <string name="widgets_on">เปิด</string> + <string name="widgets_all_n">ทั้งหมด %1$d</string> + <string name="widgets_none">ไม่มี</string> + <string name="widgets_n_of_m">%1$d จาก %2$d</string> + <string name="widgets_off">ปิด</string> + <string name="screen_bottom">หน้าจอด้านล่าง</string> + <string name="game_env_applies">มีผลในการเปิดเกมครั้งถัดไป โปรไฟล์ของเกมจะมีสิทธิ์เหนือค่าที่ใช้ร่วมกัน</string> + <string name="game_env_other">เกมอื่นตามรหัสแอป…</string> + <string name="game_env_done">เสร็จสิ้น</string> + <string name="game_env_default">ค่าเริ่มต้น</string> + <string name="game_env_off">ปิด</string> + <string name="game_env_change">เปลี่ยน…</string> + <string name="game_env_change_title">เปลี่ยนตัวแปร</string> + <string name="game_env_restore_default">กลับไปใช้ค่าเริ่มต้น</string> + <string name="game_env_eyebrow">เกม Proton</string> + <string name="pip_title">ภาพซ้อนภาพ</string> + <string name="pip_auto">เข้าสู่ภาพซ้อนภาพโดยอัตโนมัติ</string> + <string name="unfolded_controls">การควบคุมบนหน้าจอที่กางออก</string> + <string name="unfolded_controls_hint">เมื่อกางออก เกมจะอยู่เหนือแทร็กแพดและปุ่มด้านหลังของ Steam Deck เมื่อพับ เกมจะแสดงเต็มจอ</string> + <string name="pip_suspended">หยุดชั่วคราว</string> + <string name="pip_not_ready">ภาพซ้อนภาพจะพร้อมใช้งานเมื่อเซสชันพร้อม</string> + <string name="pip_permission">อนุญาตภาพซ้อนภาพสำหรับ DroidDeck ในการตั้งค่า Android</string> + <string name="pip_unavailable">Android ไม่สามารถเปิดภาพซ้อนภาพได้</string> + <string name="game_shortcut">ช็อตคัต</string> + <string name="game_shortcut_add">เพิ่มไปยังหน้าจอหลัก</string> + <string name="game_shortcut_choose">เลือกเกม</string> + <string name="game_shortcut_scanning">กำลังสแกนเกมที่ติดตั้งไว้…</string> + <string name="game_link_copy">คัดลอกลิงก์เปิดเกม</string> + <string name="game_link_copied">คัดลอกลิงก์เปิดเกมแล้ว</string> + <string name="game_shortcut_unsupported">ตัวเปิดแอปนี้ไม่สามารถเพิ่มช็อตคัตได้ ใช้คัดลอกลิงก์เปิดเกมแทน</string> + <string name="game_link_invalid">ลิงก์เปิดเกมไม่ถูกต้อง</string> + <string name="game_link_missing">เกมนี้ไม่มีอยู่ในไลบรารี DroidDeck ของคุณแล้ว</string> + <string name="game_link_session_busy">หยุดเซสชันปัจจุบันก่อนเปิดเกม Steam</string> + <string name="game_link_failed">ไม่สามารถส่งเกมไปยัง Steam ได้ ลองเปิดอีกครั้ง</string> + <string name="setup_retry_removal">ลองลบใหม่</string> + <string name="runtime_removing">กำลังลบ</string> + <string name="runtime_removal_incomplete">การลบไม่สมบูรณ์</string> + <string name="game_file_export">ส่งออกไฟล์เกม</string> + <string name="game_file_sync">ซิงค์ไฟล์เกมทั้งหมดไปยังโฟลเดอร์</string> + <string name="game_file_stop_sync">หยุดการซิงค์ไฟล์เกม</string> + <string name="game_file_folder">เลือกโฟลเดอร์สำหรับไฟล์เกมของฟรอนต์เอนด์</string> + <string name="game_file_exported">บันทึกไฟล์เกมไปยัง %1$s แล้ว</string> + <string name="game_frontend_files">ไฟล์ของฟรอนต์เอนด์</string> + + <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> + <string name="main_saves_importing">กำลังนำเข้าสู่ %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="other">นำเข้า %1$d ไฟล์จาก %2$s สู่ %3$s แล้ว</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="other">นำเข้า %1$d ไฟล์จาก %2$s สู่ %3$s แล้ว เซฟเก่าสำรองไว้ที่ Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">เลือกไฟล์ zip เซฟสำหรับ %1$s</string> + <string name="main_saves_exporting">กำลังส่งออก %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="other">ส่งออก %1$d ไฟล์เป็น %2$s: %3$s แล้ว</item> + </plurals> + <string name="main_saves_pick_folder">เลือกตำแหน่งบันทึก %1$s (%2$s)</string> + <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> + <plurals name="main_logs_cleared"> + <item quantity="other">ล้างโฟลเดอร์ล็อกเซสชัน %1$d โฟลเดอร์แล้ว</item> + </plurals> + <string name="main_wadb_pair_failed">การจับคู่ Wireless debugging ล้มเหลว</string> + <string name="main_wadb_command_failed">คำสั่ง Wireless debugging ล้มเหลว</string> + <string name="main_wadb_unavailable">Wireless debugging ไม่พร้อมใช้งาน</string> + <string name="main_adb_copied">คัดลอกคำสั่ง ADB แล้ว</string> + <string name="main_pick_roms">เลือกโฟลเดอร์ ROM</string> + <string name="main_non_adreno_title">ไม่ใช่ GPU Adreno</string> + <string name="main_non_adreno_text">Turnip รองรับ GPU Adreno บน %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> + <string name="main_applied">นำมาใช้แล้ว: %1$s</string> + <string name="android_app_open_failed_primary">ไม่สามารถเปิด %1$s บนหน้าจอหลักได้</string> + <string name="android_app_open_failed_on">ไม่สามารถเปิด %1$s บน %2$s ได้</string> + <string name="android_app_open_failed_display">ไม่สามารถเปิด %1$s บนหน้าจอ %2$d ได้</string> + <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> + <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> + <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> + <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">เลือกไฟล์ ZIP พลักอิน Decky</string> + <string name="main_storage_not_usable">ใช้งานไม่ได้: %1$s</string> + <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> + + <string name="upd_unknown_apps_failed">ไม่สามารถเปิดการตั้งค่า “Install unknown apps” ของ Android ได้</string> + <string name="upd_couldnt_check">ไม่สามารถตรวจสอบได้: %1$s</string> + <string name="upd_no_download">บิลด์นี้ไม่มีให้ดาวน์โหลดสำหรับสำเนานี้ของ DroidDeck</string> + <string name="upd_stop_first">หยุดเซสชันที่กำลังทำงานก่อน แล้วจึงอัปเดต</string> + <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> + + <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> + + <string name="drivers_not_checked">ยังไม่ตรวจสอบ - แตะรีเฟรชเพื่อค้นหาไดรเวอร์ใหม่</string> + <string name="drivers_bundle_sets_display">Android + Linux: ตั้งไดรเวอร์แสดงผลไปด้วย</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: ตั้งไดรเวอร์รันไทม์ไปด้วย</string> + <string name="drivers_runtime_builtin">Turnip ที่มีในตัวรันไทม์</string> + <string name="drivers_system_vulkan">Vulkan ของระบบ: ไม่มีบิลด์ที่รวมไว้สำหรับ GPU นี้</string> + <string name="drivers_bundled">%1$s (รวมมาด้วย)</string> + <string name="drivers_manual">กำหนดเอง</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">มีการเผยแพร่เพียงครึ่งหนึ่งในขณะนี้</string> + <string name="drivers_pair_size">%1$.0f MB สำหรับทั้งคู่</string> + <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> + <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> + <string name="drivers_switched_to">สลับเป็น %1$s %2$s แล้ว</string> + <string name="drivers_switched_to_restart">สลับเป็น %1$s %2$s แล้ว ไดรเวอร์แสดงผลจะมีผลหลัง DroidDeck รีสตาร์ท</string> + <string name="drivers_using">กำลังใช้ %1$s %2$s</string> + <string name="drivers_using_restart">กำลังใช้ %1$s %2$s ไดรเวอร์แสดงผลจะมีผลหลัง DroidDeck รีสตาร์ท</string> + <string name="drivers_not_a_driver">ไม่ใช่ไฟล์ zip ไดรเวอร์: ไม่ใช่ไดรเวอร์ AdrenoTools, Turnip -Linux หรือแพ็ก Android + Linux</string> + <string name="drivers_import_failed">นำเข้าล้มเหลว: %1$s</string> + <string name="drivers_imported_unnamed">นำเข้าไดรเวอร์แล้ว</string> + <string name="drivers_using_bundle">กำลังใช้ %1$s เป็นทั้งไดรเวอร์รันไทม์และแสดงผล</string> + <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> + <string name="drivers_release_status">ล่าสุด: %1$s · ตรวจสอบเมื่อ %2$s</string> + <string name="drivers_release_status_failed">ล่าสุด: %1$s · %2$s เข้าถึงไม่ได้ · ตรวจสอบเมื่อ %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="other">%1$d นาทีที่แล้ว</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="other">%1$d ชั่วโมงที่แล้ว</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="other">%1$d วันที่แล้ว</item> + </plurals> + <string name="drivers_checking_sources">กำลังตรวจสอบ Banners-Turnip, WinNative และ DroidDeck…</string> + <string name="drivers_check_failed">ตรวจสอบล้มเหลว</string> + <string name="drivers_installed_pick_either">ติดตั้ง %1$s แล้ว - เลือกจากรายการใดก็ได้เพื่อตั้งค่าไดรเวอร์ทั้งสอง</string> + <string name="drivers_installed_pick">ติดตั้ง %1$s แล้ว - เลือกจากเมนู</string> + + <string name="session_wait_removal">รอให้การลบรันไทม์เสร็จสิ้น</string> + <string name="session_title_game">เกม</string> + <string name="session_touch_auto_touchpad">ทัชแพด</string> + <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> + <string name="session_named_paused">%1$s หยุดชั่วคราว</string> + <string name="session_program_paused">โปรแกรมหยุดชั่วคราว</string> + <string name="session_starting_desktop">กำลังเริ่มเดสก์ท็อป</string> + <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> + <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> + <string name="session_runtime_unpacking">กำลังแตกไฟล์รันไทม์ Linux</string> + <string name="session_desktop_downloading">กำลังดาวน์โหลดเดสก์ท็อป</string> + <string name="session_desktop_downloading_mb">กำลังดาวน์โหลดเดสก์ท็อป · %1$d จาก %2$d MB</string> + <string name="session_desktop_checking">กำลังตรวจสอบเดสก์ท็อป</string> + <string name="session_desktop_unpacking">กำลังแตกไฟล์เดสก์ท็อป</string> + <string name="session_proton_downloading">กำลังดาวน์โหลด Proton Experimental (ARM64)</string> + <string name="session_proton_downloading_mb">กำลังดาวน์โหลด Proton Experimental (ARM64) · %1$d จาก %2$d MB</string> + <string name="session_proton_checking">กำลังตรวจสอบ Proton Experimental (ARM64)</string> + <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> + <string name="session_desktop_install_failed">เดสก์ท็อปติดตั้งไม่สำเร็จ (%1$s) ตรวจสอบการเชื่อมต่อแล้วกดเดสก์ท็อปอีกครั้ง</string> + <string name="session_ended_named">%1$s หยุดทำงานกะทันหัน แชร์ล็อกพร้อมรายงานข้อบกพร่อง หรือลองใหม่</string> + <string name="session_ended_program">โปรแกรมหยุดทำงานกะทันหัน แชร์ล็อกพร้อมรายงานข้อบกพร่อง หรือลองใหม่</string> + <string name="session_ended_desktop">เดสก์ท็อปหยุดทำงานกะทันหัน แชร์ล็อกพร้อมรายงานข้อบกพร่อง หรือลองใหม่</string> + <string name="session_ended_detail">สถานะออก %1$d · %2$s</string> + <string name="session_ended_no_log">ไม่มีล็อก</string> + <plurals name="session_hint_enosys"> + <item quantity="other">ล็อกแสดงข้อผิดพลาด “Function not implemented” %1$d รายการ: การเร่งความเร็ว seccomp ของ proot ทำให้ตัวช่วยบนอุปกรณ์นี้ล้มเหลว ลอง %2$s → “%3$s” (หรือ “%4$s”) แล้วเริ่มใหม่</item> + </plurals> + + <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> + <string name="store_cat_games">เกม</string> + <string name="store_cat_audio_video">เสียงและวิดีโอ</string> + <string name="store_cat_graphics">กราฟิก</string> + <string name="store_cat_internet">อินเทอร์เน็ต</string> + <string name="store_cat_office">สำนักงาน</string> + <string name="store_cat_developer">นักพัฒนา</string> + <string name="store_cat_education">การศึกษา</string> + <string name="store_cat_science">วิทยาศาสตร์</string> + <string name="store_cat_system">ระบบ</string> + <string name="store_cat_utilities">ยูทิลิตี</string> + <string name="store_section_popular">ได้รับความนิยม</string> + <string name="store_section_trending">กำลังเป็นที่นิยม</string> + <string name="store_section_new">ใหม่</string> + <string name="store_section_updated">อัปเดตแล้ว</string> + <string name="store_section_loading">กำลังโหลด…</string> + <string name="store_flatpak_setup">การตั้งค่า Flatpak</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">ไม่สามารถตรวจสอบการอัปเดตจาก Flathub ได้</string> + <string name="content_rom_installed">ติดตั้งแล้ว</string> + <string name="content_rom_folder">โฟลเดอร์</string> + <string name="theme_graphite">Graphite</string> + <string name="theme_graphite_detail">พื้นผิวสีเทาแกรไฟต์ พร้อมสีฟ้าของโลโก้สำหรับทุกสิ่งที่ถูกเลือก โฟกัส หรือกด</string> + <string name="theme_paper">กระดาษบนพื้นดำ</string> + <string name="theme_paper_detail">สีขาวเป็นหลัก สีฟ้าของไอคอนเป็นสัญญาณสำหรับเส้น วงแหวน และจุด</string> + <string name="theme_icon_blue">ฟ้าไอคอน</string> + <string name="theme_icon_blue_detail">พื้นดำ ตัวอักษรขาว สีฟ้าสำหรับทุกสิ่งที่ถูกเลือก โฟกัส หรือกด</string> + <string name="theme_electric">กรมท่าไฟฟ้า</string> + <string name="theme_electric_detail">สีฟ้าสว่างขึ้น บนพื้นผิวที่เอียงไปทางกรมท่ามากกว่าเทา</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Space</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <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">ไฟล์ manifest ของ Steam ไม่พร้อมใช้งาน</string> + <string name="content_system_many">หลายระบบ</string> + <string name="fm_drive_internal">ภายใน</string> + <string name="fm_drive_linux">ระบบ Linux</string> + <string name="fm_drive_sd">การ์ด SD</string> + <string name="fm_drive_storage">พื้นที่จัดเก็บ</string> + <string name="fm_select">เลือก</string> + <string name="fm_properties">คุณสมบัติ</string> + <string name="fm_favorite_add">เพิ่มในรายการโปรด</string> + <string name="fm_favorite_remove">ลบจากรายการโปรด</string> + <string name="fm_rename">เปลี่ยนชื่อ</string> + <string name="fm_copy">คัดลอก</string> + <string name="fm_cut">ตัด</string> + <string name="fm_actions">การดำเนินการ</string> + <string name="fm_favorites">รายการโปรด</string> + <string name="fm_pin_current">ปักหมุดโฟลเดอร์นี้</string> + <string name="fm_no_favorites">ยังไม่มีรายการโปรด - ปักหมุดโฟลเดอร์ด้วยเมนู ⋮ เพื่อย้อนกลับมาที่นี่ได้เร็ว</string> + <string name="fm_unpin_favorite">ลบออกจากรายการโปรด</string> + <string name="fm_size_zero">0 ไบต์</string> + <string name="fm_unit_bytes">ไบต์</string> + <string name="fm_prop_location">ตำแหน่ง</string> + <string name="fm_prop_type">ชนิด</string> + <string name="fm_prop_size">ขนาด</string> + <string name="fm_prop_modified">แก้ไขล่าสุด</string> + <string name="fm_type_folder">โฟลเดอร์</string> + <string name="fm_type_file">ไฟล์</string> + <string name="fm_type_ext_file">ไฟล์ %1$s</string> + <string name="fm_read_only">อ่านอย่างเดียว</string> + <string name="fm_read_only_hint">ป้องกันไม่ให้เกม (และ Wine) เขียนทับหรือลบไฟล์นี้</string> + <string name="fm_read_only_failed">ไม่สามารถเปลี่ยนสถานะอ่านอย่างเดียวได้</string> + <string name="fm_hidden">ซ่อน</string> + <string name="fm_hidden_unsupported">พื้นที่จัดเก็บนี้ไม่สามารถเก็บสถานะซ่อนได้</string> + <string name="fm_hidden_hint">ตั้งค่าไฟล์เป็นซ่อนสำหรับ Windows (แอตทริบิวต์ซ่อนของ Wine)</string> + <string name="fm_hidden_failed">ไม่สามารถเปลี่ยนสถานะซ่อนบนพื้นที่จัดเก็บนี้ได้</string> + <string name="fm_done">เสร็จสิ้น</string> + <string name="fm_op_deleting">กำลังลบ...</string> + <string name="fm_delete_failed">ลบล้มเหลว</string> + <string name="fm_op_moving">กำลังย้าย...</string> + <string name="fm_op_copying">กำลังคัดลอก...</string> + <string name="fm_op_moving_item">กำลังย้าย - %1$s</string> + <string name="fm_op_copying_item">กำลังคัดลอก - %1$s</string> + <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> + <string name="fm_paste_failed">เสร็จ %1$d รายการ ล้มเหลว %2$d รายการ</string> + <string name="fm_paste_skipped">เสร็จ %1$d รายการ ข้าม %2$d รายการ</string> + <plurals name="fm_paste_moved"> + <item quantity="other">ย้าย %1$d รายการแล้ว</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="other">คัดลอก %1$d รายการแล้ว</item> + </plurals> + <string name="fm_already_exists">“%1$s” มีอยู่แล้ว</string> + <string name="fm_op_renaming">กำลังเปลี่ยนชื่อ...</string> + <string name="fm_rename_failed">เปลี่ยนชื่อล้มเหลว</string> + <string name="fm_op_creating_folder">กำลังสร้างโฟลเดอร์...</string> + <string name="fm_create_folder_failed">ไม่สามารถสร้างโฟลเดอร์ได้</string> + <string name="fm_new_folder">โฟลเดอร์ใหม่</string> + <string name="fm_folder_name">ชื่อโฟลเดอร์</string> + <string name="fm_create">สร้าง</string> + <string name="fm_new_name">ชื่อใหม่</string> + <string name="fm_delete_title">ลบหรือไม่?</string> + <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> + <plurals name="fm_and_more"> + <item quantity="other">…และอีก %1$d รายการ</item> + </plurals> + <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> + </plurals> + <string name="fm_conflict_merge_hint">ผสานจะเพิ่มและแทนที่ไฟล์ภายในโฟลเดอร์ที่มีอยู่</string> + <string name="fm_conflict_overwrite_hint">เขียนทับจะแทนที่ไฟล์ที่มีอยู่</string> + <string name="fm_conflict_apply_all">ใช้กับความขัดแย้งทั้งหมด</string> + <string name="fm_conflict_merge">ผสาน</string> + <string name="fm_conflict_overwrite">เขียนทับ</string> + <string name="fm_conflict_keep_both">เก็บไว้ทั้งสอง</string> + <string name="fm_conflict_skip">ข้าม</string> + <string name="fm_select_this_folder">เลือกโฟลเดอร์นี้</string> + <string name="fm_back">กลับ</string> + <string name="fm_drive_not_readable">%1$s ติดตั้งอยู่แต่อ่านไม่ได้ในขณะนี้</string> + <string name="fm_list_view">มุมมองรายการ</string> + <string name="fm_grid_view">มุมมองตาราง</string> + <string name="fm_search">ค้นหา</string> + <string name="fm_sort">จัดเรียง</string> + <string name="fm_sort_name">ชื่อ</string> + <string name="fm_sort_date">วันที่แก้ไข</string> + <string name="fm_rows_comfortable">แถวแบบสบาย</string> + <string name="fm_rows_compact">แถวแบบกระชับ</string> + <string name="fm_hide_hidden">ซ่อนไฟล์ที่ซ่อนอยู่</string> + <string name="fm_show_hidden">แสดงไฟล์ที่ซ่อนอยู่</string> + <string name="fm_hide_favorites">ซ่อนรายการโปรด</string> + <string name="fm_show_favorites">แสดงรายการโปรด</string> + <string name="fm_filter_folder">กรองโฟลเดอร์นี้</string> + <string name="fm_free_space">เหลือ %1$s</string> + <string name="fm_selected_count">เลือกแล้ว %1$d รายการ</string> + <string name="fm_select_none">ไม่มี</string> + <string name="fm_select_all">ทั้งหมด</string> + <string name="fm_paste">วาง</string> + <plurals name="fm_items"> + <item quantity="other">%1$d รายการ</item> + </plurals> + <string name="fm_paste_here">วาง %1$s ที่นี่</string> + <string name="fm_paste_move_here">วาง %1$s (ย้าย) ที่นี่</string> + <string name="fm_rail_title">ไฟล์</string> + <string name="fm_rail_storage">พื้นที่จัดเก็บ</string> + <string name="fm_rail_quick">ด่วน</string> + <string name="fm_rail_favorites">รายการโปรด</string> + <string name="fm_rail_downloads">ดาวน์โหลด</string> + <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> + <string name="fm_favorite_added">เพิ่ม “%1$s” ในรายการโปรดแล้ว</string> + <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> + <string name="ctrl_tint_steam_blue">ฟ้า Steam</string> + <string name="ctrl_tint_sky">ฟ้าสดใส</string> + <string name="ctrl_tint_white">ขาว</string> + <string name="ctrl_tint_violet">ม่วง</string> + <string name="ctrl_tint_green">เขียว</string> + <string name="ctrl_tint_amber">อำพัน</string> + <string name="ctrl_tint_red">แดง</string> + <string name="ctrl_tint_pink">ชมพู</string> + <string name="ctrl_button_a">ปุ่ม A</string> + <string name="ctrl_button_b">ปุ่ม B</string> + <string name="ctrl_button_x">ปุ่ม X</string> + <string name="ctrl_button_y">ปุ่ม Y</string> + <string name="ctrl_button_lb">ปุ่มกระดกซ้าย</string> + <string name="ctrl_button_rb">ปุ่มกระดกขวา</string> + <string name="ctrl_button_lt">ทริกเกอร์ซ้าย</string> + <string name="ctrl_button_rt">ทริกเกอร์ขวา</string> + <string name="ctrl_button_view">ปุ่ม View</string> + <string name="ctrl_button_menu">ปุ่ม Menu</string> + <string name="ctrl_target_view">View</string> + <string name="ctrl_target_menu">Menu</string> + <string name="ctrl_target_up">D-pad ขึ้น</string> + <string name="ctrl_target_down">D-pad ลง</string> + <string name="ctrl_target_left">D-pad ซ้าย</string> + <string name="ctrl_target_right">D-pad ขวา</string> + <string name="ctrl_target_hidden">ซ่อน</string> + <string name="ctrl_editor_drag">ลากเพื่อย้าย</string> + <string name="ctrl_editor_snap">จัดให้พอดี</string> + <string name="ctrl_editor_save">บันทึก</string> + <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> + + <string name="rtinst_removing">กำลังลบรันไทม์ Linux</string> + <plurals name="rtinst_removing_entries"> + <item quantity="other">กำลังลบรันไทม์ Linux · %1$d รายการ</item> + </plurals> + <string name="rtinst_removed">ลบรันไทม์ Linux แล้ว</string> + <string name="rtinst_removal_failed">ไม่สามารถลบรันไทม์ Linux ให้เสร็จสมบูรณ์ได้ ลองลบใหม่ในการตั้งค่า</string> + <string name="rtinst_removing_leftovers">กำลังลบเศษไฟล์รันไทม์ที่เหลือ</string> + <string name="rtinst_downloading">กำลังดาวน์โหลด</string> + <string name="rtinst_verifying">กำลังตรวจสอบ</string> + <string name="rtinst_extracting">กำลังแตกไฟล์</string> + <string name="rtinst_channel_description">แสดงขณะติดตั้งหรือลบรันไทม์ Linux</string> + <string name="rtinst_title_installing">กำลังติดตั้งรันไทม์ Linux</string> + <string name="rtinst_title_removing">กำลังลบรันไทม์ Linux</string> + + <string name="deskpkg_runtime_missing">ยังไม่ติดตั้งรันไทม์ Linux</string> + <string name="deskpkg_no_checksum">แคตตาล็อกไม่มีค่าตรวจสอบสำหรับ %1$s</string> + <string name="deskpkg_checksum_mismatch">ค่าตรวจสอบไม่ตรงกัน - ไม่มีการเปลี่ยนแปลงใด ๆ</string> + <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> + + <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> + <string name="flatpakmgr_installing">กำลังติดตั้ง %1$s</string> + <string name="flatpakmgr_updating">กำลังอัปเดต %1$s</string> + <string name="flatpakmgr_removing">กำลังลบ %1$s</string> + <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> + <string name="flatpakmgr_ref_translations">คำแปล</string> + <string name="flatpakmgr_ref_graphics_drivers">ไดรเวอร์กราฟิก</string> + <string name="flatpakmgr_ref_media_codecs">ตัวแปลงรหัสสื่อ</string> + <string name="flatpakmgr_ref_runtime">รันไทม์ %1$s</string> + <string name="flatpakmgr_ref_runtime_branch">รันไทม์ %1$s %2$s</string> + + <string name="appimg_too_small">ไฟล์มีขนาดเล็กเกินกว่าจะเป็น AppImage</string> + <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">นำเข้าได้เฉพาะ AppImage ประเภท 2 เท่านั้น (ไฟล์นี้เป็นประเภท %1$d)</string> + <string name="appimg_no_space_import">พื้นที่ว่างไม่พอ: การนำเข้าต้องใช้ประมาณ %1$s</string> + <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> + <string name="appimg_extracting">กำลังแตกไฟล์ %1$s</string> + <string name="appimg_extract_failed">ไม่สามารถแตกไฟล์ AppImage ได้</string> + <string name="appimg_extract_failed_detail">ไม่สามารถแตกไฟล์ AppImage ได้: %1$s</string> + + <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> + <string name="deckymgr_checksum_download_failed">ไม่สามารถดาวน์โหลดค่าตรวจสอบของรุ่นที่เผยแพร่ได้</string> + <string name="deckymgr_checksum_missing">ไม่มีค่าตรวจสอบของรุ่นที่เผยแพร่</string> + <string name="deckymgr_checksum_invalid">ค่าตรวจสอบของรุ่นที่เผยแพร่ไม่ถูกต้อง</string> + <string name="deckymgr_checksum_mismatch">ค่าตรวจสอบของ Decky Loader ไม่ตรงกัน</string> + <string name="deckymgr_not_elf">ไฟล์ของรุ่นที่เผยแพร่ไม่ใช่โปรแกรม Linux 64 บิตที่ใช้ได้</string> + <string name="deckymgr_session_started_loader">มีเซสชันเริ่มขึ้นระหว่างการดาวน์โหลด หยุดเซสชันก่อนติดตั้ง Decky Loader</string> + <string name="deckymgr_loader_not_executable">ไม่สามารถตั้งค่า PluginLoader ให้รันได้</string> + <string name="deckymgr_cef_failed">ไม่สามารถเปิดการดีบักระยะไกล CEF ของ Steam ได้</string> + <string name="deckymgr_stage_failed">ไม่สามารถเตรียม PluginLoader ได้</string> + <string name="deckymgr_loader_preserve_failed">ไม่สามารถเก็บ PluginLoader เดิมไว้ได้</string> + <string name="deckymgr_loader_activate_failed">ไม่สามารถเปิดใช้ PluginLoader ใหม่ได้</string> + <string name="deckymgr_stop_session_plugin">หยุดเซสชันที่กำลังทำงานก่อนติดตั้งพลักอิน</string> + <string name="deckymgr_loader_required">ติดตั้ง Decky Loader ก่อนนำเข้าพลักอิน</string> + <string name="deckymgr_zip_unreadable">ไม่สามารถอ่านไฟล์ ZIP ที่เลือกได้</string> + <string name="deckymgr_plugins_dir_failed">ไม่สามารถสร้างไดเรกทอรีพลักอินของ Decky ได้</string> + <string name="deckymgr_extracting_zip">กำลังแตกไฟล์ ZIP พลักอิน</string> + <string name="deckymgr_zip_one_folder">ไฟล์ ZIP พลักอินต้องมีโฟลเดอร์พลักอินระดับบนสุดเพียงหนึ่งโฟลเดอร์</string> + <string name="deckymgr_zip_missing_files">ไฟล์ ZIP พลักอินไม่มี plugin.json, package.json หรือ dist/index.js</string> + <string name="deckymgr_plugin_name_missing">ไม่มีชื่อพลักอิน</string> + <string name="deckymgr_session_started_plugin">มีเซสชันเริ่มขึ้นระหว่างการติดตั้ง หยุดเซสชันก่อนติดตั้งพลักอิน</string> + <string name="deckymgr_plugin_installed">ติดตั้งพลักอินแล้ว · รีสตาร์ท Steam เพื่อโหลด</string> + <string name="deckymgr_plugin_install_failed">การติดตั้งไฟล์ ZIP พลักอินล้มเหลว</string> + <string name="deckymgr_zip_too_many">ไฟล์ ZIP พลักอินมีรายการมากเกินไป</string> + <string name="deckymgr_zip_invalid_path">ไฟล์ ZIP พลักอินมีพาธที่ไม่ถูกต้อง</string> + <string name="deckymgr_zip_duplicate_paths">ไฟล์ ZIP พลักอินมีพาธซ้ำกัน</string> + <string name="deckymgr_zip_unsafe_path">ไฟล์ ZIP พลักอินมีพาธที่ไม่ปลอดภัย</string> + <string name="deckymgr_folder_failed">ไม่สามารถสร้างโฟลเดอร์พลักอินได้</string> + <string name="deckymgr_zip_too_large">ไฟล์ ZIP พลักอินขยายเกินขีดจำกัด 1 GiB</string> + <string name="deckymgr_bundled_not_executable">ไม่สามารถตั้งค่าไฟล์ไบนารีพลักอินที่มาด้วยให้รันได้</string> + <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> + <string name="deckymgr_remote_hash_invalid">ไบนารีระยะไกล %1$s มีแฮช SHA-256 ที่ไม่ถูกต้อง</string> + <string name="deckymgr_bin_dir_failed">ไม่สามารถสร้างไดเรกทอรี bin ของพลักอินได้</string> + <string name="deckymgr_downloading_binary">กำลังดาวน์โหลดไบนารีพลักอิน · %1$s</string> + <string name="deckymgr_binary_download_failed">ไม่สามารถดาวน์โหลดไบนารีพลักอิน %1$s ได้</string> + <string name="deckymgr_binary_checksum_mismatch">ค่าตรวจสอบของไบนารีพลักอินไม่ตรงกัน · %1$s</string> + <string name="deckymgr_binary_not_executable">ไม่สามารถตั้งค่าไบนารีพลักอิน %1$s ให้รันได้</string> + <string name="deckymgr_binary_replace_failed">ไม่สามารถแทนที่ไบนารีพลักอิน %1$s ได้</string> + <string name="deckymgr_binary_install_failed">ไม่สามารถติดตั้งไบนารีพลักอิน %1$s ได้</string> + <string name="deckymgr_folder_taken">โฟลเดอร์พลักอินนี้เป็นของพลักอินอื่น</string> + <string name="deckymgr_preserve_failed">ไม่สามารถเก็บพลักอินเดิมไว้ได้</string> + <string name="deckymgr_activate_failed">ไม่สามารถเปิดใช้พลักอินที่นำเข้าได้</string> + <string name="deckymgr_settings_failed">ไม่สามารถอัปเดตการตั้งค่าพลักอินของ Decky ได้</string> + <string name="deckymgr_restore_failed">ไม่สามารถคืนค่าพลักอินเดิมได้</string> + + <string name="adbfix_save_failed">ไม่สามารถบันทึกข้อมูลประจำตัวของ Wireless debugging ได้</string> + <string name="adbfix_command_failed">คำสั่ง ADB ล้มเหลว (%1$d)</string> + <string name="adbfix_not_confirmed">Android ไม่ได้ยืนยันว่าขีดจำกัดโพรเซสลูกถูกเปลี่ยน: %1$s</string> + <string name="adbfix_no_pairing">ไม่มีการจับคู่ Wireless debugging ที่บันทึกไว้</string> + <string name="adbfix_enter_address">กรอกที่อยู่ IP และพอร์ตปัจจุบันของ Wireless debugging</string> + + <string name="adbpair_timed_out">รอป๊อปอัปการจับคู่หมดเวลา ลองใหม่จาก DroidDeck</string> + <string name="adbpair_discovery_failed_code">ไม่สามารถค้นหาป๊อปอัปการจับคู่ได้ (%1$d) ตรวจสอบว่าเปิด Wi-Fi อยู่</string> + <string name="adbpair_discovery_failed">ไม่สามารถค้นหาป๊อปอัปการจับคู่ได้: %1$s</string> + <string name="adbpair_six_digits">กรอกรหัสให้ครบทั้ง 6 หลัก</string> + <string name="adbpair_pairing">กำลังจับคู่…</string> + <string name="adbpair_code_rejected">รหัสนั้นใช้ไม่ได้ เปิดป๊อปอัปการจับคู่อีกครั้งและกรอกรหัสใหม่</string> + <string name="adbpair_connecting">จับคู่แล้ว กำลังเชื่อมต่อ…</string> + <string name="adbpair_port_not_found">จับคู่แล้ว แต่ไม่พบพอร์ตของ Wireless debugging เปิด Wireless debugging ไว้แล้วลองใหม่</string> + <string name="adbpair_applying">กำลังปรับใช้การตั้งค่า…</string> + <string name="adbpair_command_failed">คำสั่ง Wireless debugging ล้มเหลว</string> + <string name="adbpair_channel_name">การตั้งค่า Wireless debugging</string> + <string name="adbpair_channel_description">ขอรหัสจับคู่ Wireless debugging ขณะเปิดการตั้งค่าอยู่</string> + <string name="adbpair_title_pair">จับคู่ Wireless debugging</string> + <string name="adbpair_waiting_text">เปิด Wireless debugging แล้วแตะ “Pair device with pairing code”</string> + <string name="adbpair_waiting_long">เปิด Wireless debugging แล้วแตะ “Pair device with pairing code” อยู่ในการตั้งค่าต่อไป: รหัสจะอยู่ในการแจ้งเตือนนี้</string> + <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> + <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 คุณสามารถปิด Wireless debugging ได้แล้วตอนนี้</string> + <string name="adbpair_title_failed">การตั้งค่า Wireless debugging หยุดแล้ว</string> + + <string name="selfinst_http">การดาวน์โหลดตอบกลับด้วย HTTP %1$d</string> + <string name="selfinst_size_mismatch">ขนาดการดาวน์โหลดไม่ตรงกับ APK ที่เผยแพร่และถูกยกเลิก - ลองใหม่</string> + <string name="selfinst_checksum_mismatch">การดาวน์โหลดไม่ตรงกับค่าตรวจสอบและถูกยกเลิก - ลองใหม่</string> + <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> + <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> + <string name="selfinst_no_signing">APK ที่ดาวน์โหลดไม่มีข้อมูลการลงนาม</string> + <string name="selfinst_wrong_signer">APK ที่ดาวน์โหลดไม่ได้ลงนามด้วยคีย์รุ่นวางจำหน่ายของ DroidDeck</string> + <string name="appupd_http">แคตตาล็อกอัปเดตตอบกลับด้วย HTTP %1$d</string> + + <string name="loadstate_starting">กำลังเริ่มเซสชัน…</string> + <string name="loadstate_elapsed">ผ่านไป %1$d:%2$02d · ยังทำงานอยู่</string> + <string name="loadstate_steam_update">กำลังดาวน์โหลดอัปเดตไคลเอนต์ Steam · %1$d%%</string> + <string name="loadstate_steam_client">กำลังดาวน์โหลดไคลเอนต์ Steam · %1$s</string> + + <string name="pextra_busy">มีการติดตั้งเครื่องมือความเข้ากันได้อื่นกำลังทำงานอยู่แล้ว</string> + <string name="pextra_stop_session">หยุดเซสชันที่กำลังทำงานก่อนติดตั้งเครื่องมือความเข้ากันได้</string> + <string name="pextra_no_release">ไม่พบรุ่นที่เผยแพร่แบบ ARM64 สำหรับ %1$s</string> + <string name="pextra_no_space">ต้องมีพื้นที่ว่างอย่างน้อย 4 GB รวมกับขนาดดาวน์โหลดเพื่อติดตั้ง %1$s</string> + <string name="pextra_downloading">กำลังดาวน์โหลด %1$s %2$s</string> + <string name="pextra_retrying">กำลังลองดาวน์โหลดใหม่ · ครั้งที่ %1$d จาก %2$d</string> + <string name="pextra_interrupted">การดาวน์โหลดถูกขัดจังหวะ</string> + <string name="pextra_download_failed">ดาวน์โหลดล้มเหลว ไฟล์ที่ดาวน์โหลดไม่ครบถูกเก็บไว้เพื่อลองใหม่ต่อได้</string> + <string name="pextra_too_big">การดาวน์โหลดมีขนาดเกินรุ่นที่เผยแพร่และถูกลบ</string> + <string name="pextra_incomplete">การดาวน์โหลดไม่สมบูรณ์ ไฟล์ที่ดาวน์โหลดไม่ครบถูกเก็บไว้เพื่อลองใหม่ต่อได้</string> + <string name="pextra_verifying">กำลังตรวจสอบการดาวน์โหลด</string> + <string name="pextra_checksum_unreadable">ไม่สามารถอ่านค่าตรวจสอบของรุ่นที่เผยแพร่ได้ ลองใหม่</string> + <string name="pextra_no_checksum">%1$s %2$s ไม่ได้เผยแพร่ค่าตรวจสอบ จึงไม่มีการติดตั้งใด ๆ</string> + <string name="pextra_checksum_mismatch">ค่าตรวจสอบไม่ตรงกัน การดาวน์โหลดถูกลบแล้ว</string> + <string name="pextra_session_started">มีเซสชันเริ่มขึ้นระหว่างการดาวน์โหลด หยุดเซสชันก่อนติดตั้งเครื่องมือความเข้ากันได้</string> + <string name="pextra_installing">กำลังติดตั้ง %1$s</string> + <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> + + <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">ถูกเปลี่ยนแปลงนอก Components</string> + <string name="cmgr_not_present">ไม่มีอยู่</string> + <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> + <string name="cmgr_restore_queued">%1$s ของ %2$s จะกลับไปเป็นต้นฉบับเมื่อเกมที่กำลังทำงานบนนั้นปิดลง</string> + <string name="cmgr_restore_done">คืนค่า %1$s ของ %2$s จากต้นฉบับ %3$s แล้ว</string> + <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> + <string name="cmgr_deleted_original">ลบต้นฉบับ %1$s ของ %2$s แล้ว</string> + <string name="cmgr_nightlies_only">การดาวน์โหลดมาจากรุ่นที่เผยแพร่ใน Nightlies เท่านั้น</string> + <string name="cmgr_list_predates_checksums">รายการแพ็กเกจนี้เก่ากว่าระบบค่าตรวจสอบ - รีเฟรชแล้วลองใหม่</string> + <string name="cmgr_checksum_mismatch">ค่าตรวจสอบไม่ตรงกัน</string> + + <string name="sprefs_upscaler_linear">Linear</string> + <string name="sprefs_upscaler_nearest">Nearest</string> + <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (คุณภาพ)</string> + <string name="sprefs_upscaler_sharpen">เพิ่มความคมชัดเท่านั้น</string> + <string name="sprefs_lod_bias_auto">อัตโนมัติ (ตามอัตราขยาย)</string> + + <string name="look_off_desc">ไม่ใช้เอฟเฟกต์ใด ๆ</string> + <string name="look_game_clarity">ความชัดของเกม</string> + <string name="look_game_clarity_desc">เพิ่มความคมชัดพร้อมคอนทราสต์เล็กน้อย - ใช้งานทั่วไป</string> + <string name="look_vivid">สดใส</string> + <string name="look_vivid_desc">เน้นสีจัดจ้าน เปิดลดลายแถบสีเพื่อไม่ให้ท้องฟ้าเป็นลาย</string> + <string name="look_cinematic">ภาพยนตร์</string> + <string name="look_cinematic_desc">คอนทราสต์เข้มขึ้น สีเย็นลง ขอบภาพเรียบขึ้น</string> + <string name="look_competitive">แข่งขัน</string> + <string name="look_competitive_desc">ยกเงาให้สว่างขึ้นและเพิ่มความคมชัดมาก - มองเห็นคนก่อนใคร</string> + <string name="look_adaptive_sharpen">เพิ่มความคมชัดแบบปรับอัตโนมัติ</string> + <string name="look_adaptive_sharpen_desc">เพิ่มความคมชัดเท่านั้น ไม่แตะสี</string> + <string name="look_filmic">ฟิล์ม</string> + <string name="look_filmic_desc">โทนภาพแบบฟิล์ม นวลลงเล็กน้อย</string> + <string name="look_arcade">อาร์เคด</string> + <string name="look_arcade_desc">จัดจ้านและมีพลัง</string> + <string name="look_retro_crt">CRT เรโทร</string> + <string name="look_retro_crt_desc">หลอด CRT และสัญญาณอนาล็อกมาด้วยกัน</string> + <string name="look_upscale_sharp">ขยายภาพแบบคม</string> + <string name="look_upscale_sharp_desc">สำหรับการรันที่ต่ำกว่าความละเอียดจอ</string> + <string name="look_pixel_clean">พิกเซลคมชัด</string> + <string name="look_pixel_clean_desc">พิกเซลคมชัดสำหรับเกม 2D และเกมรุ่นเก่า</string> + <string name="look_anime_edge">ขอบแอนิเมะ</string> + <string name="look_anime_edge_desc">เส้นขอบแบบการ์ตูนพร้อมเพิ่มความคมชัด</string> + + <string name="gpuinfo_not_adreno">ไม่ใช่ GPU Adreno</string> + <string name="gpuinfo_supported">รองรับ</string> + <string name="gpuinfo_experimental">ทดลอง: ไดรเวอร์เป็นบิลด์ทดสอบ</string> + <string name="gpuinfo_below_tested">ต่ำกว่าฮาร์ดแวร์ที่ทดสอบแล้ว (Adreno 725 และรุ่นใหม่กว่า): อาจรันไม่ได้</string> + <string name="gpuinfo_unsupported">ไม่รองรับ: DroidDeck ต้องใช้ GPU Adreno (Snapdragon)</string> + + <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> + <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> + <string name="turnip_reject_library_missing">meta.json ระบุ %1$s ซึ่งไม่มีอยู่ในไฟล์ zip</string> + <string name="turnip_reject_not_aarch64">%1$s ไม่ใช่ไลบรารีที่ใช้ร่วมแบบ ELF AArch64 64 บิต</string> + <string name="turnip_reject_glibc">%1$s เชื่อมโยงกับ glibc - เป็น Turnip แบบ “-Linux” ซึ่งควรอยู่ภายใต้ “ไดรเวอร์รันไทม์ Linux”</string> + <string name="lvd_no_library">ไม่มี libvulkan_freedreno*.so ในไฟล์ zip นี้ ไฟล์ zip แบบ Android (AdrenoTools) หรือ -Wayland Turnip ไม่ใช่ไดรเวอร์รันไทม์ Linux</string> + <string name="lvd_not_aarch64">%1$s ไม่ใช่ไลบรารีที่ใช้ร่วมแบบ ELF AArch64 64 บิต</string> + <string name="lvd_not_glibc">%1$s ไม่ใช่ไดรเวอร์ glibc - ไฟล์นี้เชื่อมโยงกับ libc ของ Android รันไทม์ Linux ต้องใช้ไฟล์ zip แบบ “-Linux” ส่วน Turnip แบบปกติหรือ “-Wayland” ไคลเอนต์ Steam ไม่สามารถโหลดได้เลย</string> + <string name="bundle_no_manifest">ไม่ใช่แพ็กไดรเวอร์: ไม่มี manifest.json ที่ระบุไดรเวอร์ android และ linux</string> + <string name="bundle_entry_outside">ไม่ใช่แพ็กไดรเวอร์: %1$s ชี้ไปนอกแพ็ก</string> + <string name="bundle_too_large">ไม่ใช่แพ็กไดรเวอร์: แตกไฟล์แล้วมีขนาดเกิน %1$d MB</string> + + <string name="gstore_sd_card">การ์ด SD</string> + <string name="gstore_folder">โฟลเดอร์</string> + + <string name="logshare_chooser">แชร์ล็อกเซสชัน</string> + + <string name="hdr_needs_android_10">ต้องใช้ Android 10 หรือใหม่กว่า (เลเยอร์แสดงผลที่มีพื้นที่สี)</string> + <string name="hdr_none">หน้าจอนี้ไม่รองรับ HDR</string> + <string name="hdr_not_hdr10">หน้าจอนี้รองรับ %1$s ไม่ใช่ HDR10</string> + + <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> + <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">เปิด “Disable child process restrictions” ในตัวเลือกสำหรับนักพัฒนา และเปิดตัวเลือกสำหรับนักพัฒนาไว้ต่อไปหลังจากนั้น</string> + <string name="phantom_fix_no_toggle">Android รุ่นนี้ไม่มีสวิตช์สำหรับสิ่งนี้ ใช้ Wireless debugging เพื่อเปลี่ยนค่าจากอุปกรณ์นี้</string> + <string name="upd_block_no_apk">บิลด์นี้ไม่มีให้ดาวน์โหลดสำหรับสำเนานี้ของ DroidDeck</string> + <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> + <string name="upd_fail_with_message">อัปเดตไม่สามารถติดตั้งได้: %1$s</string> + <string name="upd_fail_generic">อัปเดตไม่สามารถติดตั้งได้</string> + <string name="saves_layout_zip">zip ของ %1$s</string> + <string name="saves_zip">zip</string> + <string name="saves_not_launched">%1$s ยังไม่ถูกเปิด</string> + <string name="saves_none_found">ไม่พบไฟล์เซฟสำหรับ %1$s</string> + <string name="saves_not_save_zip">%1$s ไม่มีโฟลเดอร์ drive_c จึงไม่ใช่ไฟล์ zip เซฟของ GameHub หรือ Winlator</string> + <string name="gstore_cannot_create">ไม่สามารถสร้างโฟลเดอร์ใน %1$s ได้</string> + <string name="gstore_cannot_write">ไม่สามารถเขียนใน %1$s ได้</string> + <string name="gstore_cannot_write_detail">ไม่สามารถเขียนใน %1$s ได้ (%2$s)</string> + <string name="gfiles_folder_unavailable">โฟลเดอร์ไม่พร้อมใช้งาน</string> + <string name="gfiles_name_taken">มีไฟล์อื่นใช้ชื่อ %1$s อยู่แล้ว</string> + <string name="gfiles_art_unavailable">โฟลเดอร์ภาพปกไม่พร้อมใช้งาน</string> + <string name="gfiles_unrelated_inventory">โฟลเดอร์มีรายการซิงค์ที่ไม่เกี่ยวข้อง</string> + <string name="gfiles_remove_failed">ไม่สามารถลบ %1$s ได้</string> + <string name="gfiles_outside_folder">ไฟล์ออกไปนอกโฟลเดอร์ส่งออก</string> + <string name="cmgr_fex_unnamed">FEX (บิลด์ที่ไม่มีชื่อรุ่น)</string> + <string name="cmgr_no_profile">ไม่มี profile.json ใน %1$s</string> + <string name="cmgr_not_component_package">%1$s ไม่ใช่แพ็กเกจคอมโพเนนต์ Linux (ประเภท %2$s)</string> + <string name="gstore_volume_free">%1$s · เหลือ %2$s</string> + <string name="mode_storage_auto_short">อัตโนมัติ</string> + <string name="appupd_catalog_rejected">แคตตาล็อกอัปเดตไม่ผ่านการตรวจสอบ (%1$s)</string> + <string name="main_adb_clip_label">การตั้งค่าโพรเซสลูกของ DroidDeck</string> + <string name="setup_signed_in_offline_unnamed">ลงชื่อเข้าใช้แล้ว · โหมดออฟไลน์</string> + <string name="setup_signed_in_unnamed">ลงชื่อเข้าใช้แล้ว</string> + <string name="driver_tag_bundled">มีในตัว</string> + <string name="driver_tag_downloaded">ดาวน์โหลดแล้ว</string> + <string name="driver_tag_imported">นำเข้าแล้ว</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s %2$s\n\n%3$s</string> + <string name="display_session">การแสดงผลของเซสชัน</string> + <string name="display_resolution">ความละเอียด</string> + <string name="display_game_fit">การแสดงเกม 16:9 ให้พอดี</string> + <string name="display_game_fit_hint">วิธีที่เกม 16:9 แสดงพอดีกับหน้าจอนี้ มีผลในเซสชันถัดไป</string> + <string name="display_game_fit_unavailable">เลือกพอดีหน้าจอ หรือขนาดกำหนดเองที่แคบกว่า 16:9</string> + <string name="display_preserve">คงสัดส่วนไว้</string> + <string name="display_stretch">ยืดให้พอดีหน้าจอ</string> + <string name="display_image_scaling">การปรับขนาดภาพ</string> + <string name="display_filter">ตัวกรองการปรับขนาด</string> + <string name="display_filter_hint">ใช้ร่วมกันระหว่าง Steam และเดสก์ท็อป</string> + <string name="display_filter_live">มีผลทันที ใช้ร่วมกันระหว่าง Steam และเดสก์ท็อป</string> + <string name="display_filter_note">FSR, GSR และ NIS จะมีผลเมื่อขยายภาพของเซสชัน เพิ่มความคมชัดเท่านั้นทำงานได้โดยไม่ต้องขยายภาพด้วย</string> + <string name="display_sharpness">ความคมชัดของตัวกรอง</string> + <string name="display_frame_rate">อัตราเฟรม</string> + <string name="display_look_filter">%1$s ตั้งการปรับขนาดเป็น %2$s</string> + <string name="display_match_screen">พอดีหน้าจอ (%1$d×%2$d)</string> + <string name="display_follow_screen">ตามหน้าจอ (ปรับขนาดเมื่อพับหรือกาง)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">แสดงรายละเอียดอุปกรณ์</string> + <string name="device_info_hide">ซ่อนรายละเอียดอุปกรณ์</string> + <string name="device_info_loading">กำลังอ่านข้อมูลอุปกรณ์…</string> + <string name="device_info_copy">คัดลอก</string> + <string name="device_info_copy_hint">คัดลอกรายละเอียดเหล่านี้ลงในรายงานข้อบกพร่อง</string> + <string name="device_info_copied">คัดลอกแล้ว</string> + <string name="force_ssbs_title">บังคับ SSBS</string> + <string name="force_ssbs_hint">คงการตั้งค่าความเร็ว CPU ที่ Wine มักปิดอยู่เสมอ อาจทำให้เกม Windows เร็วขึ้นบน Snapdragon 8 Elite แต่เห็นความเปลี่ยนแปลงน้อยบนชิปรุ่นเก่า มีผลในการเปิดเกมครั้งถัดไป</string> + + <string name="rail_stores">สโตร์</string> + <string name="source_steam">Steam</string> + <string name="source_added">กำหนดเอง</string> + <string name="setup_stores">สโตร์</string> + <string name="setup_stores_open_on">เปิดสโตร์บน</string> + <string name="setup_stores_speed">ระดับความเร็วการดาวน์โหลด</string> + <string name="setup_stores_engine">เอนจินสโตร์</string> + <string name="setup_stores_engine_chip_ready">● เอนจินพร้อมใช้งาน</string> + <string name="setup_stores_engine_chip_missing">ยังไม่สร้าง</string> + <string name="stores_tier_balanced">สมดุล</string> + <string name="stores_tier_fast">เร็ว</string> + <string name="stores_tier_max">สูงสุด</string> + <string name="stores_title">สโตร์</string> + <string name="stores_settings">การตั้งค่าสโตร์</string> + <string name="stores_settings_eyebrow">การตั้งค่า</string> + <string name="stores_chip_signed_out">(ยังไม่ลงชื่อเข้าใช้)</string> + <string name="stores_tab_store">สโตร์</string> + <string name="stores_tab_installed">ติดตั้งแล้ว</string> + <string name="stores_tab_library">ไลบรารี</string> + <string name="stores_tab_all">ทั้งหมด</string> + <string name="stores_tab_downloads">การดาวน์โหลด</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">ลงชื่อเข้าใช้</string> + <string name="stores_signout_named">%1$s · ลงชื่อออก</string> + <string name="stores_signout_title">ลงชื่อออกจาก %1$s?</string> + <string name="stores_signout_note">เกมที่ติดตั้งไว้จะยังคงอยู่ ไลบรารีจะถูกดึงมาใหม่เมื่อคุณลงชื่อเข้าใช้อีกครั้ง</string> + <string name="stores_signout_stay">ลงชื่อเข้าใช้ต่อไป</string> + <string name="stores_signout_go">ลงชื่อออก</string> + <string name="stores_search_catalog">ค้นหาแคตตาล็อกของ %1$s…</string> + <string name="stores_search_installed">ค้นหาเกมที่ติดตั้งไว้…</string> + <string name="stores_search_library">ค้นหาไลบรารีของคุณ…</string> + <string name="stores_search_open">ค้นหา</string> + <string name="stores_installed_empty">ไม่มีสิ่งที่ติดตั้งไว้</string> + <string name="stores_library_loading">กำลังโหลด</string> + <string name="stores_nothing_matches">ไม่มีเกม</string> + <string name="stores_shelves_loading">กำลังโหลดสโตร์…</string> + <string name="stores_amazon_no_catalog">ไม่มีแคตตาล็อก</string> + <string name="stores_shelf_new">มีอะไรใหม่</string> + <string name="stores_shelf_deals">ดีล</string> + <string name="stores_shelf_free">เกมฟรี</string> + <string name="stores_shelf_trending">กำลังเป็นที่นิยม</string> + <string name="stores_shelf_library">ไลบรารีของคุณ</string> + <string name="stores_play">เล่น</string> + <string name="stores_install">ติดตั้ง</string> + <string name="stores_resume_install">ดำเนินการติดตั้งต่อ</string> + <string name="stores_dl_interrupted">ถูกขัดจังหวะ</string> + <string name="stores_install_size">ติดตั้ง · %1$s</string> + <string name="stores_get_free">รับไปเลยฟรี</string> + <string name="stores_view_on">ดูบน %1$s</string> + <string name="stores_buy_price">ซื้อ · %1$s</string> + <string name="stores_installed_chip">ติดตั้งแล้ว</string> + <string name="stores_in_library">อยู่ในไลบรารี</string> + <string name="stores_free">ฟรี</string> + <string name="stores_resume">ดำเนินการต่อ</string> + <string name="stores_refresh">รีเฟรช</string> + <string name="stores_show_mature">แสดงเนื้อหาสำหรับผู้ใหญ่</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">ออฟไลน์</string> + <string name="epic_card_none">ปิด</string> + <string name="epic_eos">ลงชื่อเข้าใช้ Epic (EOS)</string> + <string name="epic_offline">เปิดแบบออฟไลน์</string> + <string name="epic_resolve">แก้ไขการลงชื่อเข้าใช้ Epic</string> + <string name="store_cloud_saves">เซฟบนคลาวด์</string> + <string name="cloud_upload">อัปโหลดขึ้นคลาวด์</string> + <string name="cloud_download">ดาวน์โหลดจากคลาวด์</string> + <string name="cloud_none">ไม่มีเซฟบนคลาวด์</string> + <string name="cloud_conflict">ข้อขัดแย้งบนคลาวด์</string> + <string name="cloud_conflict_files">ข้อขัดแย้งบนคลาวด์ · %1$d ไฟล์</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">เก็บของคลาวด์</string> + <string name="cloud_keep_local">เก็บของเครื่อง</string> + <string name="cloud_checking">กำลังตรวจสอบคลาวด์…</string> + <string name="cloud_last_sync">ซิงค์ล่าสุด %1$s</string> + <string name="cloud_never">ยังไม่ได้ซิงค์</string> + <string name="cloud_done">%1$d ไฟล์</string> + <string name="stores_eyebrow_installed">ติดตั้งแล้ว</string> + <string name="stores_eyebrow_library">อยู่ในไลบรารีของคุณ</string> + <string name="stores_eyebrow_store">สโตร์</string> + <string name="achievements_title">ความสำเร็จ</string> + <string name="achievements_count">ปลดล็อกแล้ว %1$d จาก %2$d</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">ปลดล็อกแล้ว · %1$d</string> + <string name="achievements_group_locked">ล็อกอยู่ · %1$d</string> + <string name="achievements_group_hidden">ซ่อนอยู่ · %1$d</string> + <string name="achievements_hidden">ความสำเร็จที่ซ่อนอยู่</string> + <string name="achievements_hidden_detail">รายละเอียดจะแสดงเมื่อปลดล็อกแล้ว</string> + <string name="achievements_all">ปลดล็อกความสำเร็จครบทุกรายการ</string> + <string name="achievements_reading">กำลังโหลด…</string> + <string name="achievements_signed_out">ลงชื่อเข้าใช้ %1$s เพื่อดูความสำเร็จของคุณ</string> + <string name="achievements_unreachable">ไม่สามารถเชื่อมต่อ %1$s ได้ ลองตรวจสอบอีกครั้งเมื่อออนไลน์</string> + <string name="achievements_none">%1$s ไม่มีรายการความสำเร็จสำหรับเกมนี้</string> + <string name="achievements_show_all">แสดงทั้งหมด %1$d รายการ</string> + <string name="achievements_dialog_title">ความสำเร็จ · %1$s</string> + <string name="stores_in_steam">✓ อยู่ใน Steam</string> + <string name="stores_uninstall">ถอนการติดตั้ง</string> + <string name="stores_uninstalling">กำลังถอนการติดตั้ง…</string> + <string name="stores_uninstall_title">ถอนการติดตั้ง %1$s?</string> + <string name="stores_uninstall_body_size">คืนพื้นที่ %1$s และลบออกจาก Steam</string> + <string name="stores_uninstall_body">ลบไฟล์และช็อตคัตใน Steam ของเกมนี้</string> + <string name="stores_launch_not_registered">Steam จะรับเกมนี้เข้ามาเมื่อเริ่มครั้งถัดไป เปิดจากที่นั่น หรือเริ่ม Steam ตอนนี้</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">การเลือกของ Steam ต่อเกม</string> + <string name="stores_card_audio">เสียง</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Components</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">เอนจิน</string> + <string name="stores_card_engine_value">%1$s · เนทีฟ</string> + <string name="stores_card_engine_java">%1$s · มีในตัว</string> + <string name="stores_how_it_runs">วิธีที่ทำงาน</string> + <string name="stores_kv_fetched">ดึงมาจาก</string> + <string name="stores_kv_fetched_value">%1$s ด้วยบัญชีของคุณ</string> + <string name="stores_kv_installed_to">ติดตั้งไปที่</string> + <string name="stores_kv_installed_to_value">พื้นที่จัดเก็บเกม (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">เปิดโดย</string> + <string name="stores_kv_launched_by_value">Steam และ Proton เส้นทางเดียวกับเกม Steam</string> + <string name="stores_kv_launched_by_epic">Steam และ Proton · ลงชื่อเข้าใช้ Epic ทุกครั้งที่เปิด ปิดโอเวอร์เลย์ EOS</string> + <string name="stores_kv_launched_by_amazon">Steam และ Proton · ตั้งค่าตัวแปรของ Amazon Games Services แล้ว</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">เพิ่มเข้า Steam เมื่อติดตั้ง (Deck UI + Steam Input)</string> + <string name="stores_downloads_empty">ไม่มีการดาวน์โหลด</string> + <string name="stores_dl_paused">หยุดชั่วคราว</string> + <string name="stores_dl_queued">อยู่ในคิว</string> + <string name="stores_dl_queued_at">อยู่ในคิว · #%1$d</string> + <string name="stores_dl_cancelled">ยกเลิกแล้ว</string> + <string name="stores_dl_failed">ล้มเหลว</string> + <string name="stores_dl_failed_reason">ล้มเหลว · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">หยุดชั่วคราว</string> + <string name="stores_dl_resume">ดำเนินการต่อ</string> + <string name="stores_dl_cancel">ยกเลิก</string> + <string name="stores_dl_retry">ลองใหม่</string> + <string name="stores_dl_clear">ล้าง</string> + <string name="stores_dl_clear_all">ล้างรายการที่เสร็จแล้ว</string> + <string name="stores_stage_manifest">Manifest</string> + <string name="stores_stage_download">ดาวน์โหลด</string> + <string name="stores_stage_verify">ตรวจสอบ</string> + <string name="stores_stage_install">ติดตั้ง</string> + <string name="stores_stage_done">เสร็จสิ้น</string> + <string name="stores_stage_checking">กำลังตรวจสอบ</string> + <string name="stores_stage_downloading">กำลังดาวน์โหลด</string> + <string name="stores_dl_cancel_confirm">ลบการดาวน์โหลด?</string> + <string name="stores_dl_failed_kept">ล้มเหลว · เก็บ %1$s ไว้</string> + <string name="stores_stage_verifying">กำลังตรวจสอบ</string> + <string name="stores_stage_installing">กำลังติดตั้ง</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d ไฟล์</string> + <string name="stores_download_manager">ตัวจัดการดาวน์โหลด</string> + <string name="stores_parallel">การดาวน์โหลดพร้อมกัน</string> + <string name="stores_install_where">ติดตั้งไปที่</string> + <string name="stores_install_title">ติดตั้ง %1$s:</string> + <string name="stores_target_internal">ภายใน</string> + <string name="stores_target_free">เหลือ %1$s</string> + <string name="stores_notification_channel">การดาวน์โหลดสโตร์</string> + <string name="stores_notification_channel_hint">แสดงความคืบหน้าการดาวน์โหลดเกมและให้การดาวน์โหลดทำงานต่อในพื้นหลัง</string> + <string name="stores_notification_one">กำลังดาวน์โหลด</string> + <string name="stores_notification_many">กำลังดาวน์โหลด %1$d รายการ</string> + <string name="stores_notification_waiting">กำลังเตรียม…</string> + <string name="stores_notification_done_channel">การดาวน์โหลดสโตร์เสร็จสิ้น</string> + <string name="stores_notification_installed">ติดตั้ง %1$s แล้ว</string> + <string name="stores_notification_failed">ดาวน์โหลด %1$s ล้มเหลว</string> + <string name="stores_notification_delete">ลบ</string> + <string name="stores_notification_keep">เก็บไว้</string> + <string name="stores_login_title">ลงชื่อเข้าใช้ %1$s</string> + <string name="stores_login_provider_title">ลงชื่อเข้าใช้ด้วยผู้ให้บริการของคุณ</string> + <string name="stores_login_reload">โหลดใหม่</string> + <string name="stores_login_close">ปิด</string> + <string name="stores_login_retry">ลองใหม่</string> + <string name="stores_login_finishing">กำลังเสร็จสิ้นการลงชื่อเข้าใช้…</string> + <string name="stores_login_error_network">ไม่สามารถโหลดหน้าลงชื่อเข้าใช้ได้: %1$s</string> + <string name="stores_login_error_idp_blocked">ผู้ให้บริการนี้ปฏิเสธการลงชื่อเข้าใช้จากหน้าที่ฝังอยู่ ให้ลงชื่อเข้าใช้ด้วยรหัสผ่านของสโตร์แทน</string> + <string name="stores_login_error_http">หน้าลงชื่อเข้าใช้ตอบกลับด้วยข้อผิดพลาด (HTTP %1$d)</string> + <string name="stores_login_error_ssl">การเชื่อมต่อไปยังหน้าลงชื่อเข้าใช้ไม่ปลอดภัย</string> + <string name="stores_login_error_verification">ไม่สามารถยืนยันการลงชื่อเข้าใช้ได้ ลองใหม่</string> + <string name="stores_login_error_generic">ลงชื่อเข้าใช้ล้มเหลว ลองใหม่</string> + <string name="common_close">ปิด</string> + <string name="common_back">กลับ</string> + <string name="common_edit">แก้ไข</string> + <string name="games_add_all">เพิ่มเกมทั้งหมดในโฟลเดอร์นี้</string> + <string name="games_added_none">ไม่มีอะไรใหม่ในโฟลเดอร์นี้</string> + <string name="edit_name">ชื่อ</string> + <string name="edit_target">เป้าหมาย</string> + <string name="edit_start_in">เริ่มที่</string> + <string name="edit_launch_options">ตัวเลือกการเปิด</string> + <string name="edit_artwork">ภาพปก</string> + <string name="edit_other_exe">exe อื่น…</string> + <string name="edit_remove">ลบ</string> + <string name="art_cover">ภาพปก</string> + <string name="art_background">พื้นหลัง</string> + <string name="art_logo">โลโก้</string> + <string name="art_icon">ไอคอน</string> + <string name="art_src_folder">โฟลเดอร์เกม</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">ไฟล์ของคุณ</string> + <string name="art_src_exe">จากไฟล์ exe</string> + <string name="art_src_auto">อัตโนมัติ</string> + <string name="art_pick_image">เลือกภาพ…</string> + <string name="art_pick_title">เลือกภาพ: %1$s</string> + <string name="art_reset">กลับไปใช้อัตโนมัติ</string> + <plurals name="games_added_n"> + <item quantity="other">เพิ่มเกมแล้ว %1$d เกม</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="other">มีอยู่ใน Games แล้ว %1$d เกม</item> + </plurals> + <string name="hop_back">B · กลับไปที่ %1$s</string> + <string name="hop_from_games">เกม</string> + <string name="hop_from_play">การตั้งค่า Play</string> + <string name="sheet_graphics_drivers">ไดรเวอร์กราฟิก</string> + <string name="comp_this_proton">Proton นี้</string> + <string name="comp_every_proton">Proton ทุกตัว</string> + <string name="comp_for_every_proton">สำหรับ Proton ทุกตัว</string> + <string name="comp_available_count">พร้อมใช้งาน · %1$d</string> + <string name="comp_checked">ตรวจสอบล่าสุด %1$s</string> + <string name="comp_check_all">ตรวจสอบคอมโพเนนต์และไดรเวอร์ใหม่</string> + <string name="comp_get">รับ</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton และ FEX ด้านล่างเป็นของ Proton นี้</string> + <string name="comp_device_tested">%1$s · ทดสอบแล้ว</string> + <string name="comp_device_untested">%1$s · ยังไม่ทดสอบ</string> + <string name="comp_device_unsupported">%1$s · ไม่รองรับ</string> + <string name="gpu_auto_keeps">คงคู่ที่แนะนำให้เป็นรุ่นล่าสุดเสมอ</string> + <string name="gpu_bar_auto">อัตโนมัติ · %1$s</string> + <string name="gpu_separately">ไดรเวอร์รันไทม์และแสดงผลแยกกัน</string> + <string name="gpu_use_title">ใช้ %1$s?</string> + <string name="proton_remove_title">ลบ %1$s?</string> + <string name="setup_tab_diagnostics">การวินิจฉัย</string> + <string name="setup_ready">พร้อมเล่นแล้ว</string> + <string name="setup_not_ready">ยังไม่พร้อมเล่น</string> + <string name="setup_ready_runtime">รันไทม์ %1$s</string> + <string name="setup_ready_limit_off">ปิดขีดจำกัดโพรเซส</string> + <string name="setup_ready_signed_in">ลงชื่อเข้าใช้เป็น %1$s</string> + <string name="setup_needs_you">เมื่อต้องการให้คุณดำเนินการ</string> + <string name="setup_group_look">รูปลักษณ์</string> + <string name="setup_group_device">อุปกรณ์นี้</string> + <string name="setup_group_art">ภาพปกไลบรารี</string> + <string name="flathub_eyebrow">แอป LINUX</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">แอปเดสก์ท็อปสำหรับเดสก์ท็อป Linux เปิดจากเดสก์ท็อป</string> + <string name="flathub_setup">ตั้งค่า Flathub</string> + <string name="flathub_remove_title">ลบ %1$s?</string> + <string name="flathub_remove_note">การตั้งค่าและไฟล์ของมันจะยังคงอยู่</string> + <string name="flathub_keep">เก็บไว้</string> + <string name="desktop_size">ขนาดบนดิสก์</string> + <string name="desktop_uninstall_title">ถอนการติดตั้งเดสก์ท็อป?</string> + <string name="desktop_all_systems">ทั้งหมด</string> + <string name="gog_achievements">ความสำเร็จของ GOG</string> + <string name="gog_achievements_hint">ซิงค์ความสำเร็จสำหรับเกม GOG ที่รองรับ มีผลในการเปิดครั้งถัดไป</string> + <string name="gog_achievements_off">ปิดความสำเร็จ</string> + <string name="gog_achievements_on">เปิดใช้ความสำเร็จ</string> + <string name="gog_achievements_signin">ลงชื่อเข้าใช้ GOG อีกครั้ง</string> + <string name="gog_achievements_uploaded">อัปโหลดความสำเร็จแล้ว</string> + <string name="gog_achievements_pending">กำลังรออัปโหลดความสำเร็จ</string> + <string name="gog_achievements_enable_store">เปิดใช้ความสำเร็จของ GOG ในการตั้งค่า Stores</string> + <string name="gog_achievements_unavailable">ไม่พบความสำเร็จ ตรวจสอบบันทึกเซสชัน</string> + <string name="achievement_unlocked">ปลดล็อกความสำเร็จแล้ว</string> + <string name="touch_paste">วาง</string> + <string name="touch_dpad">ปุ่มกากบาท</string> + <string name="touch_left_stick">อนาล็อกซ้าย</string> + <string name="touch_right_stick">อนาล็อกขวา</string> + <string name="touch_menu">เมนู</string> + <string name="touch_keyboard">แป้นพิมพ์</string> + <string name="touch_dpad_up">ปุ่มกากบาทขึ้น</string> + <string name="touch_dpad_down">ปุ่มกากบาทลง</string> + <string name="touch_dpad_left">ปุ่มกากบาทซ้าย</string> + <string name="touch_dpad_right">ปุ่มกากบาทขวา</string> + <string name="touch_title">การควบคุมแบบสัมผัส</string> + <string name="touch_controller">คอนโทรลเลอร์สัมผัส</string> + <string name="touch_mouse">เมาส์</string> + <string name="touch_on">เปิด</string> + <string name="touch_off">ปิด</string> + <string name="touch_speed_slow">ช้า</string> + <string name="touch_speed_normal">ปกติ</string> + <string name="touch_speed_fast">เร็ว</string> + <string name="touch_edit_layout">แก้ไขเค้าโครง</string> + <string name="touch_droiddeck_menu">เมนู DroidDeck</string> + <string name="touch_drag_hint">ลากปุ่มควบคุมไปวางบนหน้าจอ</string> + <string name="touch_arrange">จัดเรียง</string> + <string name="touch_discard">ละทิ้ง</string> + <string name="touch_saving">กำลังบันทึก…</string> + <string name="touch_icon">ไอคอน</string> + <string name="touch_narrower">ก−</string> + <string name="touch_wider">ก+</string> + <string name="touch_shorter">ส−</string> + <string name="touch_taller">ส+</string> + <string name="touch_hide">ซ่อน</string> + <string name="touch_color_opacity">สีและความโปร่งใส…</string> + <string name="touch_copy_layout">คัดลอกเค้าโครงนี้</string> + <string name="touch_layout_copied">คัดลอกเค้าโครงแล้ว นำไปวางในเค้าโครงสัมผัสของเกมใดก็ได้</string> + <string name="touch_paste_layout">วางเค้าโครงที่คัดลอก</string> + <string name="touch_reset_layout">รีเซ็ตเป็นค่าเริ่มต้นของ Steam</string> + <string name="touch_no_binding">ปุ่มควบคุมนี้ไม่มีการผูกคำสั่งใน Steam ที่จะใส่ไอคอนให้ได้</string> + <string name="touch_icon_for">ไอคอนสำหรับ %1$s</string> + <string name="touch_layout_color">สีของเค้าโครงสัมผัส</string> + <string name="touch_done">เสร็จสิ้น</string> + <string name="touch_save_failed">ไม่สามารถบันทึกเค้าโครงสัมผัสได้</string> + <string name="touch_saved">บันทึกเค้าโครงสัมผัสลง Steam แล้ว</string> + <string name="touch_app_id">แอป %1$d</string> + <string name="touch_label_hint">ป้ายชื่อ (ไม่บังคับ)</string> + <string name="touch_icons_missing">ไม่พบไอคอนของ Steam</string> + <plurals name="touch_icons_found"> + <item quantity="other">ไอคอน %1$d รายการจาก Steam</item> + </plurals> + <string name="touch_icon_color">สีไอคอน</string> + <string name="touch_button_color">สีปุ่ม</string> + <string name="touch_no_icon">ไม่มีไอคอน</string> + + <string name="stores_sync_sign_in">กำลังตรวจสอบการลงชื่อเข้าใช้…</string> + <string name="stores_sync_library">กำลังดึงข้อมูลคลังเกมของคุณ…</string> + <string name="stores_sync_game_list">กำลังดึงรายการเกม…</string> + <string name="stores_sync_details">กำลังโหลดรายละเอียดเกม… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="other">กำลังซิงค์เกม %1$d เกม…</item> + </plurals> + <string name="stores_sync_progress">กำลังซิงค์… %1$d/%2$d</string> + <string name="stores_sync_cover_art">กำลังดึงภาพปก…</string> + <string name="stores_sync_failed">การซิงค์ล้มเหลว</string> + <string name="stores_sync_no_games">ไม่พบเกมในคลังเกม %1$s ของคุณ</string> + <string name="stores_sync_amazon_no_answer">Amazon Games ไม่ตอบสนองสำหรับคลังเกมของคุณ: รีเฟรช หรือลงชื่อเข้าใช้อีกครั้ง</string> + <string name="stores_sync_gog_unreachable">เชื่อมต่อกับ GOG ไม่ได้</string> + <string name="stores_sync_gog_unreadable">ไม่สามารถอ่านคลังเกม GOG ของคุณได้</string> + <string name="stores_registering">กำลังลงทะเบียนกับ Steam…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">โหมด Cardboard</string> + <string name="cardboard_mode_hint">ภาพสำหรับตาซ้ายและตาขวาสำหรับแว่น VR ของโทรศัพท์</string> + <string name="cardboard_source">ภาพ</string> + <string name="cardboard_source_screen">หน้าจอเสมือน</string> + <string name="cardboard_source_sbs">3D แบบเคียงข้าง</string> + <string name="cardboard_viewer">แว่น</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">ไม่มีเลนส์</string> + <string name="cardboard_ipd">ระยะห่างระหว่างตา</string> + <string name="cardboard_screen_width">ความกว้างหน้าจอ</string> + <string name="cardboard_screen_distance">ระยะห่างหน้าจอ</string> + <string name="cardboard_head_tracking">การติดตามศีรษะ</string> + <string name="cardboard_no_sensor">อุปกรณ์นี้ไม่มีเซ็นเซอร์การหมุน</string> + <string name="cardboard_recenter">จัดมุมมองให้อยู่ตรงกลาง</string> + <string name="cardboard_recenter_button">จัดกึ่งกลาง</string> + <string name="cardboard_steamvr">SteamVR (ทดลอง)</string> + <string name="cardboard_steamvr_install">ติดตั้ง</string> + <string name="cardboard_steamvr_install_hint">ดาวน์โหลด SteamVR สำหรับ ARM64 ของ Valve (571 MB)</string> + <string name="cardboard_steamvr_installing">กำลังดาวน์โหลดและแตกไฟล์ SteamVR…</string> + <string name="cardboard_steamvr_start">เริ่ม</string> + <string name="cardboard_steamvr_ready_hint">เรียกใช้ SteamVR โดยใช้ศีรษะของคุณเป็นชุดหูฟัง ภาพจะเปลี่ยนเป็น 3D แบบเคียงข้าง</string> + <string name="cardboard_steamvr_stop">หยุด</string> + <string name="cardboard_steamvr_running_hint">SteamVR กำลังทำงาน</string> + <string name="cardboard_steamvr_unavailable">ใช้ไม่ได้ในเซสชันนี้</string> + <string name="cardboard_steamvr_failed">SteamVR ไม่ตอบสนอง ตรวจสอบบันทึกเซสชัน</string> + <string name="drawer_component_downloads">ดาวน์โหลดคอมโพเนนต์</string> + <string name="drawer_component_downloads_hint">ไฟล์ที่ดาวน์โหลดใช้ร่วมกับหน้า Components เลือกเวอร์ชันที่ดาวน์โหลดแล้วด้านบนเพื่อใช้ในการเปิดเกมครั้งถัดไป</string> + <string name="drawer_component_choose_download">เลือกเวอร์ชันที่จะดาวน์โหลด</string> + <string name="stores_fact_developer">ผู้พัฒนา</string> + <string name="stores_fact_publisher">ผู้จัดจำหน่าย</string> + <string name="stores_fact_released">วางจำหน่าย</string> + <string name="stores_hltb_main">เนื้อเรื่องหลัก</string> + <string name="stores_hltb_extras">+ ส่วนเสริม</string> + <string name="stores_hltb_completionist">เก็บครบ</string> + <string name="stores_hltb_hours">%1$s ชม.</string> + <string name="stores_hltb_minutes">%1$d นาที</string> +</resources> diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml new file mode 100644 index 000000000..75ec7fa18 --- /dev/null +++ b/app/src/main/res/values-tr/strings.xml @@ -0,0 +1,2235 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>İlk çalıştırmada Steam indirilir. Bu işlem birkaç dakika sürebilir.</item> + <item>Varsayılan olarak uygulamadan çıkınca Steam duraklatılır; geri döndüğünüzde kaldığı yerden devam eder.</item> + <item>Kumandanız Xbox 360 kumandası olarak görünür. Kumanda yoksa ekran üstü kontrolleri kullanın.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>İlk çalıştırmada masaüstü kurulur. Bu işlem birkaç dakika sürebilir.</item> + <item>Varsayılan olarak uygulamadan çıkınca oturum duraklatılır; geri döndüğünüzde kaldığı yerden devam eder.</item> + <item>Dokunma, dokunmatik yüzey gibi çalışır: işaretçiyi taşımak için sürükleyin, tıklamak için dokunun.</item> + <item>Kumandanız Xbox 360 kumandası olarak görünür. Kumanda yoksa ekran üstü kontrolleri kullanın.</item> + </string-array> + <string name="session_channel">Oturum</string> + <string name="session_channel_description">Steam veya masaüstü oturumu açıkken gösterilir</string> + <string name="session_notification">Steam çalışıyor</string> + <string name="session_notification_downloads">Steam indirmesi sürüyor</string> + <string name="session_notification_paused">Oturum duraklatıldı - hazır olduğunuzda Devam et\'e dokunun</string> + <string name="resume_session">Devam et</string> + <string name="suspend_now">Şimdi askıya al</string> + <string name="stop_session">Oturumu durdur</string> + <string name="frame_gen_title">Kare oluşturma</string> + <string name="frame_gen_hint">Kare oluşturma modunu seçin</string> + <string name="frame_gen_off">Kapalı</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG Uyarlanabilir %1$d</string> + <string name="lsfg_needs_steam">Steam\'de Lossless Scaling gerekli</string> + <string name="lsfg_needs_install">Lossless Scaling\'i Steam\'den yükleyin veya Lossless.dll dosyasını içe aktarın</string> + <string name="lsfg_import">Lossless.dll içe aktar…</string> + <string name="lsfg_using_steam">Steam\'deki kopya kullanılıyor</string> + <string name="lsfg_using_import">İçe aktarılan kopya kullanılıyor</string> + <string name="lsfg_pick_title">Lossless.dll seçin</string> + <string name="lsfg_imported">Lossless.dll içe aktarıldı</string> + <string name="lsfg_import_unchanged">Bu Lossless.dll zaten kullanılıyor</string> + <string name="lsfg_import_not_owned">Bu Steam hesabı Lossless Scaling\'e sahip değil</string> + <string name="lsfg_import_invalid">Bu dosya Lossless Scaling\'in Lossless.dll dosyası değil</string> + <string name="lsfg_import_failed">Bu Lossless.dll ile LSFG hazırlanamadı</string> + <string name="lsfg_problem_missing">LSFG için Steam\'den Lossless Scaling veya içe aktarılmış bir Lossless.dll gerekir</string> + <string name="lsfg_problem_failed">LSFG, gölgelendiricilerini Lossless.dll dosyasından hazırlayamadı</string> + <string name="hud_fg_unsupported">%1$s: desteklenmiyor</string> + <string name="hud_fg_failed">%1$s: motor hatası</string> + <string name="hud_fg_needs_lossless">%1$s: Lossless Scaling gerekli</string> + <string name="hud_fg_starting">%1$s %2$s · başlatılıyor</string> + <string name="second_library_import_hint">Bir Steam kütüphanesi ekler ve Windows oyunları için tarar. Sahip olduğunuz oyunlar Steam kayıtlarını kullanır; geri kalanlar Steam dışı kısayollar olur.</string> + <string name="game_env_title">Oyun ortamı</string> + <string name="game_env_hint">Proton oyunları için ortam değişkenleri.</string> + <string name="game_env_edit">Düzenle</string> + <string name="game_env_shared">Tüm Proton oyunları</string> + <string name="game_env_scope">Profil</string> + <string name="game_env_app_id">Steam uygulama kimliği</string> + <string name="game_env_open">Aç</string> + <string name="game_env_profile">Oyun %1$s</string> + <string name="game_env_add">Değişken ekle</string> + <string name="game_env_name">Değişken adı</string> + <string name="game_env_value">Değer</string> + <string name="game_env_save">Kaydet</string> + <string name="game_env_remove">Kaldır</string> + <string name="game_env_restore">Yeniden devral</string> + <string name="game_env_reset">Ortam geçersiz kılmalarını temizle</string> + <string name="game_env_unset">Bu profilde tanımsız bırak</string> + <string name="game_env_empty">Ayarlanmış değişken yok.</string> + <string name="game_env_inherited">Devralındı</string> + <string name="game_env_custom">Özel değişken</string> + <string name="game_env_error">Yüklenemedi veya kaydedilemedi. Boş depolama alanını kontrol edip tekrar deneyin.</string> + <string name="game_env_invalid">Geçerli bir ad ve en fazla 8192 karakterlik bir değer kullanın.</string> + <string name="game_env_unused">Standart Proton çalışma zamanı bu değişkeni okumaz.</string> + <string name="game_env_loading">Yükleniyor…</string> + <string name="game_env_feature">D3D12 özellik düzeyi geçersiz kılma. 12_2, DirectX 12 Ultimate özelliklerini bildirir; desteklenmeyen özellikler hata verebilir. Otomatik algılama için boş bırakın.</string> + <string name="game_env_shader">D3D12 gölgelendirici modeli geçersiz kılma. 6_9 için yeterince yeni bir VKD3D ve uygun GPU özellikleri gerekir. Otomatik algılama için boş bırakın.</string> + <string name="game_env_cache">Tekrarlanan derlemeyi azaltmak için Mesa gölgelendirici önbelleğini açık tutun. false önbelleği etkinleştirir.</string> + <string name="game_env_cache_size">Azami Mesa gölgelendirici önbelleği boyutu, ör. 1G. Daha büyük önbellekler daha fazla depolama alanı kullanır.</string> + <string name="game_env_vkd3d_config">D3D12 için VKD3D seçenekleri. nodxr ışın izlemeyi kapatır; yalnızca buna ihtiyaç duyan bir oyunda kullanın.</string> + <string name="game_env_proton_log">Proton tanılama günlüğü için 1 olarak ayarlayın. Günlük kaydı performansı düşürebilir ve depolama alanı kullanır.</string> + <string name="game_env_wined3d">D3D9–11 uyumluluk testi için DXVK yerine OpenGL tabanlı WineD3D kullanmak üzere 1 olarak ayarlayın.</string> + <string name="game_env_no_ntsync">Bu oyunda ntsync\'i (droiddeck-ntsync veya çekirdek sürücüsü) kapatmak için 1 olarak ayarlayın; paket varsa droiddeck-esync, yoksa droiddeck-fsync kullanılır.</string> + <string name="game_env_no_esync">Bu oyunda droiddeck-esync\'i kapatmak için 1 olarak ayarlayın; yerine droiddeck-fsync, fsync de kapalıysa droiddeck-ntsync kullanılır.</string> + <string name="game_env_no_fsync">Bu oyunda droiddeck-fsync\'i kapatmak için 1 olarak ayarlayın; paket varsa droiddeck-esync, yoksa droiddeck-ntsync kullanılır.</string> + <string name="game_env_fsync_first">Bu oyunu droiddeck-fsync ile çalıştırmak için 1, Proton sürümüne uyan bir paket varsa droiddeck-esync\'i öne almak için 0 olarak ayarlayın.</string> + <string name="game_env_xalia">Bu oyunda Proton’un kumandayla gezinme yardımcısını kapatmak için 0 olarak ayarlayın.</string> + <string name="game_env_hud">D3D9–11 için DXVK katmanı. fps kare hızını gösterir; boş bırakılırsa gizlenir.</string> + <string name="game_env_dxvk_config">Noktalı virgülle ayrılmış DXVK ayarları. Yeni sürümler kare sınırlama için dxvk.maxFrameRate kullanır; normal oyun için oturumun kare sınırlayıcısını tercih edin.</string> + <string name="game_env_vkd3d_limit">D3D12 kare hızı sınırı. 0 sınırı kaldırır.</string> + <string name="game_env_glthread">Mesa OpenGL iş parçacığı kullanımı. true, CPU sınırlı OpenGL oyunlarına yardımcı olabilir; her oyunda ayrı deneyin.</string> + <string name="sync_backend_title">Wine senkronizasyonu</string> + <string name="sync_backend_hint">Oyunların iş parçacıklarını nasıl eşzamanladığı. Sonraki oturumda uygulanır.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">FEX ön ayarı</string> + <string name="fex_next_launch">Oyun bir sonraki başlatıldığında uygulanır.</string> + <string name="game_settings_title">Oyunlar</string> + <string name="fex_default_label">FEX varsayılanları</string> + <string name="fex_default_detail">FEX varsayılanlarını kullanır.</string> + <string name="fex_stability_label">Kararlılık</string> + <string name="fex_stability_detail">En güvenli, en yavaş ayarlar.</string> + <string name="fex_compatibility_label">Uyumluluk</string> + <string name="fex_compatibility_detail">Multiblock ile tam TSO.</string> + <string name="fex_intermediate_label">Orta</string> + <string name="fex_intermediate_detail">Düşürülmüş x87 hassasiyetiyle dengeli TSO.</string> + <string name="fex_performance_label">Performans</string> + <string name="fex_performance_detail">Tüm TSO kapalı; multiblock.</string> + <string name="fex_performance_tso_label">Performans + TSO</string> + <string name="fex_performance_tso_detail">Ana TSO açık Performans ayarları.</string> + <string name="fex_extreme_label">Ekstrem</string> + <string name="fex_extreme_detail">Kendini değiştiren kod denetimlerini kapatır. JIT kullanan oyunlar ve DRM bozulabilir.</string> + <string name="fex_extreme_tso_label">Ekstrem + TSO</string> + <string name="fex_extreme_tso_detail">Ana TSO açık Ekstrem ayarları.</string> + <string name="fex_extreme_gn_label">Ekstrem-gn</string> + <string name="fex_extreme_gn_detail">TSC ve meta veri ipuçlarıyla Performans. Ekstrem\'den önce deneyin.</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">Tam kod denetimleri ve gizlenmiş hipervizör bitiyle Performans.</string> + <string name="game_env_custom_value">Özel değer</string> + <string name="game_env_select_value">Değer seçin</string> + <string name="user_apps_section">Emülatörler ve uygulamalar</string> + <string name="user_apps_add">Ekle</string> + <string name="user_apps_add_detail">Kendi uygulamanız veya oyununuz</string> + <string name="user_apps_kind_script">Betik</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %%%3$d</string> + <string name="user_apps_starting">Başlatılıyor…</string> + <string name="user_apps_added">%1$s eklendi</string> + <string name="user_apps_removed">%1$s kaldırıldı</string> + <string name="user_apps_back">Masaüstü</string> + <string name="user_apps_gone">Bu uygulama artık yok. Masaüstünden yeniden seçin.</string> + <string name="user_apps_open">%1$s uygulamasını aç</string> + <string name="user_apps_remove">Kaldır</string> + <string name="user_apps_remove_confirm">Kaldırmak için tekrar basın</string> + <string name="user_apps_stop_session">Kaldırmak için oturumu durdurun</string> + <string name="user_apps_script_linked">%1$s kendi klasöründen çalışır; klasör bulunduğu yerden oturuma bağlanır. Uygulamayı kaldırmak o klasöre dokunmaz.</string> + <string name="user_apps_script_copied">%1$s, klasörünün DroidDeck içindeki bir kopyasından çalışır. Uygulamayı kaldırmak orijinali değil, kopyayı siler.</string> + <string name="user_apps_runtime_required">Önce Linux çalışma zamanını yükleyin</string> + <string name="user_apps_file_gone">Dosya artık yok</string> + <string name="user_apps_no_space">Yeterli boş alan yok: klasörü kopyalamak için %1$s gerekli</string> + <string name="user_apps_copying">%1$s kopyalanıyor</string> + <string name="user_apps_failed">Uygulama eklenemedi</string> + <string name="user_apps_github_checking">%1$s için sürüm aranıyor</string> + <string name="user_apps_github_unreachable">%1$s sürümleri okunamadı</string> + <string name="user_apps_github_no_appimage">%1$s sürümlerinin hiçbirinde ARM64 veya x86_64 AppImage yok</string> + <string name="user_apps_downloading">%1$s indiriliyor</string> + <string name="user_apps_download_failed">İndirme başarısız oldu</string> + <string name="user_apps_update_failed">Uygulama güncellenemedi</string> + <string name="user_apps_icon_failed">Bu simge okunamadı</string> + <string name="user_apps_saved">%1$s kaydedildi</string> + <string name="user_apps_edit">Düzenle</string> + <string name="user_apps_check_updates">Güncellemeleri kontrol et</string> + <string name="user_apps_checking_updates">Güncellemeler kontrol ediliyor…</string> + <string name="user_apps_update_to">%1$s sürümüne güncelle</string> + <string name="user_apps_up_to_date">Güncel · %1$s</string> + <string name="user_apps_updated">%1$s güncellendi</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">Uygulama ekle</string> + <string name="add_app_tab_script">Betik</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">Bir .sh dosyası. Kendi klasöründen, gamescope altında tam ekran çalışır.</string> + <string name="add_app_appimage_hint">ARM64 (aarch64) bir AppImage ya da FEX ile çalışan x86_64 bir AppImage. Linux çalışma zamanına bir kez çıkarılır.</string> + <string name="add_app_github_hint">Deponun en yeni sürümündeki ARM64 AppImage dosyasını ya da FEX ile çalışacak x86_64 dosyasını yükler.</string> + <string name="app_fex_title">FEX kullan</string> + <string name="app_fex_auto">Otomatik</string> + <string name="app_fex_on">Açık</string> + <string name="app_fex_off">Kapalı</string> + <string name="app_fex_hint">FEX, x86 Linux programlarını bu ARM64 cihazda çalıştırır. Otomatik, program x86 için derlenmişse FEX\'i kullanır.</string> + <string name="app_fex_x86">FEX ile %1$s</string> + <string name="app_fex_not_ready">Bu program %1$s PC\'ler için derlenmiş ve FEX ile çalışır, ancak FEX henüz kurulmadı. Oturum açmışken Steam\'i bir kez açın: DroidDeck, FEX\'i ve gereken Steam Linux Runtime\'ı indirtir, ardından bu program açılır.</string> + <string name="session_end_program">Bu program</string> + <string name="session_end_fex_missing">%1$s x86 PC\'ler için derlenmiş ve FEX ile çalışır, ancak FEX henüz yüklü değil. Oturum açmışken Steam\'i bir kez başlatıp FEX\'i ve Steam Linux Runtime\'ı indirmesini bekleyin, ardından bunu yeniden başlatın.</string> + <string name="add_app_flatpak_hint">Bir Flathub uygulama kimliği veya Flathub sayfası. Flatpak henüz kurulu değilse önce o kurulur.</string> + <string name="add_app_file">Dosya</string> + <string name="add_app_no_file">Dosya seçilmedi</string> + <string name="add_app_choose_file">Dosya seç</string> + <string name="add_app_change_file">Değiştir</string> + <string name="add_app_pick_script">Bir betik seçin</string> + <string name="add_app_pick_appimage">Bir AppImage seçin</string> + <string name="add_app_pick_icon">Bir simge seçin</string> + <string name="add_app_repository">Depo</string> + <string name="add_app_repository_placeholder">sahip/depo veya github.com bağlantısı</string> + <string name="add_app_repository_invalid">GitHub deposu değil</string> + <string name="add_app_flatpak_id">Uygulama kimliği</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">Flathub uygulama kimliği değil</string> + <string name="add_app_flatpak_bundle_hint">Ya da Flathub\'da olmayan bir uygulama için .flatpak dosyası.</string> + <string name="add_app_pick_flatpak">Bir .flatpak dosyası seçin</string> + <string name="add_app_source">Kaynak</string> + <string name="add_app_appearance">Masaüstünüzde</string> + <string name="add_app_preview_name">Yeni uygulama</string> + <string name="add_app_name">Ad</string> + <string name="add_app_name_placeholder">Uygulamadan</string> + <string name="add_app_icon">Simge</string> + <string name="add_app_icon_default">Varsayılan simge</string> + <string name="add_app_choose_icon">Simge seç</string> + <string name="add_app_clear_icon">Temizle</string> + <string name="add_app_icon_suggestions">Depodan</string> + <string name="add_app_icon_searching">Simgeler aranıyor…</string> + <string name="add_app_icon_reset">Varsayılan</string> + <string name="edit_app_title">Düzenle: %1$s</string> + <string name="edit_app_save">Kaydet</string> + <string name="add_app_checking_folder">Klasör kontrol ediliyor…</string> + <string name="add_app_folder_linked">Klasörü oturuma bağlanır: %1$s</string> + <string name="add_app_folder_copied">Klasöründe paylaşılan depolamanın çalıştıramadığı Linux programları var, bu yüzden DroidDeck içine kopyalanır (%1$s).</string> + <string name="add_app_confirm">Ekle</string> + <string name="add_app_cancel">İptal</string> + <string name="add_app_runtime_required">Önce Kurulum sayfasından Linux çalışma zamanını yükleyin.</string> + <string name="add_app_busy">%1$s bitene kadar bekleyin.</string> + <string name="steam_compat_label">Uyumlu</string> + <string name="proton_compatible_note">Steam’in Uyumluluk menüsünde “%1$s” ile işaretli sürümler DroidDeck’in önek düzeltmelerini içerir ve oyunları burada başlatır. Varsayılan Proton Experimental ARM64’tür (%1$s); daha yeni Proton 11.0 ARM64 (%1$s) yüklüyse o kullanılır. Valve’ın bu işareti taşımayan kendi Proton girdileri bu cihazda oyun başlatamaz.</string> + <string name="proton_installed_compatible">Yüklü: %1$s · %2$s</string> + <string name="common_cancel">İptal</string> + <string name="common_auto">Otomatik</string> + <string name="common_never">Asla</string> + <string name="common_always">Her zaman</string> + <string name="common_uninstall">Yüklemeyi kaldır</string> + <string name="common_check">Kontrol et</string> + <string name="common_update">Güncelle</string> + <string name="common_checking">Kontrol ediliyor…</string> + <string name="common_working">İşleniyor…</string> + <string name="common_forget">Unut</string> + <string name="common_add_ellipsis">Ekle…</string> + <string name="common_use">Kullan</string> + <string name="common_applies_next_session">Sonraki oturumda uygulanır.</string> + <string name="mode_steam_title">Steam oturumu</string> + <string name="mode_desktop_title">Masaüstü oturumu</string> + <string name="mode_display">Ekran</string> + <string name="mode_resolution">Çözünürlük</string> + <string name="mode_res_720">En fazla 720p</string> + <string name="mode_res_900">En fazla 900p</string> + <string name="mode_res_1080">En fazla 1080p</string> + <string name="mode_res_native">Yerel</string> + <string name="mode_res_default">%1$s (varsayılan)</string> + <string name="mode_res_custom_value">Özel · %1$d×%2$d</string> + <string name="mode_res_custom">Özel…</string> + <string name="mode_res_note">720p, menülerin daha hızlı tepki vermesini sağlayabilir.</string> + <string name="mode_ratio">Ekran oranı</string> + <string name="mode_ratio_custom">Özel çözünürlüğe göre ayarlanır.</string> + <string name="mode_ratio_auto">Otomatik en az 16:9 kullanır.</string> + <string name="mode_fps">Kare sınırı</string> + <string name="mode_fps_note">Tüm oturumu sınırlar. Ekran, sınırın tam katı olan bir yenileme hızına geçer.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">HDR10 çıkışı</string> + <string name="mode_hdr_unavailable">Kullanılamıyor: %1$s.</string> + <string name="mode_hdr_restart">Uygulamak için uygulamayı yeniden başlatın.</string> + <string name="mode_drivers">Sürücüler</string> + <string name="mode_gpu_drivers">GPU sürücüleri</string> + <string name="mode_gpu_drivers_hint">Steam ve Masaüstü tarafından paylaşılır. Bileşenler sayfasından ayarlanır.</string> + <string name="mode_touch_controls">Dokunma ve kontroller</string> + <string name="mode_touch">Dokunma</string> + <string name="mode_touch_touchpad">Dokunmatik yüzey</string> + <string name="mode_touch_direct">Doğrudan</string> + <string name="mode_touch_note">Otomatik: Masaüstünde dokunmatik yüzey, Steam\'de doğrudan.</string> + <string name="mode_osc">Ekran üstü kontroller</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">Otomatik, kumanda bağlı değilken kontrolleri gösterir.</string> + <string name="osc_steam_touch">Steam dokunmatik (beta)</string> + <string name="osc_style_next_session">Steam\'in dokunmatik kontrolleri sonraki Steam oturumunda başlar</string> + <string name="osc_steam_layout">Steam dokunmatik düzeni</string> + <string name="osc_steam_layout_hint">Bu oyunun dokunmatik kontrollerini taşıyın, boyutlandırın veya gizleyin; Steam\'e kaydedilir</string> + <string name="mode_controller">Kumanda</string> + <string name="mode_controller_hint">Steam\'in kumandanızı nasıl gördüğü. Sonraki oturumda uygulanır.</string> + <string name="mode_controller_deck">Steam Deck kumandası</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Xbox 360 kumandası</string> + <string name="mode_controller_note">Deck: Hızlı Erişim düğmesi ve jiroskop. Steam Controller: aynısı, ayrıca arka düğmelerde kavrama algılama. DualShock 4 ve DualSense: jiroskop; dokunmatik yüzeyler kumandanın dokunmatik paneli olur. Xbox 360: Hızlı Erişim\'i Guide+A ile açın.</string> + <string name="mode_back">Geri</string> + <string name="back_menu_then_qam">Menü → QAM</string> + <string name="back_qam_then_menu">QAM → Menü</string> + <string name="mode_session">Oturum</string> + <string name="mode_suspend">Arka plan davranışı</string> + <string name="mode_suspend_hint">Uygulama gizlendiğinde veya ekran kapandığında.</string> + <string name="mode_suspend_manual">Manuel</string> + <string name="mode_suspend_native">Yerel</string> + <string name="mode_suspend_steam_note">Otomatik: duraklatır, gösterilince devam eder. Yerel: önce Steam\'i uykuya hazırlar. Manuel: Devam et\'i bekler. Asla: çalışmaya devam eder.</string> + <string name="mode_background_downloads">İndirmeleri sürdür</string> + <string name="mode_background_downloads_hint">Steam etkin olarak indirme yaparken arka planda askıya almayı erteler.</string> + <string name="mode_suspend_note">Otomatik: arka planda duraklatır, gösterilince devam eder. Manuel: Devam et\'i bekler. Asla: çalışmaya devam eder.</string> + <string name="mode_startup">Başlangıç</string> + <string name="mode_steam_startup">Steam\'i DroidDeck ile başlat</string> + <string name="mode_steam_startup_hint">DroidDeck açıldığında Steam oturumunu açar.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">Sürümler kontrol ediliyor…</string> + <string name="mode_decky_ready">%1$s yüklenmeye hazır.</string> + <string name="mode_decky_none">Uyumlu derleme bulunamadı. Daha sonra tekrar deneyin.</string> + <string name="mode_decky_installed">Yüklü · %1$s</string> + <string name="mode_decky_update">Güncelleme var · %1$s</string> + <string name="mode_decky_current">Güncel · %1$s</string> + <string name="mode_decky_loader">Yükleyici</string> + <string name="mode_decky_install_latest">En yenisini yükle</string> + <string name="mode_decky_on">Steam ile başlar. Açıkken bu cihazdaki diğer uygulamalar Steam\'i kontrol edebilir.</string> + <string name="mode_decky_off">Kapalı. Steam\'in uzaktan hata ayıklama bağlantı noktası kapalı kalır.</string> + <string name="mode_decky_remove_title">Decky Loader kaldırılsın mı?</string> + <string name="mode_decky_remove_text">Yükleyiciyi kaldırır. Eklentileriniz ve ayarlarınız korunur.</string> + <string name="mode_client">İstemci</string> + <string name="mode_mangoapp">Performans katmanı</string> + <string name="mode_mangoapp_hint">Oyunlar Deck modunda çökerse veya ekran kararırsa kapatın. Sonraki oturumda uygulanır.</string> + <string name="mode_steam_repair">Steam istemcisini onar</string> + <string name="mode_steam_repair_hint">Steam bir sonraki başlatıldığında Steam istemcisini yeniden indirir. Steam yükleme ekranında takılı kalırsa kullanın. Oyunlar, oturum bilgileri ve ayarlar korunur.</string> + <string name="mode_steam_repair_queued">Steam istemcisi, Steam bir sonraki başlatıldığında yeniden indirilecek.</string> + <string name="mode_steam_repair_button">Onar</string> + <string name="mode_added_art">Görseller</string> + <string name="mode_sgdb_key">SteamGridDB API anahtarı</string> + <string name="mode_sgdb_key_yours">Kendi anahtarınız</string> + <string name="mode_sgdb_edit">Düzenle…</string> + <string name="mode_added_art_hint">Eksik görselleri önce Steam\'den, sonra SteamGridDB\'den tamamlar. Oyun klasöründeki kendi cover.jpg, hero.jpg, logo.png veya icon.png dosyalarınız önceliklidir.</string> + <string name="summary_remove_game">Kaldır: %1$s</string> + <string name="mode_added_remove_confirm">Kaldırılsın mı?</string> + <string name="games_add">Oyun ekle</string> + <string name="games_add_title">Oyun ekle:</string> + <string name="games_pick_exe_title">Exe dosyasını seçin:</string> + <string name="games_add_unreachable">Bu konum oturumda kullanılamıyor</string> + <string name="mode_added_exe_check">Bu exe dosyasını kontrol et</string> + <string name="wincomp_title">Windows bileşenleri</string> + <string name="wincomp_pad_hint">LB / RB ile bölümler arasında geçin.</string> + <string name="wincomp_card_none">Yok</string> + <string name="wincomp_applies">Proton\'un içermediği Windows DLL\'leri, bileşen listesinden. Bu oyunun ihtiyaç duyduklarını açın: oyunun dosyalarına değil, kendi Windows klasörüne yüklenir ve oyun hangi Proton ile çalışırsa çalışsın yerinde kalır.</string> + <string name="wincomp_offline">Bileşen listesine ulaşılamadı. Önceden indirilmiş bileşenler yine de açılıp kapatılabilir.</string> + <string name="wincomp_recommended">Bu oyun için önerilenler</string> + <string name="wincomp_checking">Bu oyunun ihtiyaçları kontrol ediliyor…</string> + <string name="wincomp_when_online">Çevrimiçi olunca kullanılabilir</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">%1$d bileşen açılıyor…</item> + <item quantity="other">%1$d bileşen açılıyor…</item> + </plurals> + <string name="wincomp_auto_section">Bu oyun için açık</string> + <string name="wincomp_auto_steam">Steam, %1$s bileşenini %2$s ile yükler (uygulama %3$s)</string> + <string name="wincomp_auto_gog">Bu oyun için GOG listesinden</string> + <string name="wincomp_auto_epic">Bu oyun için Epic ön koşullarından</string> + <string name="wincomp_auto_amazon">Bu oyun için Amazon kurulum adımlarından</string> + <string name="wincomp_steam_by_name">Steam, %1$s bileşenini %2$s uygulamasıyla yükler (adla eşleştirildi)</string> + <string name="wincomp_suggestions">Ayrıca listelenenler</string> + <string name="wincomp_suggestion_reason">%1$s tarafından listelendi; burada bir bileşen değil</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">Oyun %1$s ile birlikte gelir</string> + <string name="wincomp_found_shipped">Oyunda %1$s var (çoğu zaman olduğu gibi çalışır)</string> + <string name="wincomp_already_here">Bu oyunda zaten var</string> + <string name="wincomp_no_recommendation">Oyunun klasöründe bir bileşene işaret eden bir şey yok.</string> + <string name="wincomp_active">Bu oyun için yüklü (%1$d)</string> + <string name="wincomp_all">Tüm bileşenler (%1$d)</string> + <string name="wincomp_installers">Windows yükleyicisi gerekenler (%1$d)</string> + <string name="wincomp_installers_note">Bunlar, içinde açılacak bir paket bulunmayan kendi kurulum programlarıdır ve yükledikleri şeylerin henüz bir kaydı yok. Çoğu, Proton içindeki Mono\'nun zaten yerini tuttuğu eski çalışma zamanlarıdır; kayıtlar hazırlandıkça eklenir.</string> + <string name="wincomp_show">Göster</string> + <string name="wincomp_hide">Gizle</string> + <string name="wincomp_needs_installer">Kendi kurulum programını çalıştırır (henüz kayıt yok)</string> + <string name="wincomp_unsupported">Burada henüz desteklenmiyor</string> + <string name="wincomp_loading">Bileşenler yükleniyor…</string> + <string name="wincomp_failed">Yüklenemedi: %1$s</string> + <string name="wincomp_downloaded">İndirildi</string> + <string name="wincomp_downloading">%1$s indiriliyor</string> + <string name="wincomp_installing">Yükleniyor: %1$s</string> + <string name="wincomp_reverting">Kapatılıyor…</string> + <string name="wincomp_download_size">İndirme: %1$s</string> + <string name="wincomp_next_launch">Oyun bir sonraki başlatıldığında etkili olur.</string> + <string name="mode_stretch169">16:9 oyunları uzat</string> + <string name="mode_stretch169_hint">16:9\'dan daha kare ekranlarda oyunları siyah şerit göstermek yerine ekranı dolduracak şekilde uzatır. Sonraki oturumda uygulanır.</string> + <string name="mode_audio">Ses</string> + <string name="mode_directaudio">Oyunlar için DirectAudio</string> + <string name="mode_directaudio_hint">Daha düşük gecikme için PulseAudio\'yu atlar.</string> + <string name="mode_mic">Mikrofon</string> + <string name="mode_mic_hint">Sesli sohbet için.</string> + <string name="mode_network">Ağ</string> + <string name="mode_wifi_discovery">Wi-Fi keşfi</string> + <string name="mode_wifi_discovery_hint">Steam\'de Wi-Fi adlarını ve yakındaki ağları göster.</string> + <string name="mode_wifi_location_off">Konum hizmetleri kapalı.</string> + <string name="mode_wifi_permission_denied">Konum izni verilmedi.</string> + <string name="mode_wifi_open_settings">Ayarları aç</string> + <string name="mode_wifi_continue">Devam</string> + <string name="mode_wifi_explain_title">Wi-Fi keşfi etkinleştirilsin mi?</string> + <string name="mode_wifi_explain_text">Android, Wi-Fi adlarını okumak ve yakındaki ağları keşfetmek için Konum izni gerektirir. DroidDeck bu erişimi, bu ağları Steam\'de göstermek için kullanır. Konumunuzu takip etmeyiz. İnternet erişimi bu izin olmadan da çalışır.</string> + <string name="mode_storage">Oyun depolama alanı</string> + <string name="mode_storage_auto">Otomatik - varsa SD kart, ayrıca Games klasörleriniz</string> + <string name="mode_storage_auto_none">Otomatik - Games klasörleriniz, ayrıca varsa SD kart (şu an yok)</string> + <string name="mode_storage_internal">Yalnızca dahili</string> + <string name="mode_storage_folder">Klasör: %1$s</string> + <string name="mode_second_library">İkinci kütüphane</string> + <string name="mode_storage_note">Varlıkları SD karttan veya paylaşılan depolamadan akışla yükleyen oyunlarda takılmalar olabilir.</string> + <string name="mode_storage_diagnostics">Depolama tanılaması</string> + <string name="mode_storage_diagnostics_hint">Ayırma sonuçlarını ve örneklenmiş depolama sürelerini “Günlükleri paylaş” çıktısına ekler. Açıkken yüklemeleri yavaşlatabilir. Sonraki oturumda uygulanır.</string> + <string name="mode_choose_folder">Klasör seçin…</string> + <string name="mode_custom_res_title">Özel çözünürlük</string> + <string name="mode_custom_res_text">Sınırın ve oranın yerine geçer. Ekrana uymayan bir boyutta siyah şeritler görünür.</string> + <string name="mode_width">Genişlik</string> + <string name="mode_height">Yükseklik</string> + <string name="mode_custom_res_range">320×240 ile 3840×2160 arasında.</string> + <plurals name="mode_added_count"> + <item quantity="one">%1$d oyun</item> + <item quantity="other">%1$d oyun</item> + </plurals> + <string name="common_ok">Tamam</string> + <string name="common_delete">Sil</string> + <string name="common_restore">Geri yükle</string> + <string name="common_download">İndir</string> + <string name="common_delete_named">Sil: %1$s</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Çalışma zamanı sürücüsü</string> + <string name="comp_runtime_driver_hint">Steam, oyunlar ve masaüstü tarafından kullanılır. Sonraki oturumda uygulanır.</string> + <string name="comp_import_turnip">Turnip zip dosyası içe aktar…</string> + <string name="comp_display_driver">Ekran sürücüsü</string> + <string name="comp_display_driver_hint">Her iki modda kareleri ekrana çizer. Uygulamak için uygulamayı yeniden başlatın.</string> + <string name="comp_import_adrenotools">AdrenoTools zip dosyası içe aktar…</string> + <string name="comp_never">hiçbir zaman</string> + <string name="comp_nightlies_checked">Nightlies son kontrol: %1$s</string> + <string name="comp_title">Bileşenler</string> + <string name="comp_about">Bileşenler hakkında</string> + <string name="comp_check_drivers">Yeni sürücüleri kontrol et</string> + <string name="comp_check_nightlies">Yeni paketler için Nightlies\'i kontrol et</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">Burada seçilen Proton, Steam\'de kendi Proton\'unu seçmemiş her oyunu çalıştırır; Steam\'de yapılan bir değişiklik de burada görünür. Steam güncellenince Valve\'ın Proton\'unu değiştirir; orijinaller her derleme için saklanır.</string> + <string name="comp_scope">Yapılandırma</string> + <string name="comp_profile_configure">Profili yapılandır</string> + <string name="comp_profile">Profil</string> + <string name="comp_scope_general">Varsayılan profil</string> + <string name="comp_scope_general_detail">Geçersiz kılma ayarı olmayan oyunlarda otomatik kullanılır.</string> + <string name="comp_profile_game_detail">%1$s başlatıldığında otomatik kullanılan kayıtlı ayarlar.</string> + <string name="comp_game_hint">Yalnızca bu oyunun ihtiyaç duyduğu ayarları seçin. Geri kalan her şey Varsayılan profili kullanmaya devam eder.</string> + <string name="comp_game_fex">FEX ön ayarı</string> + <string name="comp_game_fex_inherited">Varsayılan profilin ön ayarı kullanılıyor.</string> + <string name="comp_game_fex_override">Bu oyun için özel ön ayar kaydedildi.</string> + <string name="comp_game_fex_custom">Özel FEX değişkenleri</string> + <string name="comp_game_use_general">Varsayılanı kullan</string> + <string name="comp_game_compatibility">Oyun uyumluluğu</string> + <string name="comp_game_environment_hint">Bu oyun için Proton, DXVK, VKD3D ve senkronizasyon ortam seçenekleri.</string> + <string name="comp_game_proton_inherited">Varsayılan profilin Proton\'u kullanılıyor.</string> + <string name="comp_game_proton_override">Bu oyun için özel Proton, Steam uyumluluk eşlemesiyle kaydedildi.</string> + <string name="comp_no_proton_short">Proton yok</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Bu oyun başka bir paket seçmedikçe %1$s kaynağındaki Varsayılan profil paketini kullanır.</string> + <string name="comp_game_component_general">Varsayılan profilin bileşen paketini kullanır.</string> + <string name="comp_game_component_override">Yalnızca bu oyun için %1$s kullanır.</string> + <string name="comp_game_runtime_options">Çalışma zamanı seçenekleri</string> + <string name="comp_game_environment_other_hint">Oyuna özel diğer Proton, DXVK, VKD3D-Proton ve senkronizasyon ortam seçenekleri.</string> + <string name="comp_game_saved_next">%1$s profili kaydedildi; oyun bir sonraki başlatıldığında uygulanır.</string> + <string name="game_no_steam_title">Steam olmadan başlat</string> + <string name="game_no_steam_detail">Hemen kapanan bir oyun için: Proton\'un Steam kurulumu olmadan başlatır. Overlay gibi Steam özellikleri bu oyun için kapalıdır.</string> + <string name="game_no_steam_on">Açık</string> + <string name="comp_game_proton_live">%1$s için Proton seçimi kaydedildi ve şimdi Steam\'de kullanılabilir.</string> + <string name="comp_game_proton_next">%1$s için Proton seçimi kaydedildi. Oyunu başlatmadan önce Steam\'i yeniden başlatın.</string> + <string name="comp_game_proton_not_runnable">%1$s bu cihazda seçili Proton\'u kullanamaz.</string> + <string name="comp_game_load_failed">%1$s profili yüklenemedi: %2$s</string> + <string name="comp_game_save_failed">Oyun profili kaydedilemedi: %1$s</string> + <string name="comp_game_reset">Oyun profilini sıfırla</string> + <string name="comp_game_reset_title">%1$s profili sıfırlansın mı?</string> + <string name="comp_game_reset_body">Bu oyunun Proton, FEX, bileşen paketi, ortam, doku filtreleme ve Windows bileşeni geçersiz kılmaları kaldırılsın mı? Oyun Varsayılan profili kullanır.</string> + <string name="comp_game_reset_done">%1$s artık Varsayılan profili kullanıyor.</string> + <string name="comp_game_reset_failed">Oyun profili tamamen sıfırlanamadı: %1$s</string> + <string name="comp_default_live">%1$s artık Steam\'de kendi Proton\'unu seçmemiş her oyunu çalıştırıyor.</string> + <string name="comp_default_next">%1$s, Steam bir sonraki başlatıldığından itibaren kendi Proton\'unu seçmemiş her oyunu çalıştıracak.</string> + <string name="comp_default_not_runnable">%1$s burada oyun başlatamaz, bu yüzden varsayılan olamaz.</string> + <string name="comp_default_failed">Varsayılan Proton kaydedilemedi: %1$s</string> + <string name="comp_swapped">%1$s · %2$d değiştirildi</string> + <string name="comp_all_original">%1$s · tümü orijinal</string> + <string name="comp_game_running_suffix">%1$s · oyun çalışıyor</string> + <string name="comp_esync_pack">%1$s · droiddeck-esync paketi</string> + <string name="comp_esync_wanted">%1$s · henüz droiddeck-esync paketi yok</string> + <string name="comp_esync_builtin">%1$s · esync yerleşik, paket gerekmez</string> + <string name="comp_gpu_drivers">GPU sürücüleri</string> + <string name="comp_reapplied">%1$s · başlatmada yeniden uygulandı %2$s</string> + <string name="comp_next_queued">Sıradaki: %1$s · oyun kapandıktan sonra</string> + <string name="comp_in_use">%1$s kullanımda: %2$s</string> + <string name="comp_waits">Oyun kapanana kadar bekler</string> + <string name="comp_applies_next_game">Bir oyun bir sonraki başlatıldığında uygulanır</string> + <string name="comp_import_zip">.zip içe aktar</string> + <string name="comp_reading">Proton sürümleri okunuyor…</string> + <string name="comp_no_proton">Henüz Proton yüklü değil. ARM64 Proton\'u indirmesi için Steam\'i bir kez başlatın veya Kurulum sayfasından GE-Proton, CachyOS ya da DroidDeck-Proton ekleyin.</string> + <string name="comp_original">Orijinal · %1$s</string> + <string name="comp_this_build">%1$s · bu derleme</string> + <string name="comp_earlier_build">%1$s · önceki derleme</string> + <string name="comp_installed_count">Yüklü · %1$d</string> + <string name="comp_nothing_stored">%1$s için henüz saklanan bir şey yok. Bu Proton\'un kendi dosyaları orijinali olarak kaydedilir.</string> + <string name="comp_waits_long">Bu Proton\'da bir oyun çalışıyor, bu yüzden oyun kapanana kadar beklenir.</string> + <string name="comp_applies_long">Bir oyun bir sonraki başlatıldığında uygulanır.</string> + <string name="comp_swap_title">%1$s ile değiştirilsin mi?</string> + <string name="comp_swap_body">%1$s, %2$s içine yerleştirilsin mi? Orijinal dosyaları kayıtlı kalır. %3$s</string> + <string name="comp_restore_title">%1$s geri yüklensin mi?</string> + <string name="comp_restore_body">%1$s orijinali %2$s içine geri konulsun mu? %3$s</string> + <string name="comp_delete_package_body">Paketi uygulamadan kaldırır. Yeniden indirebilirsiniz.</string> + <string name="comp_delete_original_title">Bu orijinal silinsin mi?</string> + <string name="comp_delete_original_body">Bu %1$s orijinali, %2$s sürümünün önceki bir derlemesine ait. Geçerli derlemenin orijinali her zaman saklanır.</string> + <string name="comp_nightlies_count">Nightlies içinde · %1$d</string> + <string name="comp_refresh_to_list">Nightlies paketlerini listelemek için yenileyin.</string> + <string name="comp_nothing_new">Son kontrolden beri yeni bir şey yok.</string> + <string name="comp_import_wcp">.wcp içe aktar</string> + <string name="comp_restore_original">Bu Proton\'un orijinalini geri yükle</string> + <string name="comp_restore_original_body">%1$s için orijinal %2$s geri yüklensin mi?</string> + <string name="comp_swap">Değiştir</string> + <string name="comp_about_text">Steam\'in oyunları çalıştırdığı her Proton için FEX, DXVK ve VKD3D-Proton.\n\nDeğiştirmek için yüklü bir satırı seçin. Bir oyun bir sonraki başlatıldığında ya da çalışan oyun kapandıktan sonra uygulanır.\n\nSeçimleriniz her başlatmadan önce yeniden kontrol edilir ve bir şey onları değiştirdiyse geri konur. Her Proton\'un orijinal dosyaları saklanır, böylece Steam güncellemeleri onları asla kaybettirmez.</string> + <string name="comp_tag_original">Orijinal</string> + <string name="comp_tag_in_use">Kullanımda</string> + <string name="comp_tag_new">Yeni</string> + <string name="comp_tag_stored">Saklandı</string> + <string name="perf_title">Performans</string> + <string name="perf_lede">CPU çekirdekleri, oturum düzeltmeleri ve ana bilgisayar adı. Sonraki oturumda uygulanır.</string> + <string name="perf_client_cores_group">Steam istemcisi çekirdekleri</string> + <string name="perf_override">Steam\'in çekirdek seçimini geçersiz kıl</string> + <string name="perf_override_hint">Steam\'i, arayüz yardımcısını ve gamescope\'u aşağıdaki çekirdeklere sabitler.</string> + <string name="perf_client_cores">İstemci çekirdekleri</string> + <string name="perf_client_cores_on">Steam için çekirdekler.</string> + <string name="perf_client_cores_off">Seçmek için geçersiz kılmayı açın.</string> + <string name="perf_game_cores_group">Oyun çekirdekleri</string> + <string name="perf_game_cores">Oyun çekirdekleri</string> + <string name="perf_game_cores_hint">Oyunlar için çekirdekler. Tümü seçiliyse Android serbestçe zamanlar.</string> + <string name="perf_client_ui">İstemci arayüzü</string> + <string name="perf_glthread">İş parçacıklı GL</string> + <string name="perf_glthread_hint">Steam menülerinin daha hızlı tepki vermesini sağlayabilir.</string> + <string name="perf_zink">Zink: tembel tanımlayıcılar</string> + <string name="perf_zink_hint">Tanımlayıcı arabelleği olmayan sürücüler için önerilir.</string> + <string name="perf_noglerror">GL hata denetimlerini atla</string> + <string name="perf_noglerror_hint">Çağrı başına GL doğrulamasını kapatır.</string> + <string name="perf_gsrealtime">gamescope: gerçek zamanlı GPU kuyruğu</string> + <string name="perf_gsrealtime_hint">Bileştiricinin GPU işine öncelik verir. Oyunlardan biraz feda ederek kare zamanlamasını yumuşatabilir.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">GPU\'yu en yüksek saat hızında tut</string> + <string name="perf_gpuclock_hint">Yalnızca Adreno. Daha az takılma, daha fazla pil tüketimi ve ısınma.</string> + <string name="perf_fixes">Oturum düzeltmeleri</string> + <string name="perf_sysmem">Turnip: sysmem işleme</string> + <string name="perf_sysmem_hint">Steam ve çoğu oyun için daha hızlı. Adreno 710/720/722\'de gereklidir.</string> + <string name="perf_xalia">Steam\'in xalia yardımcısını atla</string> + <string name="perf_fastsync">droiddeck-ntsync (deneysel)</string> + <string name="perf_fastsync_hint">Linux ntsync sürücüsünün test amaçlı, kullanıcı alanında çalışan bir yedeği. Açıkken Proton, droiddeck-esync ve droiddeck-fsync yerine bunu kullanır. Proton Experimental ARM64, GE-Proton ve proton-cachyos ile çalışır. Sonraki oturumda uygulanır.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">Önce droiddeck-fsync</string> + <string name="perf_fsync_hint">Proton\'un kendi fsync\'ini standart Proton dosyalarıyla, futex_waitv kullanıcı alanında sağlanarak çalıştırır; bu yüzden paket gerektirmez ve Steam Proton\'u güncellediği anda da çalışmaya devam eder. Açıkken oyunlar, Proton\'un fsync dışında tuttuğu birkaç oyun hariç, bunu droiddeck-esync\'ten önce kullanır. Sonraki oturumda uygulanır.</string> + <string name="perf_esync_hint">Oyunların Windows iş parçacıkları, wineserver üzerinden gidip gelmek yerine eventfd ve paylaşılan bellek aracılığıyla birbirini bekler. Tam olarak o Proton derlemesi için hazırlanmış bir droiddeck-esync paketi kullanır; paketler otomatik indirilir. Yeni bir Proton derlemesi için paket çıkana kadar droiddeck-fsync devreye girer. Sonraki oturumda uygulanır.</string> + <string name="perf_xalia_hint">Proton\'un her oyunda CPU harcayan kumandayla gezinme yardımcısını kapatır.</string> + <string name="perf_seccomp">proot\'u seccomp olmadan çalıştır</string> + <string name="perf_seccomp_hint">Bazı çekirdeklerde eksik sistem çağrısı hatalarını düzeltebilir. Performansı düşürebilir.</string> + <string name="perf_fastpath">proot hızlı yolu</string> + <string name="perf_fastpath_needs_seccomp">proot\'un seccomp filtresi gerekir.</string> + <string name="perf_fastpath_hint">Daha hızlı dosya aramaları ve daha az takılma. Bir program dosyalarını bulamıyorsa kapatın.</string> + <string name="perf_identity">Oturum kimliği</string> + <string name="perf_hostname">Ana bilgisayar adı</string> + <string name="perf_hostname_hint">Steam\'in bu cihaz için gördüğü ad. Boş bırakılırsa %1$s geri yüklenir.</string> + <string name="perf_hostname_invalid">Yalnızca harf, rakam ve arada kısa çizgi; en fazla 63 karakter.</string> + <string name="perf_phantom_warning">Android bu oturumu sonlandıracak şekilde ayarlı</string> + <string name="driver_installed">Yüklü</string> + <string name="driver_available">İndirilebilir</string> + <string name="driver_nothing_new">Son kontrolden beri yeni bir şey yok.</string> + <string name="driver_restore_builtin">Yerleşik sürücüleri geri yükle</string> + <string name="driver_delete_builtin">Uygulamada yerleşik: bu işlem açılmış dosyalarını kaldırır ve sürücüyü gizler. “Yerleşik sürücüleri geri yükle” onu geri getirir.</string> + <string name="driver_delete_bundle">Bir Android + Linux paketinin yarısı: iki yarı da kaldırılır. Kullanımdaki bir sürücü varsayılana döner.</string> + <string name="driver_delete_plain">Dosyaları uygulamadan kaldırılır.</string> + <string name="driver_delete_in_use">Dosyaları kaldırılır. Kullanımda olduğu için varsayılan devreye girer.</string> + <string name="drawer_page_display">Ekran</string> + <string name="drawer_page_effects">Efektler</string> + <string name="drawer_page_controls">Kontroller</string> + <string name="drawer_page_components">Bileşenler</string> + <string name="drawer_page_controller">Kumanda</string> + <string name="drawer_page_settings">Ayarlar</string> + <string name="drawer_page_desc">%1$s sayfası</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Masaüstü</string> + <string name="drawer_steam_menu">Steam menüsü</string> + <string name="drawer_quick_access">Hızlı erişim</string> + <string name="drawer_open_qam">Hızlı Erişim Menüsünü aç</string> + <string name="drawer_prev_page">Önceki sayfa</string> + <string name="drawer_next_page">Sonraki sayfa</string> + <string name="drawer_hud">Performans HUD\'u</string> + <string name="drawer_scaling">Ölçekleme modu</string> + <string name="drawer_scaling_sharpness">Ölçekleme keskinliği</string> + <string name="drawer_effects">Ekran efektleri</string> + <string name="drawer_effects_hint">Anında uygulanır. Herhangi bir efekt açıkken biraz GPU süresi harcar.</string> + <string name="drawer_look">Ön ayarlar</string> + <string name="drawer_look_custom">Özel</string> + <string name="drawer_effects_advanced">Gelişmiş</string> + <string name="widgets_expanded">Genişletildi</string> + <string name="widgets_collapsed">Daraltıldı</string> + <string name="drawer_cas">Keskinleştirme (CAS)</string> + <string name="drawer_cas_level">Keskinleştirme düzeyi</string> + <string name="drawer_fake_hdr">Sahte HDR</string> + <string name="drawer_deband">Bant giderme</string> + <string name="drawer_deband_strength">Bant giderme gücü</string> + <string name="drawer_brightness">Parlaklık</string> + <string name="drawer_contrast">Kontrast</string> + <string name="drawer_gamma">Gama</string> + <string name="drawer_saturation">Doygunluk</string> + <string name="drawer_fxaa">FXAA (kenar yumuşatma)</string> + <string name="drawer_toon">Çizgi film konturları</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Doku filtreleme</string> + <string name="drawer_texture_hint">DirectX 9–11 oyunları, bir sonraki başlatmadan itibaren.</string> + <string name="drawer_anisotropy">Anizotropik filtreleme</string> + <string name="drawer_texture_sharpness">Doku keskinliği</string> + <string name="drawer_texture_sharpness_note">Negatif mip sapması: daha keskin dokular, biraz titreşim. Otomatik, oturumun ekrana ne kadar büyütüldüğüne göre ayarlanır.</string> + <string name="drawer_touch_auto">Otomatik (%1$s)</string> + <string name="drawer_keyboard">Klavye</string> + <string name="drawer_pc_keyboard">PC klavyesi</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">İkinci ekran</string> + <string name="drawer_second_screen_mode">Mod</string> + <string name="drawer_second_screen_shows">Gösterilen</string> + <string name="drawer_android_apps">Android uygulamaları</string> + <string name="drawer_launch_app">Uygulama başlat</string> + <string name="common_hide">Gizle</string> + <string name="common_show">Göster</string> + <string name="drawer_no_apps">Başlatılabilir uygulama yok</string> + <string name="drawer_session_behavior">Oturum davranışı</string> + <string name="drawer_suspend_hint">Hemen ve bu moddaki sonraki oturumlarda uygulanır.</string> + <string name="drawer_next_session">Sonraki oturum</string> + <string name="drawer_support">Destek</string> + <string name="drawer_logs">Oturum günlükleri</string> + <string name="drawer_logs_hint">Hata raporuna ekleyin</string> + <string name="drawer_share_logs">Günlükleri paylaş</string> + <string name="drawer_background">Arka plan</string> + <string name="drawer_stop">Durdur</string> + <string name="drawer_stop_title">Oturum durdurulsun mu?</string> + <string name="drawer_running_now">Şu anda çalışan</string> + <string name="drawer_no_game">Proton üzerinde çalışan oyun yok</string> + <string name="drawer_selected_profile">Oyun profili</string> + <string name="drawer_selected_profile_none">Profil ayrıntıları henüz kullanılamıyor.</string> + <string name="drawer_profile_refresh_failed_initial">Oyun profilleri yüklenemedi. Otomatik olarak yeniden deneniyor.</string> + <string name="drawer_profile_refresh_failed_stale">Oyun profilleri yenilenemedi. Son mevcut veriler gösteriliyor; otomatik olarak yeniden deneniyor.</string> + <string name="drawer_profile_inspect_hint">Bir sonraki başlatmada kullanılacak kayıtlı ayarları görüntüleyin ve düzenleyin.</string> + <string name="drawer_profile_default">Varsayılan profil kullanılıyor</string> + <string name="drawer_profile_custom">Özel profil</string> + <string name="drawer_component_inherited">Varsayılan profildeki %1$s kullanılıyor.</string> + <string name="drawer_game_components_note">Değişiklikler %1$s profiline kaydedilir ve oyun bir sonraki başlatıldığında uygulanır.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d oyun geçersiz kılması</item> + <item quantity="other">%1$d oyun geçersiz kılması</item> + </plurals> + <string name="drawer_no_proton">Henüz Proton yüklü değil.</string> + <string name="drawer_game_running">oyun çalışıyor</string> + <string name="drawer_reapplied">başlatmada yeniden uygulandı %1$s</string> + <string name="drawer_original">Orijinal</string> + <string name="drawer_components_note">İçe aktarma ve silme işlemleri Bileşenler sayfasında yapılır.</string> + <string name="setup_check_done">Tamamlandı</string> + <string name="setup_check_warn">Dikkat gerekiyor</string> + <string name="setup_check_busy">İşleniyor</string> + <string name="setup_limit_adb_failed">Kablosuz hata ayıklama IP adresini ve bağlantı noktasını kontrol edin ya da Android bu cihazı kaldırdıysa yeniden eşleyin.</string> + <string name="setup_install">Yükle</string> + <string name="setup_manage">Yönet</string> + <string name="setup_tab_overview">Genel bakış</string> + <string name="setup_tab_controller">Kumanda</string> + <string name="setup_tab_session">Oturum</string> + <string name="setup_tab_launcher">Başlatıcı</string> + <string name="setup_title">Kurulum</string> + <string name="setup_all_set">● Her şey hazır</string> + <string name="setup_n_ready">%1$d / %2$d hazır</string> + <string name="setup_system_check">Sistem kontrolü</string> + <string name="setup_gpu_supported">Cihaz destekleniyor</string> + <string name="setup_gpu_untested">Test edilmemiş GPU</string> + <string name="setup_gpu_unsupported">GPU desteklenmiyor</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">Steam için Adreno sürücüsü gerekir; %1$s siyah ekran gösterebilir</string> + <string name="setup_runtime">Linux çalışma zamanı</string> + <string name="setup_runtime_progress">%1$s · %%%2$d</string> + <string name="setup_runtime_missing">Yüklü değil · yaklaşık 3 GB, ilk kez Oyna\'ya basınca yüklenir</string> + <string name="setup_installed">Yüklü</string> + <string name="setup_runtime_update">%1$s · güncelleme var</string> + <string name="setup_runtime_current">%1$s · güncel</string> + <string name="setup_limit">Alt süreç sınırı</string> + <string name="setup_limit_on">Açık · Android Steam\'i kapatabilir. Kapatmak bir dakika sürer.</string> + <string name="setup_limit_unset">Ayarlanmadı · Android Steam\'i kapatabilir. Kapatmak bir dakika sürer.</string> + <string name="setup_limit_unknown">Kontrol edilemedi · kapatmak bir dakika sürer.</string> + <string name="setup_fix_it">Düzelt</string> + <string name="setup_details">Ayrıntılar</string> + <string name="setup_dev_options">Geliştirici seçenekleri</string> + <string name="setup_use_wireless">Kablosuz hata ayıklamayı kullan</string> + <string name="setup_check_again">Tekrar kontrol et</string> + <string name="setup_limit_turn_on">Sınırı aç</string> + <string name="setup_account">Steam hesabı</string> + <string name="setup_signed_in_offline">%1$s olarak oturum açıldı · çevrimdışı mod</string> + <string name="setup_signed_in">%1$s olarak oturum açıldı</string> + <string name="setup_sign_in">Oyna\'ya basın ve Steam\'de oturum açın</string> + <string name="setup_tools">Araçlar</string> + <string name="setup_controller">Kumanda</string> + <string name="setup_controller_unavailable">Kumanda ayarları kullanılamıyor.</string> + <string name="setup_session">Oturum</string> + <string name="setup_logs">Oturum günlükleri</string> + <string name="setup_logs_hint">Her oturumdan sonra kaydedilir</string> + <string name="setup_agent_commands">Hata ayıklama araçları</string> + <string name="setup_latest_logs">Son oturum günlükleri</string> + <string name="setup_saved_logs">Kayıtlı oturum günlükleri</string> + <string name="setup_saved_logs_hint">En yeni %1$d tanesini saklar</string> + <string name="setup_clear_logs">Günlükleri temizle</string> + <string name="setup_offline">Çevrimdışı mod</string> + <string name="setup_sign_in_first">Önce Steam\'de oturum açın</string> + <string name="setup_launcher">Başlatıcı</string> + <string name="setup_language">Dil</string> + <string name="setup_language_hint">Steam de bir sonraki başlatılışından itibaren bunu kullanır.</string> + <string name="setup_language_system">Sistem varsayılanı (%1$s)</string> + <string name="setup_language_system_short">Sistem varsayılanı</string> + <string name="setup_app_scale">Uygulama ölçeği</string> + <string name="setup_app_scale_hint">DroidDeck metinleri ve kontrolleri</string> + <string name="setup_app_scale_default">%%%1$d (Varsayılan)</string> + <string name="setup_theme">Tema</string> + <string name="setup_theme_hint">Başlatıcı görünümü</string> + <string name="setup_home">Ana ekran olarak kullan</string> + <string name="setup_home_on">DroidDeck ana ekran uygulaması olabilir</string> + <string name="setup_home_off">DroidDeck hiçbir zaman ana ekran uygulaması olarak sunulmaz</string> + <string name="setup_orientation">Yön</string> + <string name="setup_orientation_auto">Otomatik yatay</string> + <string name="setup_orientation_landscape">Yatay</string> + <string name="setup_orientation_reverse">Ters yatay</string> + <string name="setup_fullscreen">Tam ekran</string> + <string name="setup_fullscreen_on">Android durum ve gezinme çubuklarını gizler</string> + <string name="setup_fullscreen_off">Android durum ve gezinme çubuklarını gösterir</string> + <string name="setup_default_home">Varsayılan ana ekran uygulaması</string> + <string name="setup_choose_home">Ana ekran uygulaması seçin</string> + <string name="setup_choose">Seç</string> + <string name="setup_linux_apps">Linux uygulamaları (beta)</string> + <string name="setup_tool_files">Dosyalar</string> + <string name="setup_tool_files_hint">Dosyalara göz atın ve yönetin</string> + <string name="setup_tool_protons">Proton sürümleri</string> + <string name="setup_tool_protons_hint">ARM64 Proton derlemelerini yükleyin</string> + <string name="setup_tool_performance">Performans</string> + <string name="setup_tool_performance_hint">CPU çekirdeği ataması</string> + <string name="setup_tool_roms">ROM klasörü</string> + <string name="setup_tool_roms_hint">Emülatör oyunlarının saklandığı yer</string> + <string name="setup_card_components">Bileşenler</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Kontroller</string> + <string name="setup_card_controls_hint">Düğme eşleme</string> + <string name="store_tab_discover">Keşfet</string> + <string name="store_tab_search">Ara</string> + <string name="store_tab_installed">Yüklü</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Mağaza</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak hazır</string> + <string name="store_needs_runtime">Önce Kurulum sayfasından Linux çalışma zamanını yükleyin.</string> + <string name="store_setup_title">Flatpak\'i kur</string> + <string name="store_setup_text">Flatpak\'i ekler ve Flathub\'a bağlar (yaklaşık 20 MB). İlk uygulama kendi çalışma zamanını da indirir.</string> + <string name="store_setting_up">Kuruluyor…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Güncellemeler</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %%%3$d</string> + <string name="store_starting">Başlatılıyor…</string> + <string name="store_categories">Kategoriler</string> + <string name="store_unreachable">Flathub\'a ulaşılamadı. Bağlantınızı kontrol edip tekrar deneyin.</string> + <string name="store_try_again">Tekrar dene</string> + <string name="store_search_label">Flathub\'da ara</string> + <string name="store_search_placeholder">Firefox, oyunlar, emülatörler…</string> + <string name="store_search">Ara</string> + <string name="store_all">Tümü</string> + <string name="store_searching">Aranıyor…</string> + <string name="store_search_hint">Ada göre arayın veya bir kategori seçin. Yalnızca ARM64 uygulamaları listelenir.</string> + <string name="store_no_results">Flathub\'da bununla eşleşen ARM64 uygulaması yok.</string> + <string name="store_results">Sonuçlar</string> + <string name="store_check_updates">Güncellemeleri kontrol et</string> + <string name="store_update_all">Tümünü güncelle (%1$d)</string> + <string name="store_all_current">● Her şey güncel</string> + <string name="store_no_apps">Henüz uygulama yok. Keşfet veya Ara bölümünden bulabilirsiniz.</string> + <string name="store_setup_first_note">Uygulama yüklemek için Flatpak\'i kurun.</string> + <string name="store_update_available_prefix">Güncelleme var · %1$s</string> + <string name="store_open">Aç</string> + <string name="store_details">Ayrıntılar</string> + <string name="store_remove">Kaldır</string> + <string name="store_press_again">Kaldırmak için tekrar basın</string> + <string name="store_installing">Yükleniyor…</string> + <string name="store_install">Yükle</string> + <string name="store_setup_first">Önce Flatpak\'i kurun</string> + <string name="store_download_size">İndirme: %1$s</string> + <string name="store_installed_chip">● Yüklü</string> + <string name="store_details_failed">Bu uygulamanın Flathub sayfası yüklenemedi.</string> + <string name="store_loading">Flathub\'dan yükleniyor…</string> + <string name="store_no_arm">Flathub\'da ARM64 derlemesi yok, bu yüzden burada yüklenemez.</string> + <string name="store_screenshots">Ekran görüntüleri</string> + <string name="store_about">Hakkında</string> + <string name="store_app_id">Uygulama kimliği</string> + <string name="store_license">Lisans</string> + <string name="store_runtime">Çalışma zamanı</string> + <string name="store_installed_size">Yüklü boyut</string> + <string name="store_installed_size_value">%1$s (ayrıca paylaşılan çalışma ortamı)</string> + <string name="store_website">Web sitesi</string> + <string name="store_verified">Doğrulandı</string> + <string name="upd_title">Güncellemeler</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Kontrol edildi %1$s</string> + <string name="upd_not_checked">Henüz kontrol edilmedi</string> + <string name="upd_perm_title">DroidDeck kendini güncellesin</string> + <string name="upd_perm_text">Sonraki ekranda “Bu kaynağa izin ver” seçeneğini açın, sonra buraya dönün. Güncelleme kendiliğinden sürer.</string> + <string name="upd_open_settings">Ayarları aç</string> + <string name="upd_not_now">Şimdi değil</string> + <string name="upd_signed_status">Farklı imzalanmış</string> + <string name="upd_signed_headline">Yerinde güncellenemez</string> + <string name="upd_signed_detail">Bu kopya DroidDeck\'in yayın anahtarıyla imzalanmamış, bu yüzden güncellemeler üzerine yüklenemez. Geçmek için önce kaldırın.</string> + <string name="upd_available">Güncelleme var</string> + <string name="upd_blocked_status">Yüklenemiyor</string> + <string name="upd_switch">Geçişe hazır</string> + <string name="upd_ahead_status">Kararlı\'nın önünde</string> + <string name="upd_ahead_headline">Kararlı\'nın önündesiniz</string> + <string name="upd_ahead_detail">Bu derleme en yeni Kararlı sürümden daha yeni. Bir sonraki sürümüyle Kararlı\'ya geçeceksiniz.</string> + <string name="upd_gone_status">Test sona erdi</string> + <string name="upd_gone_headline">Bu test sona erdi</string> + <string name="upd_gone_detail">Düzeltmesi birleştirildi veya bırakıldı. En yeni düzeltmeler için Önizleme\'yi takip edin.</string> + <string name="upd_current_status">Güncel</string> + <string name="upd_current_headline">En yeni %1$s\'e sahipsiniz</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Güncelle%1$s</string> + <string name="upd_install_button">Yükle%1$s</string> + <string name="upd_install_anyway">Yine de Kararlı %1$s\'i yükle</string> + <string name="upd_follow_preview">Önizleme\'yi takip et</string> + <string name="upd_stop_session">Güncellemek için çalışan oturumu durdurun.</string> + <string name="upd_less">Daha az</string> + <string name="upd_more">Daha fazla</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Yeni Önizleme derlemesi</string> + <string name="upd_new_test">PR #%1$d test derlemesi</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">GÜNCELLEME KANALI</string> + <string name="upd_stable">Kararlı</string> + <string name="upd_stable_hint">Test edilmiş sürümler, çoğu kişi için</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Önizleme</string> + <string name="upd_preview_hint">En yeni düzeltmeler, Kararlı\'dan önce</string> + <string name="upd_preview_current">Güncel</string> + <string name="upd_tests">Test derlemeleri</string> + <string name="upd_tests_hint">Yayınlanmadan önce bir düzeltmeyi deneyin</string> + <string name="upd_tests_none">Şu anda yok</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Bu DroidDeck kopyası için derlenmedi</string> + <string name="upd_updated">Güncellendi %1$s</string> + <string name="upd_test_name">PR #%1$d testi</string> + <string name="upd_while_ago">bir süre önce</string> + <string name="upd_just_now">az önce</string> + <plurals name="upd_tests_count"> + <item quantity="one">%1$d denenecek</item> + <item quantity="other">%1$d denenecek</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="one">Derlemenizden sonraki değişiklikler · %1$d derleme geride</item> + <item quantity="other">Derlemenizden sonraki değişiklikler · %1$d derleme geride</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="one">Son değişiklikler · %1$d derleme</item> + <item quantity="other">Son değişiklikler · %1$d derleme</item> + </plurals> + <string name="gate_title">Değiştirilecek bir Android ayarı</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Android, çok sayıda arka plan süreci başlatan uygulamaları durdurur; Steam da onlarca süreç başlatır. Bu değişmeden oyunlar hata vermeden kapanır.</string> + <string name="gate_fix_for_me">Benim için düzelt</string> + <string name="gate_toggle_steps">“Alt süreç kısıtlamalarını devre dışı bırak” seçeneğini açın, sonra buraya dönün. Geliştirici seçeneklerini açık bırakın, yoksa anahtar yeniden kapanır.</string> + <string name="gate_open_dev">Geliştirici seçeneklerini aç</string> + <string name="gate_no_toggle">Bu Android sürümünde bunun için bir anahtar yok, bu yüzden DroidDeck bunu Kablosuz hata ayıklama üzerinden değiştirir. Yaklaşık bir dakika sürer, bilgisayar gerekmez.</string> + <string name="gate_needs_wifi">Kablosuz hata ayıklama Wi-Fi gerektirir. Herhangi bir ağ işe yarar; internet gerekmez.</string> + <string name="gate_needs_notifications">Eşleştirme kodu bir bildirime gelir. DroidDeck için bildirimlere izin verin veya Diğer yollar\'ı kullanın.</string> + <string name="gate_allow_notifications">Bildirimlere izin ver</string> + <string name="gate_step1">Ayarlar → Geliştirici seçenekleri → Kablosuz hata ayıklama\'yı açın</string> + <string name="gate_step2">“Eşleştirme koduyla cihaz eşle” seçeneğine dokunun</string> + <string name="gate_step3">Kodu DroidDeck bildirimine yazın</string> + <string name="gate_waiting">Eşleştirme açılır penceresi bekleniyor…</string> + <string name="gate_code_needed">Açılır pencere bulundu. Kodunu bildirime girin.</string> + <string name="gate_status">Durum</string> + <string name="gate_polling">Her 2 saniyede bir kontrol ediliyor. Kapandığında bu sayfa kapanır.</string> + <string name="gate_other_ways">Diğer yollar</string> + <string name="gate_other_toggle">Kablosuz hata ayıklama, elle adres girme, bilgisayardan ADB</string> + <string name="gate_other_no_toggle">Elle adres girme, bilgisayardan ADB, Geliştirici seçenekleri</string> + <string name="gate_use_wireless_instead">Bunun yerine Kablosuz hata ayıklamayı kullan</string> + <string name="gate_some_roms">Bazı ROM\'lar bu anahtarı yine de ekler. Geliştirici seçenekleri\'nde “alt süreç” ifadesini arayın.</string> + <string name="gate_manual_hint">Bildirim işe yaramazsa açılır penceredeki adresi ve kodu kendiniz girin.</string> + <string name="gate_enter_manually">Adresi elle girin</string> + <string name="gate_from_computer">ADB\'li bir bilgisayardan:</string> + <string name="gate_copy_command">Komutu kopyala</string> + <string name="gate_choose_display">Android Ayarları\'nın hangi ekranda açılacağını seçin.</string> + <string name="gate_main_screen">Ana ekran</string> + <string name="wadb_finding_port">Eşleşti. Kablosuz hata ayıklama bağlantı noktası bulunuyor…</string> + <string name="wadb_enter_port">Eşleşti. Bağlantı noktasını Ayarlar\'dan girin.</string> + <string name="wadb_applying">Cihaz bulundu. Ayar uygulanıyor…</string> + <string name="wadb_apply_failed">Otomatik olarak uygulanamadı. Aşağıdaki adresi kontrol edip yeniden deneyin. %1$s</string> + <string name="wadb_title_wireless">Kablosuz hata ayıklama</string> + <string name="wadb_title_done">Alt süreç sınırı güncellendi</string> + <string name="wadb_title">Alt süreç sınırını değiştir</string> + <string name="wadb_lede_compact0">Seçenek yoksa Kablosuz hata ayıklama üzerinden eşleyin.</string> + <string name="wadb_lede_compact1">Eşleşti. IP adresini ve bağlantı noktasını Kablosuz hata ayıklama\'da kontrol edin.</string> + <string name="wadb_lede_done">Alt süreç sınırı güncellendi.</string> + <string name="wadb_lede0">Geliştirici seçenekleri\'nde alt süreç ayarı yoksa 2. adımı kullanın. Geliştirici seçenekleri → Kablosuz hata ayıklama → Eşleştirme koduyla cihaz eşle\'yi açın.</string> + <string name="wadb_lede1">Eşleşti. Aşağıdaki adresi kullanın veya Kablosuz hata ayıklama → IP adresi ve bağlantı noktası\'ndan güncelleyin.</string> + <string name="wadb_lede_done_long">Alt süreç sınırı Kablosuz hata ayıklama üzerinden güncellendi.</string> + <string name="wadb_confirmed_on">Android sınırın açık olduğunu onayladı.</string> + <string name="wadb_confirmed_off">Android sınırın kapalı olduğunu onayladı.</string> + <string name="wadb_pair_group">2 · Kablosuz hata ayıklamayı eşle</string> + <string name="wadb_pair_address">Eşleştirme açılır penceresinin IP adresi ve bağlantı noktası</string> + <string name="wadb_pin">Eşleştirme kodu (PIN)</string> + <string name="wadb_pin_placeholder">6 hane</string> + <string name="wadb_pair">Eşle</string> + <string name="wadb_apply_group">2 · Kablosuz hata ayıklama üzerinden uygula</string> + <string name="wadb_connect_address">Bağlantı IP adresi ve bağlantı noktası</string> + <string name="wadb_pair_again">Yeniden eşle</string> + <string name="wadb_turn_off">Sınırı kapat</string> + <string name="wadb_other_ways">Değiştirmenin diğer yolları</string> + <string name="wadb_dev_try_first">1 · Geliştirici seçenekleri (önce bunu deneyin)</string> + <string name="wadb_restrict_on">“Alt süreçleri kısıtla” seçeneğini açık olarak ayarlayın.</string> + <string name="wadb_restrict_off">“Alt süreçleri kısıtla” seçeneğini kapalı olarak ayarlayın.</string> + <string name="wadb_computer_last">3 · Bilgisayardan ADB (son yöntem)</string> + <string name="wadb_only_if">Yalnızca Kablosuz hata ayıklama kullanılamıyorsa.</string> + <string name="wadb_computer_group">3 · Bilgisayardan ADB (son yöntem)</string> + <string name="ctrl_osc_hint">Dokunmatik kontrollerin bir oturumda ne zaman görüneceği</string> + <string name="ctrl_color">Renk</string> + <string name="ctrl_color_hint">Ekrandaki düğmelerin rengi</string> + <string name="ctrl_custom">Özel</string> + <string name="ctrl_opacity">Opaklık</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">Düğme boyutu</string> + <string name="ctrl_size_hint" formatted="false">%100 standart boyuttur</string> + <string name="ctrl_stick_click">Çubuk tıklaması</string> + <string name="ctrl_stick_click_hint">L3 veya R3 için bir çubuğa çift dokunup basılı tutun</string> + <string name="ctrl_adaptive">Uyarlanabilir çubuklar</string> + <string name="ctrl_rumble">Oyun titreşimi</string> + <string name="ctrl_steam_button">Steam düğmesi</string> + <string name="ctrl_qam_button">QAM düğmesi</string> + <string name="ctrl_keyboard_button">Klavye düğmesi</string> + <string name="ctrl_select_steam">Steam menüsünü açmak için Select\'e basın</string> + <string name="ctrl_select_steam_hint">Görünüm/Select yerine.</string> + <string name="setup_animations">Animasyonlar</string> + <string name="ctrl_adaptive_hint">Çubuklar dokunduğunuz yerde görünür, bıraktığınızda gizlenir</string> + <string name="ctrl_layout">Düzen</string> + <string name="ctrl_layout_custom">Özel konumlar kaydedildi</string> + <string name="ctrl_layout_default">Bu ekrana uyarlandı</string> + <string name="ctrl_edit">Düzenle</string> + <string name="ctrl_reset">Sıfırla</string> + <string name="ctrl_mapping">Düğme eşlemesi</string> + <string name="ctrl_mapping_default">Her düğme kendi girdisini gönderir</string> + <string name="ctrl_mapping_count">%2$d düğmeden %1$d\'i yeniden eşlendi</string> + <string name="ctrl_configure">Yapılandır</string> + <string name="ctrl_reset_all">Kumandayı sıfırla</string> + <string name="ctrl_reset_all_hint">Tüm kumanda ayarlarını ve düzeni geri yükler</string> + <string name="ctrl_mapping_lede">Ekrandaki her düğmenin oyuna gönderdiği girdi. Gizli, düğmeyi kaldırır.</string> + <string name="ctrl_onscreen_buttons">Ekrandaki düğmeler</string> + <string name="ctrl_defaults">Varsayılanlar</string> + <string name="ctrl_reset_mapping">Eşlemeyi sıfırla</string> + <string name="content_android_apps">Android uygulamaları</string> + <string name="content_apps_count">%1$d uygulama</string> + <string name="content_no_android_apps">Başlatılabilir Android uygulaması bulunamadı.</string> + <string name="content_desktop_installed">● Masaüstü yüklü</string> + <string name="content_desktop_first_open">İlk açılışta yüklenir</string> + <string name="content_available">Yüklenebilir</string> + <string name="content_not_installed">Yüklü değil.</string> + <string name="content_not_installed_suffix">%1$s · yüklü değil</string> + <string name="content_open_named">%1$s\'i aç</string> + <string name="content_games">Oyunlar</string> + <string name="content_choose_roms">Bir ROM\'lar klasörü seçin.</string> + <string name="content_retroarch_roms">RetroArch\'ta /root/ROMs\'a göz atın.</string> + <string name="content_add_roms">%1$s oyunlarını ROMs/%2$s\'e ekleyin.</string> + <string name="content_install_named">%1$s\'i yükle</string> + <string name="content_stop_to_install">Yüklemek için oturumu durdurun</string> + <string name="content_runtime_required">Çalışma ortamı gerekli</string> + <string name="content_loading_details">Yükleme ayrıntıları yükleniyor…</string> + <string name="content_details_unavailable">Yükleme ayrıntıları kullanılamıyor. Daha sonra tekrar deneyin.</string> + <string name="content_needs_runtime">Önce Kurulum\'dan Linux çalışma ortamını yükleyin.</string> + <string name="content_stop_first">Masaüstü uygulamalarını yüklemek için çalışan oturumu durdurun.</string> + <string name="content_install_into">%1$s\'i Linux masaüstüne yükler.</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">Gizlemek onu Masaüstü\'nden kaldırır. Dosyaları Linux çalışma ortamında kalır.</string> + <string name="content_rom_gone">Bu oyun artık ROM\'lar klasöründe değil.</string> + <string name="content_desktop_system">Masaüstü · %1$s</string> + <string name="content_launch_in">%1$s\'de başlat</string> + <string name="content_also_in">%1$s\'de de var</string> + <string name="content_page_gone">Bu sayfa artık yok. Soldan bir bölüm seçin.</string> + <string name="content_open_desktop">Masaüstünü aç</string> + <string name="content_install_open_desktop">Masaüstünü yükle ve aç</string> + <string name="desktop_remove_title">Masaüstünü kaldır</string> + <string name="desktop_remove_text">Masaüstünü ve birlikte gelen uygulamaları kaldırır. Oyunlar, eklenen uygulamalar, ayarlar ve paylaşılan kitaplıklar saklanır.</string> + <string name="desktop_removing">Masaüstü kaldırılıyor</string> + <string name="desktop_removed">Masaüstü kaldırıldı</string> + <string name="desktop_cleaning_legacy">Eski LXQt dosyaları kaldırılıyor</string> + <string name="desktop_removal_unknown">Bu masaüstü sürümünü kaldırmadan önce DroidDeck\'i güncelleyin.</string> + <string name="content_linux_desktop">Linux masaüstü</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox ve emülatörleriniz</string> + <string name="games_empty">Steam\'de yüklediğiniz oyunlar oturumdan sonra burada görünür.</string> + <string name="games_play_steam">Steam\'de oyna</string> + <string name="games_launch_settings">Başlatma ayarları</string> + <string name="games_launch">Başlat</string> + <string name="games_files">Oyun dosyaları</string> + <string name="games_prefix">Proton öneki</string> + <string name="games_saves_looking">Kayıtlar aranıyor…</string> + <string name="games_saves_none">Ada göre bir kayıt klasörü bulunamadı. Dışa aktarma tüm kullanıcı klasörünü alır.</string> + <string name="games_saves_folders">%1$d kayıt klasörü</string> + <string name="games_saves_summary">%1$s · %2$d dosya · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">Kayıtları yönet</string> + <string name="games_saves">Oyun kayıtları</string> + <string name="games_import_saves">Kayıtları içe aktar…</string> + <string name="games_export_gamehub">GameHub zip\'i olarak dışa aktar…</string> + <string name="games_export_winlator">Winlator zip\'i olarak dışa aktar…</string> + <string name="games_open_save_folder">Kayıt klasörünü aç</string> + <string name="games_never_played">Hiç oynanmadı</string> + <string name="games_runtime_busy_percent">Çalışma ortamı meşgul · %1$d%%</string> + <string name="games_runtime_busy">Çalışma ortamı meşgul</string> + <string name="games_runtime_first_play">Çalışma ortamı ilk Oyna\'da yüklenir</string> + <string name="games_runtime_update">Çalışma ortamı güncellemesi var</string> + <string name="games_runtime_ready">● Çalışma ortamı hazır</string> + <string name="games_last_played">Son oynanma %1$s</string> + <string name="games_internal">Dahili depolama</string> + <string name="games_added">Eklenen oyun</string> + <string name="games_custom">Özel oyun</string> + <string name="games_store_storage">Oyun depolama alanı</string> + <string name="gpu_auto_hint">Bu GPU için en iyi çift, güncel tutulur</string> + <string name="gpu_manual_hint">Çifti kendiniz seçin</string> + <string name="gpu_checking">Sürücüler kontrol ediliyor…</string> + <string name="gpu_none_listed">Henüz listelenen sürücü yok. Aramak için yenileyin.</string> + <string name="gpu_hide_other">Diğer GPU\'lara ait sürücüleri gizle</string> + <string name="gpu_show_other">Diğer GPU\'lara ait sürücüleri göster</string> + <string name="gpu_advanced">Gelişmiş</string> + <string name="gpu_set_with_display">Ekran sürücüsüyle birlikte ayarlanır: %1$s ikisini de taşır.</string> + <string name="gpu_set_with_runtime">Çalışma ortamı sürücüsüyle birlikte ayarlanır: %1$s ikisini de taşır.</string> + <string name="gpu_runtime_hint">Oyunlar ve masaüstü tarafından kullanılır.</string> + <string name="gpu_runtime_default">Çalışma ortamı varsayılanı</string> + <string name="gpu_display_hint">Kareleri ekrana çizer. DroidDeck yeniden başlatıldıktan sonra uygulanır.</string> + <string name="gpu_auto_by_gpu">Otomatik - GPU\'ya göre seçilir</string> + <string name="gpu_manage">İndir, içe aktar veya sil…</string> + <string name="gpu_in_use_caps">KULLANIMDA</string> + <string name="gpu_builtin">DroidDeck\'e yerleşik sürücüler</string> + <string name="gpu_matched_pair">Çalışma ortamı ve ekran sürücüleri eşleşen bir çift olarak birlikte ayarlanır.</string> + <string name="gpu_recommended_for">Bu GPU için önerilir: %1$s.</string> + <string name="gpu_refresh_hint">Bu GPU için sürücü aramak için yenileyin.</string> + <string name="gpu_recommended_caps">ÖNERİLİR</string> + <string name="gpu_unavailable">Kullanılamıyor</string> + <string name="load_runtime">Linux çalışma ortamı</string> + <string name="load_runtime_detail">Linux çalışma ortamı hazırlanıyor</string> + <string name="load_session">Oturum</string> + <string name="load_session_detail">Ekran ve ses başlatılıyor</string> + <string name="load_steam">Steam istemcisi</string> + <string name="load_steam_detail">Steam ve uyumluluk araçları kontrol ediliyor</string> + <string name="load_opening_steam">Steam açılıyor</string> + <string name="load_opening_steam_detail">Steam\'in ilk karesi bekleniyor</string> + <string name="load_desktop">Masaüstü</string> + <string name="load_desktop_detail">Masaüstü hazırlanıyor</string> + <string name="load_opening">Açılıyor</string> + <string name="load_opening_detail">İlk kare bekleniyor</string> + <string name="load_starting_steam">Steam başlatılıyor</string> + <string name="load_ended">Oturum sona erdi</string> + <string name="load_step">Adım %1$d / %2$d</string> + <string name="load_steam_paused">Steam duraklatıldı</string> + <string name="deck_tab_grips">Tutamaklar</string> + <string name="deck_tab_left">Sol taraf</string> + <string name="deck_tab_right">Sağ taraf</string> + <string name="deck_tab_both">Her ikisi</string> + <string name="deck_tab_gamepad">Oyun kolu</string> + <string name="deck_surface_stick">Çubuk</string> + <string name="deck_surface_trackpad">Dokunmatik yüzey</string> + <string name="deck_surface_switch">Çubuk ile dokunmatik yüzey arasında geçiş yap</string> + <string name="deck_more_controls">Diğer Deck kontrolleri</string> + <string name="deck_panels">PANELLER</string> + <string name="deck_close">İkinci ekran kontrollerini kapat</string> + <string name="deck_open_steam_menu">Steam menüsünü aç</string> + <string name="deck_left_upper">Sol · üst</string> + <string name="deck_left_lower">Sol · alt</string> + <string name="deck_right_upper">Sağ · üst</string> + <string name="deck_right_lower">Sağ · alt</string> + <string name="deck_click">Tıkla</string> + <string name="deck_right_trackpad">Sağ dokunmatik yüzey</string> + <string name="deck_left_trackpad">Sol dokunmatik yüzey</string> + <string name="second_trackpad">Dokunmatik yüzey</string> + <string name="second_show_keyboard">Klavyeyi göster</string> + <string name="second_hide_keyboard">Klavyeyi gizle</string> + <string name="second_terminal">Linux terminali</string> + <string name="second_terminal_not_ready">Linux oturumu henüz terminal için hazır değil.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Oyunlar</string> + <string name="rail_desktop">Masaüstü</string> + <string name="rail_store">Mağaza</string> + <string name="rail_components">Bileşenler</string> + <string name="rail_apps">Uygulamalar</string> + <string name="rail_setup">Kurulum</string> + <string name="rail_updates">Güncellemeler</string> + <string name="rail_percent">%%%1$d</string> + <string name="rail_resume_named">Devam et: %1$s</string> + <string name="roms_text">Her oturumda /root/ROMs olarak görünür. Dahili depolama /root/Storage konumundadır.</string> + <string name="roms_none">Klasör seçilmedi</string> + <string name="roms_applies">Sonraki oturumda uygulanır. SD kartlar da çalışır.</string> + <string name="roms_choose">Klasör seç</string> + <string name="roms_change">Klasörü değiştir</string> + <string name="proton_lede">Bir ARM64 Proton derlemesi yükleyin, ardından Steam → Özellikler → Uyumluluk bölümünde oyun bazında seçin.</string> + <string name="proton_stop_first">Uyumluluk aracı yüklemek veya kaldırmak için çalışan oturumu durdurun.</string> + <string name="proton_available">Mevcut derlemeler</string> + <string name="proton_installed">Yüklü: %1$s</string> + <string name="proton_queued">Önceki bir istekten bekliyor</string> + <string name="proton_not_installed">Yüklü değil</string> + <string name="proton_update_available">Güncelleme mevcut → %1$s</string> + <string name="proton_remove_older">Eski %1$s sürümlerini kaldır</string> + <string name="proton_remove_older_body">%1$s kaldırılır. Bu sürümlere ayarlı oyunlar başka bir Proton sürümüne geçirilmelidir.</string> + <string name="proton_remove_body">Bu sürüme ayarlı oyunlar başka bir Proton sürümüne geçirilmelidir.</string> + <string name="proton_install_now">Şimdi yükle</string> + <string name="proton_cancel_queued">Sıradakini iptal et</string> + <string name="pckb_android">Android klavyesi</string> + <string name="pckb_hide">Gizle ✕</string> + <string name="main_launch_named">Başlat: %1$s</string> + <string name="main_choose_screen">Bir ekran seçin. İkincil: %1$s.</string> + <string name="main_secondary_gone">İkincil ekran artık kullanılamıyor.</string> + <string name="main_primary">Birincil ekran</string> + <string name="main_secondary">İkincil ekran</string> + <string name="widgets_prev_tab">Önceki sekme</string> + <string name="widgets_next_tab">Sonraki sekme</string> + <string name="widgets_on">Açık</string> + <string name="widgets_all_n">Tümü (%1$d)</string> + <string name="widgets_none">Hiçbiri</string> + <string name="widgets_n_of_m">%1$d / %2$d</string> + <string name="widgets_off">Kapalı</string> + <string name="screen_bottom">Alt ekran</string> + <string name="game_env_applies">Oyun bir sonraki başlatıldığında uygulanır. Oyunun profili ortak profili geçersiz kılar.</string> + <string name="game_env_other">Uygulama kimliğiyle başka bir oyun…</string> + <string name="game_env_done">Bitti</string> + <string name="game_env_default">Varsayılan</string> + <string name="game_env_off">Kapalı</string> + <string name="game_env_change">Değiştir…</string> + <string name="game_env_change_title">Değişkeni değiştir</string> + <string name="game_env_restore_default">Varsayılana dön</string> + <string name="game_env_eyebrow">Proton oyunları</string> + <string name="pip_title">Pencere içinde pencere</string> + <string name="pip_auto">Otomatik olarak PiP moduna geç</string> + <string name="unfolded_controls">Açılmış ekrandaki kontroller</string> + <string name="unfolded_controls_hint">Cihaz açıkken oyun, Steam Deck dokunmatik yüzeylerinin ve arka tutamaklarının üstünde durur. Katlıyken oyun tam ekran olur.</string> + <string name="pip_suspended">Askıya alındı</string> + <string name="pip_not_ready">Pencere içinde pencere, oturum hazır olduğunda kullanılabilir.</string> + <string name="pip_permission">Android ayarlarında DroidDeck için pencere içinde pencereye izin verin.</string> + <string name="pip_unavailable">Android, pencere içinde pencereyi açamadı.</string> + <string name="game_shortcut">Kısayol</string> + <string name="game_shortcut_add">Ana ekrana ekle</string> + <string name="game_shortcut_choose">Oyun seç</string> + <string name="game_shortcut_scanning">Yüklü oyunlar taranıyor…</string> + <string name="game_link_copy">Başlatma bağlantısını kopyala</string> + <string name="game_link_copied">Başlatma bağlantısı kopyalandı</string> + <string name="game_shortcut_unsupported">Bu başlatıcı kısayol ekleyemiyor. Bunun yerine “Başlatma bağlantısını kopyala” seçeneğini kullanın.</string> + <string name="game_link_invalid">Geçersiz oyun başlatma bağlantısı.</string> + <string name="game_link_missing">Bu oyun artık DroidDeck kütüphanenizde değil.</string> + <string name="game_link_session_busy">Bir Steam oyunu başlatmadan önce mevcut oturumu durdurun.</string> + <string name="game_link_failed">Oyun Steam\'e gönderilemedi. Yeniden başlatmayı deneyin.</string> + <string name="setup_retry_removal">Kaldırmayı yeniden dene</string> + <string name="runtime_removing">Kaldırılıyor</string> + <string name="runtime_removal_incomplete">Kaldırma tamamlanmadı</string> + <string name="game_file_export">Oyun dosyasını dışa aktar</string> + <string name="game_file_sync">Tüm oyun dosyalarını klasörle eşitle</string> + <string name="game_file_stop_sync">Oyun dosyalarını eşitlemeyi durdur</string> + <string name="game_file_folder">Ön yüz oyun dosyaları için bir klasör seçin</string> + <string name="game_file_exported">Oyun dosyaları şuraya kaydedildi: %1$s</string> + <string name="game_frontend_files">Ön yüz dosyaları</string> + + <string name="main_wait_runtime">Çalışma ortamı işleminin bitmesini bekleyin</string> + <string name="main_wait_proton_install">Uyumluluk aracı yüklemesinin bitmesini bekleyin</string> + <string name="main_wait_desktop_install">Masaüstü uygulaması yüklemesinin bitmesini bekleyin</string> + <string name="main_wait_install_game">Oyun başlatmadan önce yüklemenin bitmesini bekleyin</string> + <string name="main_saves_close_session">Önce Steam oturumunu kapatın; böylece oyun, içe aktarılan kayıtların üzerine yazamaz</string> + <string name="main_saves_importing">İçe aktarılıyor: %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="one">%2$s içinden %1$d dosya %3$s oyununa aktarıldı</item> + <item quantity="other">%2$s içinden %1$d dosya %3$s oyununa aktarıldı</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">%2$s içinden %1$d dosya %3$s oyununa aktarıldı. Eski kayıtlar Download/DroidDeck/Saves/backups klasörüne yedeklendi</item> + <item quantity="other">%2$s içinden %1$d dosya %3$s oyununa aktarıldı. Eski kayıtlar Download/DroidDeck/Saves/backups klasörüne yedeklendi</item> + </plurals> + <string name="main_saves_pick_zip">%1$s için bir kayıt zip dosyası seçin</string> + <string name="main_saves_exporting">Dışa aktarılıyor: %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="one">%1$d dosya %2$s olarak dışa aktarıldı: %3$s</item> + <item quantity="other">%1$d dosya %2$s olarak dışa aktarıldı: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">%1$s kayıtlarının konumunu seçin (%2$s)</string> + <string name="main_action_failed">İşlem başarısız (%1$s): %2$s</string> + <string name="main_no_logs_yet">Henüz oturum günlüğü yok: önce bir oturum çalıştırın.</string> + <string name="main_no_logs_to_clear">Temizlenecek oturum günlüğü yok.</string> + <plurals name="main_logs_cleared"> + <item quantity="one">%1$d oturum günlüğü klasörü temizlendi.</item> + <item quantity="other">%1$d oturum günlüğü klasörü temizlendi.</item> + </plurals> + <string name="main_wadb_pair_failed">Kablosuz hata ayıklama eşlemesi başarısız oldu</string> + <string name="main_wadb_command_failed">Kablosuz hata ayıklama komutu başarısız oldu</string> + <string name="main_wadb_unavailable">Kablosuz hata ayıklama kullanılamıyor</string> + <string name="main_adb_copied">ADB komutu kopyalandı</string> + <string name="main_pick_roms">ROM klasörünü seçin</string> + <string name="main_non_adreno_title">Adreno GPU değil</string> + <string name="main_non_adreno_text">Turnip, Adreno GPU\'larını destekler. %1$s üzerinde Steam siyah ekran gösterebilir. İndirme: %2$.0f MB.</string> + <string name="main_install_anyway">Yine de yükle</string> + <string name="main_remove_runtime_title">Linux çalışma ortamını kaldır</string> + <string name="main_remove_runtime_text">Bu işlem çalışma ortamını, içindeki Steam istemcisini ve oraya yüklenen tüm oyunları siler.</string> + <string name="main_applied">Uygulandı: %1$s</string> + <string name="android_app_open_failed_primary">%1$s birincil ekranda açılamadı</string> + <string name="android_app_open_failed_on">%1$s, %2$s üzerinde açılamadı</string> + <string name="android_app_open_failed_display">%1$s, %2$d numaralı ekranda açılamadı</string> + <string name="comp_busy_importing">İçe aktarılıyor</string> + <string name="comp_busy_swapping">Değiştiriliyor</string> + <string name="comp_busy_restoring">Geri yükleniyor</string> + <string name="comp_busy_cancelling">İptal ediliyor</string> + <string name="comp_busy_deleting">Siliniyor</string> + <string name="comp_cannot_read">dosya okunamıyor</string> + <string name="comp_imported">İçe aktarıldı: %1$s</string> + <string name="comp_swap_cancelled">Bekleyen değişim iptal edildi.</string> + <string name="comp_pick_wcp">Bir bileşen paketi seçin (-linux .wcp)</string> + <string name="comp_nightlies_unreachable">Nightlies kaynağına ulaşılamadı</string> + <string name="comp_stored">Kaydedildi: %1$s</string> + <string name="comp_download_failed">İndirme başarısız oldu: %1$s</string> + <string name="drivers_pick_linux">Bir Linux çalışma ortamı sürücüsü seçin (-Linux zip veya Android + Linux paketi)</string> + <string name="drivers_pick_android">Bir ekran sürücüsü seçin (AdrenoTools zip veya Android + Linux paketi)</string> + <string name="drivers_pick_any">Bir sürücü zip dosyası veya Android + Linux paketi seçin</string> + <string name="main_pkg_problem">%1$s: %2$s</string> + <string name="main_removing_named">%1$s kaldırılıyor…</string> + <string name="main_forgetting_named">%1$s unutuluyor…</string> + <string name="main_pick_game_storage">Oyun depolama klasörünü seçin</string> + <string name="main_pick_decky_zip">Bir Decky eklentisi ZIP dosyası seçin</string> + <string name="main_storage_not_usable">Kullanılamaz: %1$s</string> + <string name="main_running_program">Program</string> + <string name="main_removing_runtime">Linux çalışma ortamı kaldırılıyor</string> + <string name="main_settings_main_screen">Ayarlar alt ekranda açılamadı; ana ekranda açıldı.</string> + <string name="main_open_dev_options">Ayarlar\'da Geliştirici seçeneklerini açın</string> + + <string name="upd_unknown_apps_failed">Android\'in “Bilinmeyen uygulamaları yükle” ayarı açılamadı</string> + <string name="upd_couldnt_check">Kontrol edilemedi: %1$s</string> + <string name="upd_no_download">Bu sürümde DroidDeck\'in bu kopyası için indirme yok</string> + <string name="upd_stop_first">Önce çalışan oturumu durdurun, ardından güncelleyin</string> + <string name="upd_stage_downloading">İndiriliyor</string> + <string name="upd_stage_installing">Yükleniyor</string> + <string name="upd_stage_confirm">Onayınız bekleniyor</string> + <string name="upd_failed">Güncelleme başarısız oldu</string> + <string name="upd_not_offered">Android güncellemeyi yüklemeyi önermedi</string> + + <string name="decky_install_failed">Decky yüklemesi başarısız oldu: %1$s</string> + <string name="decky_plugin_starting">Eklenti içe aktarma başlatılıyor…</string> + <string name="decky_plugin_failed">Eklenti içe aktarma başarısız oldu: %1$s</string> + <string name="decky_plugin_installed">Eklenti yüklendi. Etkinleştirmek için Steam\'i yeniden başlatın.</string> + + <string name="drivers_not_checked">Henüz kontrol edilmedi - yeni sürücüleri aramak için yenile düğmesine dokunun</string> + <string name="drivers_bundle_sets_display">Android + Linux: ekran sürücüsünü de birlikte ayarlar</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: çalışma ortamı sürücüsünü de birlikte ayarlar</string> + <string name="drivers_runtime_builtin">çalışma ortamına yerleşik Turnip</string> + <string name="drivers_system_vulkan">sistem Vulkan sürücüsü: bu GPU için dahili derleme yok</string> + <string name="drivers_bundled">%1$s (dahili)</string> + <string name="drivers_manual">Manuel</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">Şu anda yalnızca bir yarısı yayımlanmış</string> + <string name="drivers_pair_size">İkisi için %1$.0f MB</string> + <string name="drivers_none_to_set">Ayarlanacak sürücü yok: %1$s</string> + <string name="drivers_checking_latest">En son sürücüler kontrol ediliyor…</string> + <string name="drivers_up_to_date_check_failed">Güncel (kontrol edilemedi: %1$s)</string> + <string name="drivers_recommended_unpublished">Önerilen sürücüler şu anda yayımlanmış değil</string> + <string name="drivers_downloading_pair">%1$s %2$s indiriliyor…</string> + <string name="drivers_updated_to">%1$s %2$s sürümüne güncellendi.</string> + <string name="drivers_updated_to_restart">%1$s %2$s sürümüne güncellendi. Ekran sürücüsü, DroidDeck yeniden başlatıldıktan sonra uygulanır.</string> + <string name="drivers_switched_to">%1$s %2$s sürümüne geçildi.</string> + <string name="drivers_switched_to_restart">%1$s %2$s sürümüne geçildi. Ekran sürücüsü, DroidDeck yeniden başlatıldıktan sonra uygulanır.</string> + <string name="drivers_using">Kullanılan: %1$s %2$s.</string> + <string name="drivers_using_restart">Kullanılan: %1$s %2$s. Ekran sürücüsü, DroidDeck yeniden başlatıldıktan sonra uygulanır.</string> + <string name="drivers_not_a_driver">Sürücü zip dosyası değil: ne AdrenoTools sürücüsü, ne -Linux Turnip ne de Android + Linux paketi</string> + <string name="drivers_import_failed">İçe aktarma başarısız oldu: %1$s</string> + <string name="drivers_imported_unnamed">İçe aktarılan sürücü</string> + <string name="drivers_using_bundle">%1$s hem çalışma ortamı hem de ekran sürücüsü olarak kullanılıyor.</string> + <string name="drivers_using_bundle_restart">%1$s hem çalışma ortamı hem de ekran sürücüsü olarak kullanılıyor. Ekran sürücüsü, DroidDeck yeniden başlatıldıktan sonra uygulanır.</string> + <string name="drivers_deleted_bundle">Silindi: %1$s (iki sürücü de)</string> + <string name="drivers_deleted">Silindi: %1$s</string> + <string name="drivers_release_status">En son: %1$s · %2$s kontrol edildi</string> + <string name="drivers_release_status_failed">En son: %1$s · %2$s ulaşılamıyor · %3$s kontrol edildi</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">%1$d dk önce</item> + <item quantity="other">%1$d dk önce</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">%1$d sa önce</item> + <item quantity="other">%1$d sa önce</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">%1$d gün önce</item> + <item quantity="other">%1$d gün önce</item> + </plurals> + <string name="drivers_checking_sources">Banners-Turnip, WinNative ve DroidDeck kontrol ediliyor…</string> + <string name="drivers_check_failed">kontrol başarısız oldu</string> + <string name="drivers_installed_pick_either">%1$s yüklendi - iki sürücüyü de ayarlamak için listelerden birinde seçin</string> + <string name="drivers_installed_pick">%1$s yüklendi - menüden seçin</string> + + <string name="session_wait_removal">Çalışma ortamının kaldırılmasının bitmesini bekleyin</string> + <string name="session_title_game">Oyun</string> + <string name="session_touch_auto_touchpad">dokunmatik yüzey</string> + <string name="session_touch_auto_direct">doğrudan</string> + <string name="session_swap_failed">Değiştirme başarısız oldu: %1$s</string> + <string name="session_profile_save_failed">Profil kaydedilemedi: %1$s</string> + <string name="session_desktop_paused">Masaüstü duraklatıldı</string> + <string name="session_named_paused">%1$s duraklatıldı</string> + <string name="session_program_paused">Program duraklatıldı</string> + <string name="session_starting_desktop">Masaüstü başlatılıyor</string> + <string name="session_starting_named">%1$s başlatılıyor</string> + <string name="session_starting_program">Program başlatılıyor</string> + <string name="session_no_logs">Bu oturum için günlük yok.</string> + <string name="session_logs_archive_failed">Oturum günlüğü arşivi oluşturulamadı.</string> + <string name="session_logs_share_failed">Oturum günlükleri paylaşılamadı.</string> + <string name="session_starting_session">Oturum başlatılıyor…</string> + <string name="session_runtime_downloading">Linux çalışma ortamı indiriliyor</string> + <string name="session_runtime_downloading_mb">Linux çalışma ortamı indiriliyor · %1$d / %2$d MB</string> + <string name="session_runtime_checking">Linux çalışma ortamı kontrol ediliyor</string> + <string name="session_runtime_unpacking">Linux çalışma ortamı ayıklanıyor</string> + <string name="session_desktop_downloading">masaüstü indiriliyor</string> + <string name="session_desktop_downloading_mb">masaüstü indiriliyor · %1$d / %2$d MB</string> + <string name="session_desktop_checking">masaüstü kontrol ediliyor</string> + <string name="session_desktop_unpacking">masaüstü ayıklanıyor</string> + <string name="session_proton_downloading">Proton Experimental (ARM64) indiriliyor</string> + <string name="session_proton_downloading_mb">Proton Experimental (ARM64) indiriliyor · %1$d / %2$d MB</string> + <string name="session_proton_checking">Proton Experimental (ARM64) kontrol ediliyor</string> + <string name="session_proton_unpacking">Proton Experimental (ARM64) ayıklanıyor</string> + <string name="session_runtime_catalog_unreachable">Çalışma ortamı kataloğuna ulaşılamadı. Bağlantıyı kontrol edip yeniden Oyna\'ya basın.</string> + <string name="session_runtime_install_failed">Linux çalışma ortamı yüklenmedi. Kurulum bölümünde çalışma ortamını kontrol edip yeniden Oyna\'ya basın.</string> + <string name="session_desktop_install_failed">Masaüstü yüklenmedi (%1$s). Bağlantıyı kontrol edip yeniden Masaüstü\'ne basın.</string> + <string name="session_ended_named">%1$s beklenmedik şekilde durdu. Günlükleri bir hata raporuyla paylaşın veya yeniden deneyin.</string> + <string name="session_ended_program">Program beklenmedik şekilde durdu. Günlükleri bir hata raporuyla paylaşın veya yeniden deneyin.</string> + <string name="session_ended_desktop">Masaüstü beklenmedik şekilde durdu. Günlükleri bir hata raporuyla paylaşın veya yeniden deneyin.</string> + <string name="session_ended_detail">Çıkış durumu %1$d · %2$s</string> + <string name="session_ended_no_log">günlük yok</string> + <plurals name="session_hint_enosys"> + <item quantity="one">Günlükte %1$d “Function not implemented” hatası görünüyor: proot\'un seccomp hızlandırması bu cihazda bir yardımcı işlemi bozuyor. %2$s → “%3$s” (veya “%4$s”) seçeneğini deneyip yeniden başlatın.</item> + <item quantity="other">Günlükte %1$d “Function not implemented” hatası görünüyor: proot\'un seccomp hızlandırması bu cihazda bir yardımcı işlemi bozuyor. %2$s → “%3$s” (veya “%4$s”) seçeneğini deneyip yeniden başlatın.</item> + </plurals> + + <string name="mode_decky_plugins">Eklentiler</string> + <string name="mode_decky_plugins_hint">Sonraki Steam oturumu için bir eklenti ZIP dosyası yükleyin.</string> + <string name="mode_decky_plugin_downloading">İndiriliyor…</string> + <string name="mode_decky_plugin_install_zip">ZIP dosyasından yükle</string> + <string name="store_cat_games">Oyunlar</string> + <string name="store_cat_audio_video">Ses ve Video</string> + <string name="store_cat_graphics">Grafik</string> + <string name="store_cat_internet">İnternet</string> + <string name="store_cat_office">Ofis</string> + <string name="store_cat_developer">Geliştirici</string> + <string name="store_cat_education">Eğitim</string> + <string name="store_cat_science">Bilim</string> + <string name="store_cat_system">Sistem</string> + <string name="store_cat_utilities">Araçlar</string> + <string name="store_section_popular">Popüler</string> + <string name="store_section_trending">Gündemde</string> + <string name="store_section_new">Yeni</string> + <string name="store_section_updated">Güncellenen</string> + <string name="store_section_loading">yükleniyor…</string> + <string name="store_flatpak_setup">Flatpak kurulumu</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Flathub\'da güncellemeler kontrol edilemedi</string> + <string name="content_rom_installed">yüklü</string> + <string name="content_rom_folder">klasör</string> + <string name="theme_graphite">Grafit</string> + <string name="theme_graphite_detail">Grafit yüzeyler; seçilen, odaklanan veya basılan her şey için logonun kendi küre mavisi.</string> + <string name="theme_paper">Siyah üzerine kâğıt</string> + <string name="theme_paper_detail">Ana renk beyaz; simgenin mavisi çizgiler, halkalar ve noktalar için vurgu rengi.</string> + <string name="theme_icon_blue">Simge mavisi</string> + <string name="theme_icon_blue_detail">Siyah zemin, beyaz yazı; seçilen, odaklanan veya basılan her şey için mavi.</string> + <string name="theme_electric">Elektrik laciverti</string> + <string name="theme_electric_detail">Daha parlak mavi; gri yerine laciverte çalan yüzeylerde.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Boşluk</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <string name="game_sync_err_runtime">Önce çalışma ortamını yükleyin</string> + <string name="game_sync_err_steam_library">Steam kütüphanesi kullanılamıyor</string> + <string name="game_sync_err_previous_sd">Önceki SD kütüphanesi kullanılamıyor</string> + <string name="game_sync_err_game_storage">Oyun depolama alanı kullanılamıyor</string> + <string name="game_sync_err_added_games">Eklenen oyunlar klasörü kullanılamıyor</string> + <string name="game_sync_err_manifests">Steam manifest dosyaları kullanılamıyor</string> + <string name="content_system_many">Birçok sistem</string> + <string name="fm_drive_internal">Dahili</string> + <string name="fm_drive_linux">Linux sistemi</string> + <string name="fm_drive_sd">SD kart</string> + <string name="fm_drive_storage">Depolama</string> + <string name="fm_select">Seç</string> + <string name="fm_properties">Özellikler</string> + <string name="fm_favorite_add">Favorilere ekle</string> + <string name="fm_favorite_remove">Favorilerden kaldır</string> + <string name="fm_rename">Yeniden adlandır</string> + <string name="fm_copy">Kopyala</string> + <string name="fm_cut">Kes</string> + <string name="fm_actions">İşlemler</string> + <string name="fm_favorites">Favoriler</string> + <string name="fm_pin_current">Geçerli klasörü sabitle</string> + <string name="fm_no_favorites">Henüz favori yok - buraya hızlıca dönmek için bir klasörü ⋮ menüsünden sabitleyin.</string> + <string name="fm_unpin_favorite">Favorilerden kaldır</string> + <string name="fm_size_zero">0 bayt</string> + <string name="fm_unit_bytes">bayt</string> + <string name="fm_prop_location">Konum</string> + <string name="fm_prop_type">Tür</string> + <string name="fm_prop_size">Boyut</string> + <string name="fm_prop_modified">Değiştirilme</string> + <string name="fm_type_folder">Klasör</string> + <string name="fm_type_file">Dosya</string> + <string name="fm_type_ext_file">%1$s dosyası</string> + <string name="fm_read_only">Salt okunur</string> + <string name="fm_read_only_hint">Oyunların (ve Wine\'ın) bu dosyanın üzerine yazmasını veya onu silmesini engeller.</string> + <string name="fm_read_only_failed">Salt okunur ayarı değiştirilemedi</string> + <string name="fm_hidden">Gizli</string> + <string name="fm_hidden_unsupported">Bu depolama alanı gizli işaretini saklayamaz.</string> + <string name="fm_hidden_hint">Dosyayı Windows\'ta gizli olarak işaretler (Wine\'ın gizli özniteliği).</string> + <string name="fm_hidden_failed">Bu depolama alanında Gizli ayarı değiştirilemedi</string> + <string name="fm_done">Bitti</string> + <string name="fm_op_deleting">Siliniyor...</string> + <string name="fm_delete_failed">Silinemedi</string> + <string name="fm_op_moving">Taşınıyor...</string> + <string name="fm_op_copying">Kopyalanıyor...</string> + <string name="fm_op_moving_item">Taşınıyor - %1$s</string> + <string name="fm_op_copying_item">Kopyalanıyor - %1$s</string> + <string name="fm_op_moving_batch">Taşınıyor %1$d/%2$d - %3$s</string> + <string name="fm_op_copying_batch">Kopyalanıyor %1$d/%2$d - %3$s</string> + <string name="fm_paste_failed">%1$d tamamlandı, %2$d başarısız</string> + <string name="fm_paste_skipped">%1$d tamamlandı, %2$d atlandı</string> + <plurals name="fm_paste_moved"> + <item quantity="one">%1$d öğe taşındı</item> + <item quantity="other">%1$d öğe taşındı</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">%1$d öğe kopyalandı</item> + <item quantity="other">%1$d öğe kopyalandı</item> + </plurals> + <string name="fm_already_exists">“%1$s” zaten var</string> + <string name="fm_op_renaming">Yeniden adlandırılıyor...</string> + <string name="fm_rename_failed">Yeniden adlandırılamadı</string> + <string name="fm_op_creating_folder">Klasör oluşturuluyor...</string> + <string name="fm_create_folder_failed">Klasör oluşturulamadı</string> + <string name="fm_new_folder">Yeni Klasör</string> + <string name="fm_folder_name">Klasör adı</string> + <string name="fm_create">Oluştur</string> + <string name="fm_new_name">Yeni ad</string> + <string name="fm_delete_title">Silinsin mi?</string> + <string name="fm_delete_text">“%1$s” kalıcı olarak silinsin mi?</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">%1$d öğe silinsin mi?</item> + <item quantity="other">%1$d öğe silinsin mi?</item> + </plurals> + <string name="fm_cannot_undo">Bu işlem geri alınamaz.</string> + <plurals name="fm_and_more"> + <item quantity="one">…ve %1$d tane daha</item> + <item quantity="other">…ve %1$d tane daha</item> + </plurals> + <string name="fm_op_deleting_batch">Siliniyor %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">%1$d öğe silinemedi</item> + <item quantity="other">%1$d öğe silinemedi</item> + </plurals> + <string name="fm_conflict_merge_hint">Birleştir, mevcut klasörün içine dosya ekler ve dosyaları değiştirir.</string> + <string name="fm_conflict_overwrite_hint">Üzerine yaz, mevcut dosyayı değiştirir.</string> + <string name="fm_conflict_apply_all">Tüm çakışmalara uygula</string> + <string name="fm_conflict_merge">Birleştir</string> + <string name="fm_conflict_overwrite">Üzerine yaz</string> + <string name="fm_conflict_keep_both">İkisini de koru</string> + <string name="fm_conflict_skip">Atla</string> + <string name="fm_select_this_folder">Bu klasörü seç</string> + <string name="fm_back">Geri</string> + <string name="fm_drive_not_readable">%1$s bağlı ancak şu anda okunamıyor</string> + <string name="fm_list_view">Liste görünümü</string> + <string name="fm_grid_view">Izgara görünümü</string> + <string name="fm_search">Ara</string> + <string name="fm_sort">Sırala</string> + <string name="fm_sort_name">Ad</string> + <string name="fm_sort_date">Değiştirilme tarihi</string> + <string name="fm_rows_comfortable">Geniş satırlar</string> + <string name="fm_rows_compact">Kompakt satırlar</string> + <string name="fm_hide_hidden">Gizli dosyaları gizle</string> + <string name="fm_show_hidden">Gizli dosyaları göster</string> + <string name="fm_hide_favorites">Favorileri gizle</string> + <string name="fm_show_favorites">Favorileri göster</string> + <string name="fm_filter_folder">Bu klasörü filtrele</string> + <string name="fm_free_space">%1$s boş</string> + <string name="fm_selected_count">%1$d seçildi</string> + <string name="fm_select_none">Hiçbiri</string> + <string name="fm_select_all">Tümü</string> + <string name="fm_paste">Yapıştır</string> + <plurals name="fm_items"> + <item quantity="one">%1$d öğe</item> + <item quantity="other">%1$d öğe</item> + </plurals> + <string name="fm_paste_here">Buraya yapıştır: %1$s</string> + <string name="fm_paste_move_here">Buraya yapıştır (taşı): %1$s</string> + <string name="fm_rail_title">Dosyalar</string> + <string name="fm_rail_storage">DEPOLAMA</string> + <string name="fm_rail_quick">HIZLI ERİŞİM</string> + <string name="fm_rail_favorites">FAVORİLER</string> + <string name="fm_rail_downloads">İndirilenler</string> + <string name="fm_rail_downloads_short">İndirilen</string> + <string name="fm_rail_roms">ROM\'lar</string> + <string name="fm_rail_session_logs">Oturum günlükleri</string> + <string name="fm_rail_pictures">Resimler</string> + <string name="fm_favorite_added">“%1$s” Favorilere eklendi</string> + <string name="fm_favorite_removed">“%1$s” Favorilerden kaldırıldı</string> + <string name="fm_empty_directory">Boş klasör</string> + <string name="fm_up_one_level">Bir üst düzey</string> + <string name="fm_choose_folder">Bir klasör seçin</string> + <string name="fm_choose_file">Bir dosya seçin</string> + <string name="ctrl_tint_steam_blue">Steam mavisi</string> + <string name="ctrl_tint_sky">Gök mavisi</string> + <string name="ctrl_tint_white">Beyaz</string> + <string name="ctrl_tint_violet">Mor</string> + <string name="ctrl_tint_green">Yeşil</string> + <string name="ctrl_tint_amber">Kehribar</string> + <string name="ctrl_tint_red">Kırmızı</string> + <string name="ctrl_tint_pink">Pembe</string> + <string name="ctrl_button_a">A düğmesi</string> + <string name="ctrl_button_b">B düğmesi</string> + <string name="ctrl_button_x">X düğmesi</string> + <string name="ctrl_button_y">Y düğmesi</string> + <string name="ctrl_button_lb">Sol omuz tuşu</string> + <string name="ctrl_button_rb">Sağ omuz tuşu</string> + <string name="ctrl_button_lt">Sol tetik</string> + <string name="ctrl_button_rt">Sağ tetik</string> + <string name="ctrl_button_view">Görünüm düğmesi</string> + <string name="ctrl_button_menu">Menü düğmesi</string> + <string name="ctrl_target_view">Görünüm</string> + <string name="ctrl_target_menu">Menü</string> + <string name="ctrl_target_up">Yön tuşu yukarı</string> + <string name="ctrl_target_down">Yön tuşu aşağı</string> + <string name="ctrl_target_left">Yön tuşu sol</string> + <string name="ctrl_target_right">Yön tuşu sağ</string> + <string name="ctrl_target_hidden">Gizli</string> + <string name="ctrl_editor_drag">Taşımak için sürükleyin</string> + <string name="ctrl_editor_snap">Hizala</string> + <string name="ctrl_editor_save">Kaydet</string> + <string name="second_mode_none">Hiçbiri</string> + <string name="second_mode_keyboard_trackpad">Klavye + dokunmatik yüzey</string> + <string name="second_mode_terminal">Terminal</string> + <string name="second_mode_deck_controls">Deck tutamakları + dokunmatik yüzeyler</string> + + <string name="rtinst_removing">Linux çalışma ortamı kaldırılıyor</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">Linux çalışma ortamı kaldırılıyor · %1$d öğe</item> + <item quantity="other">Linux çalışma ortamı kaldırılıyor · %1$d öğe</item> + </plurals> + <string name="rtinst_removed">Linux çalışma ortamı kaldırıldı</string> + <string name="rtinst_removal_failed">Linux çalışma ortamının kaldırılması tamamlanamadı. Kurulum bölümünden kaldırmayı yeniden deneyin.</string> + <string name="rtinst_removing_leftovers">Çalışma ortamı kalıntıları kaldırılıyor</string> + <string name="rtinst_downloading">İndiriliyor</string> + <string name="rtinst_verifying">Doğrulanıyor</string> + <string name="rtinst_extracting">Ayıklanıyor</string> + <string name="rtinst_channel_description">Linux çalışma ortamı yüklenirken veya kaldırılırken gösterilir</string> + <string name="rtinst_title_installing">Linux çalışma ortamı yükleniyor</string> + <string name="rtinst_title_removing">Linux çalışma ortamı kaldırılıyor</string> + + <string name="deskpkg_runtime_missing">Linux çalışma ortamı yüklü değil</string> + <string name="deskpkg_no_checksum">Katalogda %1$s için sağlama toplamı yok</string> + <string name="deskpkg_checksum_mismatch">Sağlama toplamı uyuşmuyor - hiçbir şey değiştirilmedi</string> + <string name="deskpkg_installing">%1$s yükleniyor</string> + <string name="deskpkg_place_failed">AppImage yerleştirilemedi</string> + <string name="deskpkg_extract_failed">Ayıklama başarısız oldu</string> + <string name="deskpkg_install_failed">Yükleme başarısız oldu</string> + + <string name="flatpakmgr_setup_failed">Flatpak kurulumu başarısız oldu (çıkış %1$d)</string> + <string name="flatpakmgr_setup_unfinished">Flatpak kurulumu tamamlanmadı</string> + <string name="flatpakmgr_busy">Başka bir mağaza görevi çalışıyor</string> + <string name="flatpakmgr_not_ready">Önce Flatpak\'i kurun</string> + <string name="flatpakmgr_starting">Başlatılıyor</string> + <string name="flatpakmgr_exit_status">Flatpak %1$d durum koduyla çıktı</string> + <string name="flatpakmgr_installing">%1$s yükleniyor</string> + <string name="flatpakmgr_updating">%1$s güncelleniyor</string> + <string name="flatpakmgr_removing">%1$s kaldırılıyor</string> + <string name="flatpakmgr_installing_step">%1$s yükleniyor (%2$d/%3$d)</string> + <string name="flatpakmgr_updating_step">%1$s güncelleniyor (%2$d/%3$d)</string> + <string name="flatpakmgr_removing_step">%1$s kaldırılıyor (%2$d/%3$d)</string> + <string name="flatpakmgr_ref_translations">çeviriler</string> + <string name="flatpakmgr_ref_graphics_drivers">grafik sürücüleri</string> + <string name="flatpakmgr_ref_media_codecs">medya kodekleri</string> + <string name="flatpakmgr_ref_runtime">%1$s çalışma ortamı</string> + <string name="flatpakmgr_ref_runtime_branch">%1$s çalışma ortamı %2$s</string> + + <string name="appimg_too_small">Dosya, AppImage olamayacak kadar küçük</string> + <string name="appimg_unreadable">Dosya okunamıyor: %1$s</string> + <string name="appimg_not_linux">Bu bir AppImage değil (Linux programı değil)</string> + <string name="appimg_wrong_arch">Bu AppImage ne ARM64 (aarch64) ne de x86 bilgisayarlar için derlenmiş</string> + <string name="appimg_not_appimage">Bu bir Linux programı ama AppImage değil</string> + <string name="appimg_wrong_type">Yalnızca Tür 2 AppImage dosyaları içe aktarılabilir (bu dosya Tür %1$d)</string> + <string name="appimg_no_space_import">Yeterli boş alan yok: içe aktarma için yaklaşık %1$s gerekiyor</string> + <string name="appimg_no_space_update">Yeterli boş alan yok: güncelleme için yaklaşık %1$s gerekiyor</string> + <string name="appimg_import_failed">İçe aktarma başarısız oldu</string> + <string name="appimg_gone">Bu AppImage artık yok</string> + <string name="appimg_update_failed">Güncelleme başarısız oldu</string> + <string name="appimg_extracting">%1$s ayıklanıyor</string> + <string name="appimg_extract_failed">AppImage ayıklanamadı</string> + <string name="appimg_extract_failed_detail">AppImage ayıklanamadı: %1$s</string> + + <string name="deckymgr_stop_session_loader">Decky Loader\'ı yüklemeden önce etkin oturumu durdurun</string> + <string name="deckymgr_download_failed">Decky Loader indirilemedi</string> + <string name="deckymgr_size_mismatch">Decky Loader boyutu yayın meta verileriyle eşleşmedi</string> + <string name="deckymgr_checksum_download_failed">Yayının sağlama toplamı indirilemedi</string> + <string name="deckymgr_checksum_missing">Yayının sağlama toplamı eksik</string> + <string name="deckymgr_checksum_invalid">Yayının sağlama toplamı geçersiz</string> + <string name="deckymgr_checksum_mismatch">Decky Loader sağlama toplamı uyuşmuyor</string> + <string name="deckymgr_not_elf">Yayın dosyası uyumlu bir 64 bit Linux çalıştırılabilir dosyası değil</string> + <string name="deckymgr_session_started_loader">İndirme sırasında bir oturum başladı; Decky Loader\'ı yüklemeden önce durdurun</string> + <string name="deckymgr_loader_not_executable">PluginLoader çalıştırılabilir olarak işaretlenemedi</string> + <string name="deckymgr_cef_failed">Steam CEF uzaktan hata ayıklaması etkinleştirilemedi</string> + <string name="deckymgr_stage_failed">PluginLoader hazırlanamadı</string> + <string name="deckymgr_loader_preserve_failed">Mevcut PluginLoader korunamadı</string> + <string name="deckymgr_loader_activate_failed">Yeni PluginLoader etkinleştirilemedi</string> + <string name="deckymgr_stop_session_plugin">Bir eklenti yüklemeden önce etkin oturumu durdurun</string> + <string name="deckymgr_loader_required">Bir eklenti içe aktarmadan önce Decky Loader\'ı yükleyin</string> + <string name="deckymgr_zip_unreadable">Seçilen ZIP dosyası okunamadı</string> + <string name="deckymgr_plugins_dir_failed">Decky eklentiler dizini oluşturulamadı</string> + <string name="deckymgr_extracting_zip">Eklenti ZIP\'i ayıklanıyor</string> + <string name="deckymgr_zip_one_folder">Eklenti ZIP\'i en üst düzeyde tek bir eklenti klasörü içermelidir</string> + <string name="deckymgr_zip_missing_files">Eklenti ZIP\'inde plugin.json, package.json veya dist/index.js eksik</string> + <string name="deckymgr_plugin_name_missing">Eklenti adı eksik</string> + <string name="deckymgr_session_started_plugin">Yükleme sırasında bir oturum başladı; bir eklenti yüklemeden önce durdurun</string> + <string name="deckymgr_plugin_installed">Eklenti yüklendi · yüklemek için Steam\'i yeniden başlatın</string> + <string name="deckymgr_plugin_install_failed">Eklenti ZIP\'i yüklemesi başarısız oldu</string> + <string name="deckymgr_zip_too_many">Eklenti ZIP\'i çok fazla öğe içeriyor</string> + <string name="deckymgr_zip_invalid_path">Eklenti ZIP\'i geçersiz bir yol içeriyor</string> + <string name="deckymgr_zip_duplicate_paths">Eklenti ZIP\'i yinelenen yollar içeriyor</string> + <string name="deckymgr_zip_unsafe_path">Eklenti ZIP\'i güvenli olmayan bir yol içeriyor</string> + <string name="deckymgr_folder_failed">Eklenti klasörü oluşturulamadı</string> + <string name="deckymgr_zip_too_large">Eklenti ZIP\'i 1 GiB sınırının üzerine çıkıyor</string> + <string name="deckymgr_bundled_not_executable">Pakete dahil eklenti ikili dosyası çalıştırılabilir olarak işaretlenemedi</string> + <string name="deckymgr_remote_entry_invalid">Geçersiz remote_binary girdisi</string> + <string name="deckymgr_remote_name_invalid">Geçersiz uzak ikili dosya adı</string> + <string name="deckymgr_remote_url_https">Uzak ikili dosya URL\'si HTTPS kullanmalıdır</string> + <string name="deckymgr_remote_hash_invalid">Uzak ikili dosya %1$s geçersiz bir SHA-256 karmasına sahip</string> + <string name="deckymgr_bin_dir_failed">Eklenti bin dizini oluşturulamadı</string> + <string name="deckymgr_downloading_binary">Eklenti ikili dosyası indiriliyor · %1$s</string> + <string name="deckymgr_binary_download_failed">Eklenti ikili dosyası %1$s indirilemedi</string> + <string name="deckymgr_binary_checksum_mismatch">Eklenti ikili dosyası sağlama toplamı uyuşmuyor · %1$s</string> + <string name="deckymgr_binary_not_executable">Eklenti ikili dosyası çalıştırılabilir olarak işaretlenemedi · %1$s</string> + <string name="deckymgr_binary_replace_failed">Eklenti ikili dosyası değiştirilemedi · %1$s</string> + <string name="deckymgr_binary_install_failed">Eklenti ikili dosyası yüklenemedi · %1$s</string> + <string name="deckymgr_folder_taken">Eklenti klasörü başka bir eklentiye ait</string> + <string name="deckymgr_preserve_failed">Mevcut eklenti korunamadı</string> + <string name="deckymgr_activate_failed">İçe aktarılan eklenti etkinleştirilemedi</string> + <string name="deckymgr_settings_failed">Decky eklenti ayarları güncellenemedi</string> + <string name="deckymgr_restore_failed">Mevcut eklenti geri yüklenemedi</string> + + <string name="adbfix_save_failed">Kablosuz hata ayıklama kimliği kaydedilemedi</string> + <string name="adbfix_command_failed">ADB komutu başarısız oldu (%1$d)</string> + <string name="adbfix_not_confirmed">Android, alt süreç sınırının değiştirildiğini onaylamadı: %1$s</string> + <string name="adbfix_no_pairing">Kayıtlı bir Kablosuz hata ayıklama eşleşmesi yok</string> + <string name="adbfix_enter_address">Geçerli Kablosuz hata ayıklama IP adresini ve bağlantı noktasını girin</string> + + <string name="adbpair_timed_out">Eşleştirme açılır penceresi beklenirken zaman aşımına uğradı. DroidDeck\'ten yeniden deneyin.</string> + <string name="adbpair_discovery_failed_code">Eşleştirme açılır penceresi aranamadı (%1$d). Wi-Fi\'nin açık olduğunu kontrol edin.</string> + <string name="adbpair_discovery_failed">Eşleştirme açılır penceresi aranamadı: %1$s</string> + <string name="adbpair_six_digits">Kodun 6 hanesinin tamamını girin</string> + <string name="adbpair_pairing">Eşleştiriliyor…</string> + <string name="adbpair_code_rejected">O kod işe yaramadı. Eşleştirme açılır penceresini yeniden açıp yeni kodunu girin.</string> + <string name="adbpair_connecting">Eşleşti. Bağlanıyor…</string> + <string name="adbpair_port_not_found">Eşleşti, ancak Kablosuz hata ayıklama bağlantı noktası bulunamadı. Kablosuz hata ayıklamayı açık tutup yeniden deneyin.</string> + <string name="adbpair_applying">Ayar uygulanıyor…</string> + <string name="adbpair_command_failed">Kablosuz hata ayıklama komutu başarısız oldu</string> + <string name="adbpair_channel_name">Kablosuz hata ayıklama kurulumu</string> + <string name="adbpair_channel_description">Ayarlar açıkken Kablosuz hata ayıklama eşleştirme kodunu ister</string> + <string name="adbpair_title_pair">Kablosuz hata ayıklamayı eşle</string> + <string name="adbpair_waiting_text">Kablosuz hata ayıklamayı açın, sonra “Eşleştirme koduyla cihaz eşle” seçeneğine dokunun.</string> + <string name="adbpair_waiting_long">Kablosuz hata ayıklamayı açın, sonra “Eşleştirme koduyla cihaz eşle” seçeneğine dokunun. Ayarlar\'dan çıkmayın: kod bu bildirime gelir.</string> + <string name="adbpair_code_label">6 haneli eşleştirme kodu</string> + <string name="adbpair_code_hint">Kodu gir\'e dokunun ve Ayarlar\'da gösterilen 6 haneyi yazın.</string> + <string name="adbpair_title_code">Wi-Fi eşleştirme kodunu girin</string> + <string name="adbpair_title_retry">Kodu yeniden deneyin</string> + <string name="adbpair_enter_code">Kodu gir</string> + <string name="adbpair_title_working">Steam kuruluyor</string> + <string name="adbpair_title_done">Steam hazır</string> + <string name="adbpair_done_text">DroidDeck\'e geri dönmek için dokunun. Kablosuz hata ayıklamayı şimdi kapatabilirsiniz.</string> + <string name="adbpair_title_failed">Kablosuz hata ayıklama kurulumu durdu</string> + + <string name="selfinst_http">İndirme HTTP %1$d yanıtı verdi</string> + <string name="selfinst_size_mismatch">İndirme boyutu yayınlanan APK ile eşleşmedi ve atıldı - yeniden deneyin</string> + <string name="selfinst_checksum_mismatch">İndirme, sağlama toplamıyla eşleşmedi ve atıldı - yeniden deneyin</string> + <string name="selfinst_wrong_package">Güncelleme, %2$s değil %1$s Android paketi içindir</string> + <string name="selfinst_metadata_signer">Güncelleme meta verileri DroidDeck\'in yayın imzalama anahtarını belirtmiyor</string> + <string name="selfinst_unreadable">Android indirilen APK\'yı okuyamadı</string> + <string name="selfinst_apk_wrong_package">İndirilen APK, %2$s değil %1$s içindir</string> + <string name="selfinst_version_mismatch">İndirilen APK, yayınlanan %2$d değil versionCode %1$d diyor</string> + <string name="selfinst_downgrade">Android, yüklü %2$d üzerine versionCode %1$d\'i yüklemez</string> + <string name="selfinst_no_signing">İndirilen APK\'da imza bilgisi yok</string> + <string name="selfinst_wrong_signer">İndirilen APK, DroidDeck\'in yayın anahtarıyla imzalanmamış</string> + <string name="appupd_http">Güncelleme kataloğu HTTP %1$d yanıtı verdi</string> + + <string name="loadstate_starting">Oturum başlatılıyor…</string> + <string name="loadstate_elapsed">%1$d:%2$02d geçti · hâlâ çalışıyor</string> + <string name="loadstate_steam_update">Steam istemcisi güncellemesi indiriliyor · %1$d%%</string> + <string name="loadstate_steam_client">Steam istemcisi indiriliyor · %1$s</string> + + <string name="pextra_busy">Zaten çalışan bir uyumluluk aracı yüklemesi var</string> + <string name="pextra_stop_session">Uyumluluk araçlarını yüklemeden önce etkin oturumu durdurun</string> + <string name="pextra_no_release">%1$s için bir ARM64 yayını bulunamadı</string> + <string name="pextra_no_space">%1$s\'i yüklemek için indirme boyutuna ek olarak en az 4 GB boş alan gerekir</string> + <string name="pextra_downloading">%1$s %2$s indiriliyor</string> + <string name="pextra_retrying">İndirme yeniden deneniyor · %2$d denemenin %1$d\'i</string> + <string name="pextra_interrupted">İndirme kesintiye uğradı</string> + <string name="pextra_download_failed">İndirme başarısız oldu; yeniden deneme devam edebilsin diye yarım dosya tutuluyor</string> + <string name="pextra_too_big">İndirme, yayın boyutunu aştı ve silindi</string> + <string name="pextra_incomplete">İndirme tamamlanmadı; devam edilebilir bir yeniden deneme için yarım dosya tutuluyor</string> + <string name="pextra_verifying">İndirme doğrulanıyor</string> + <string name="pextra_checksum_unreadable">Yayının sağlama toplamı okunamadı; yeniden deneyin</string> + <string name="pextra_no_checksum">%1$s %2$s sağlama toplamı yayınlamıyor; hiçbir şey yüklenmedi</string> + <string name="pextra_checksum_mismatch">Sağlama toplamı uyuşmuyor; indirme silindi</string> + <string name="pextra_session_started">İndirme sırasında bir oturum başladı; uyumluluk araçlarını yüklemeden önce durdurun</string> + <string name="pextra_installing">%1$s yükleniyor</string> + <string name="pextra_install_failed">%1$s yüklemesi başarısız oldu (çıkış %2$d)</string> + <string name="pextra_could_not_install">%1$s yüklenemedi</string> + <string name="esync_fetching">droiddeck-esync paketi getiriliyor</string> + + <string name="cmgr_fex_linux_helpers">Linux yardımcılarıyla</string> + <string name="cmgr_fex_dlls_only">Yalnızca DLL\'ler</string> + <string name="cmgr_original_proton_updated">Orijinal (son değişimden sonra Proton güncellendi)</string> + <string name="cmgr_changed_outside">Bileşenler\'in dışında değiştirildi</string> + <string name="cmgr_not_present">yok</string> + <string name="cmgr_original_version">Orijinal %1$s</string> + <string name="cmgr_original_from">%1$s\'ten orijinal</string> + <string name="cmgr_swap_queued">%1$s %2$s, üzerinde çalışan oyun kapandığında %3$s\'e geçer.</string> + <string name="cmgr_swap_done">%1$s %2$s artık %3$s\'te. Bir sonraki oyun başlatıldığında uygulanır.</string> + <string name="cmgr_original_missing">Bu orijinal paket saklanmıyor</string> + <string name="cmgr_restore_queued">%2$s\'nin %1$s\'i, üzerinde çalışan oyun kapandığında orijinaline döner.</string> + <string name="cmgr_restore_done">%2$s\'nin %1$s\'i %3$s orijinalinden geri yüklendi.</string> + <string name="cmgr_package_in_use">%1$s, %2$s\'te kullanımda. Önce o Proton\'u başka bir şeyle değiştirin.</string> + <string name="cmgr_package_not_stored">%1$s saklanmıyor</string> + <string name="cmgr_deleted">%1$s silindi.</string> + <string name="cmgr_original_kept">Yüklü derlemenin orijinali saklanır: geri dönüş yolu budur.</string> + <string name="cmgr_not_stored">Saklanmıyor</string> + <string name="cmgr_deleted_original">%2$s\'nin %1$s orijinali silindi.</string> + <string name="cmgr_nightlies_only">İndirmeler yalnızca Nightlies yayınlarından gelir</string> + <string name="cmgr_list_predates_checksums">Bu paket listesi sağlama toplamlarından önceki bir tarihe ait - yenileyip yeniden deneyin</string> + <string name="cmgr_checksum_mismatch">Sağlama toplamı uyuşmuyor</string> + + <string name="sprefs_upscaler_linear">Doğrusal</string> + <string name="sprefs_upscaler_nearest">En yakın</string> + <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (kalite)</string> + <string name="sprefs_upscaler_sharpen">Yalnızca keskinleştirme</string> + <string name="sprefs_lod_bias_auto">Otomatik (ölçeklemeye göre)</string> + + <string name="look_off_desc">Hiçbir şey uygulanmadı.</string> + <string name="look_game_clarity">Oyun Netliği</string> + <string name="look_game_clarity_desc">Hafif kontrastla keskinleştirme - günlük kullanım için.</string> + <string name="look_vivid">Canlı</string> + <string name="look_vivid_desc">Renk sonuna kadar zorlanır. Gökyüzü çizgilenmesin diye bant giderme açık.</string> + <string name="look_cinematic">Sinematik</string> + <string name="look_cinematic_desc">Daha derin kontrast, daha soğuk renk, yumuşatılmış kenarlar.</string> + <string name="look_competitive">Rekabetçi</string> + <string name="look_competitive_desc">Gölgeler açılır ve ağır keskinleştirme - önce insanları fark edin.</string> + <string name="look_adaptive_sharpen">Uyarlanabilir Keskinleştirme</string> + <string name="look_adaptive_sharpen_desc">Yalnızca keskinleştirme, renk dokunulmadan kalır.</string> + <string name="look_filmic">Filmsel</string> + <string name="look_filmic_desc">Film benzeri eğri, hafifçe kısık.</string> + <string name="look_arcade">Arcade</string> + <string name="look_arcade_desc">Abartılı ve çarpıcı.</string> + <string name="look_retro_crt">Retro CRT</string> + <string name="look_retro_crt_desc">CRT tüpü ve analog sinyal geçişleri bir arada.</string> + <string name="look_upscale_sharp">Keskin Büyütme</string> + <string name="look_upscale_sharp_desc">Panel çözünürlüğünün altında çalıştırmak için.</string> + <string name="look_pixel_clean">Temiz Piksel</string> + <string name="look_pixel_clean_desc">2D ve eski oyunlar için net pikseller.</string> + <string name="look_anime_edge">Anime Kenar</string> + <string name="look_anime_edge_desc">Çizgi film konturları ve keskinleştirme.</string> + + <string name="gpuinfo_not_adreno">Adreno GPU değil</string> + <string name="gpuinfo_supported">Destekleniyor</string> + <string name="gpuinfo_experimental">Deneysel: sürücüleri test derlemeleri</string> + <string name="gpuinfo_below_tested">Test edilen donanımın altında (Adreno 725 ve daha yenisi): çalışmayabilir</string> + <string name="gpuinfo_unsupported">Desteklenmiyor: DroidDeck bir Adreno (Snapdragon) GPU\'su gerektirir</string> + + <string name="turnip_rate_limited">GitHub\'ın hız sınırına ulaşıldı - daha sonra yeniden deneyin</string> + <string name="turnip_github_http">GitHub HTTP %1$d yanıtı verdi</string> + <string name="turnip_download_http">indirme HTTP %1$d yanıtı verdi</string> + <string name="turnip_list_predates_checksums">Bu sürücü listesi sağlama toplamlarından önceki bir tarihe ait - yenileyip yeniden deneyin</string> + <string name="turnip_checksum_mismatch">Sağlama toplamı uyuşmuyor - indirme atıldı</string> + <string name="turnip_not_adrenotools">Bir AdrenoTools sürücüsü değil: %1$s</string> + <string name="turnip_reject_no_meta">meta.json yok</string> + <string name="turnip_reject_meta_unreadable">meta.json okunamıyor (%1$s)</string> + <string name="turnip_reject_linux_driver">bu bir Linux çalışma ortamı sürücüsü (“Linux çalışma ortamı sürücüsü” altında içe aktarın)</string> + <string name="turnip_reject_wayland">bu bir Wayland oyun sürücüsü, bu uygulamanın buna bir kullanımı yok</string> + <string name="turnip_reject_no_library_name">meta.json bir libraryName belirtmiyor, bu yüzden AdrenoTools\'a hiçbir şey verilemedi</string> + <string name="turnip_reject_library_missing">meta.json, zip içinde olmayan %1$s\'i belirtiyor</string> + <string name="turnip_reject_not_aarch64">%1$s 64 bit AArch64 ELF paylaşılan kitaplığı değil</string> + <string name="turnip_reject_glibc">%1$s glibc\'ye bağlanıyor - bir “-Linux” Turnip\'i, “Linux çalışma ortamı sürücüsü” altına ait</string> + <string name="lvd_no_library">Bu zip içinde libvulkan_freedreno*.so yok. Bir Android (AdrenoTools) veya -Wayland Turnip zip\'i bir Linux çalışma ortamı sürücüsü değildir.</string> + <string name="lvd_not_aarch64">%1$s 64 bit AArch64 ELF paylaşılan kitaplığı değil.</string> + <string name="lvd_not_glibc">%1$s bir glibc sürücüsü değil - Android\'in libc\'sine bağlanıyor. Linux çalışma ortamı bir “-Linux” zip\'i gerektirir; düz veya “-Wayland” bir Turnip, Steam istemcisi tarafından hiç yüklenemez.</string> + <string name="bundle_no_manifest">Bir sürücü paketi değil: bir android ve bir linux sürücüsü belirten manifest.json yok</string> + <string name="bundle_entry_outside">Bir sürücü paketi değil: %1$s dışına işaret ediyor</string> + <string name="bundle_too_large">Bir sürücü paketi değil: %1$d MB\'den fazla alana açılıyor</string> + + <string name="gstore_sd_card">SD kart</string> + <string name="gstore_folder">Klasör</string> + + <string name="logshare_chooser">Oturum günlüklerini paylaş</string> + + <string name="hdr_needs_android_10">Android 10 veya daha yenisi gerekir (renk uzayına sahip ekran katmanları)</string> + <string name="hdr_none">bu ekran HDR desteği bildirmiyor</string> + <string name="hdr_not_hdr10">bu ekran HDR10 değil, %1$s destekliyor</string> + + <string name="gpu_this_gpu">bu GPU</string> + <string name="phantom_title_on">Alt süreç sınırı açık</string> + <string name="phantom_title_unset">Alt süreç sınırı ayarlanmamış</string> + <string name="phantom_title_unreadable">Alt süreç sınırı kontrol edilemedi</string> + <string name="phantom_title_off">Alt süreç sınırı kapalı</string> + <string name="phantom_title_not_required">Alt süreç sınırı gerekli değil</string> + <string name="phantom_instructions_fix">%1$s Bu mümkün değilse bu cihazı ADB\'li bir bilgisayara bağlayıp aşağıdaki komutu çalıştırın. Buraya dönüp yeniden kontrol edin.</string> + <string name="phantom_instructions_off">Steam oturumları başlayabilir.</string> + <string name="phantom_instructions_not_applicable">Bu Android sürümü bu sınırı kullanmaz.</string> + <string name="phantom_fix_toggle">Geliştirici seçenekleri\'nde “Alt süreç kısıtlamalarını devre dışı bırak” seçeneğini açın ve sonrasında Geliştirici seçeneklerini açık tutun.</string> + <string name="phantom_fix_no_toggle">Bu Android sürümünde bunun için bir anahtar yok. Bu cihazdan değiştirmek için Kablosuz hata ayıklamayı kullanın.</string> + <string name="upd_block_no_apk">Bu derlemede bu DroidDeck kopyası için bir indirme yok.</string> + <string name="upd_block_version_code">Android, versionCode %1$d yüklü %2$d\'nin altında olduğu için bunu bu derlemenin üzerine yüklemez.</string> + <string name="upd_fail_downgrade">Android, daha eski bir sürümü daha yeni birinin üzerine yüklemez. Kanal yetişene kadar bu derlemede kalın.</string> + <string name="upd_fail_signature">Bu DroidDeck kopyası farklı imzalanmış (yerel bir derleme mi?), bu yüzden Android onu yerinde güncellemez.</string> + <string name="upd_fail_storage">Güncelleme için yeterli boş alan yok.</string> + <string name="upd_fail_aborted">Güncelleme iptal edildi.</string> + <string name="upd_fail_with_message">Güncelleme yüklenmedi: %1$s</string> + <string name="upd_fail_generic">Güncelleme yüklenmedi.</string> + <string name="saves_layout_zip">%1$s zip\'i</string> + <string name="saves_zip">zip</string> + <string name="saves_not_launched">%1$s henüz başlatılmadı</string> + <string name="saves_none_found">%1$s için kayıt dosyası bulunamadı</string> + <string name="saves_not_save_zip">%1$s\'de drive_c klasörü yok, bu yüzden bir GameHub veya Winlator kayıt zip\'i değil</string> + <string name="gstore_cannot_create">%1$s içinde klasör oluşturulamıyor</string> + <string name="gstore_cannot_write">%1$s içine yazılamıyor</string> + <string name="gstore_cannot_write_detail">%1$s içine yazılamıyor (%2$s)</string> + <string name="gfiles_folder_unavailable">Klasör kullanılamıyor</string> + <string name="gfiles_name_taken">Başka bir dosya zaten %1$s\'i kullanıyor</string> + <string name="gfiles_art_unavailable">Görsel klasörü kullanılamıyor</string> + <string name="gfiles_unrelated_inventory">Klasörde ilgisiz bir eşitleme envanteri var</string> + <string name="gfiles_remove_failed">%1$s kaldırılamadı</string> + <string name="gfiles_outside_folder">Dosya dışa aktarma klasörünün dışına çıkıyor</string> + <string name="cmgr_fex_unnamed">FEX (sürüm adı olmayan derleme)</string> + <string name="cmgr_no_profile">%1$s içinde profile.json yok</string> + <string name="cmgr_not_component_package">%1$s bir Linux bileşen paketi değil (tür %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s boş</string> + <string name="mode_storage_auto_short">Otomatik</string> + <string name="appupd_catalog_rejected">Güncelleme kataloğu kontrollerini geçemedi (%1$s)</string> + <string name="main_adb_clip_label">DroidDeck alt süreç ayarı</string> + <string name="setup_signed_in_offline_unnamed">Oturum açıldı · çevrimdışı mod</string> + <string name="setup_signed_in_unnamed">Oturum açıldı</string> + <string name="driver_tag_bundled">PAKETLİ</string> + <string name="driver_tag_downloaded">İNDİRİLDİ</string> + <string name="driver_tag_imported">İÇE AKTARILDI</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Oturum ekranı</string> + <string name="display_resolution">Çözünürlük</string> + <string name="display_game_fit">16:9 oyun sığdırma</string> + <string name="display_game_fit_hint">16:9 oyunların bu ekrana nasıl sığacağı. Sonraki oturumda.</string> + <string name="display_game_fit_unavailable">Ekrana Uydur\'u veya 16:9\'dan daha dar özel bir boyutu seçin.</string> + <string name="display_preserve">Oranları koru</string> + <string name="display_stretch">Ekrana uzat</string> + <string name="display_image_scaling">Görüntü ölçekleme</string> + <string name="display_filter">Ölçekleme filtresi</string> + <string name="display_filter_hint">Steam ve Masaüstü tarafından paylaşılır.</string> + <string name="display_filter_live">Anında uygulanır. Steam ve Masaüstü tarafından paylaşılır.</string> + <string name="display_filter_note">FSR, GSR ve NIS, oturum görüntüsü büyütülürken uygulanır. Yalnızca keskinleştirme büyütme olmadan da çalışır.</string> + <string name="display_sharpness">Filtre keskinliği</string> + <string name="display_frame_rate">Kare hızı</string> + <string name="display_look_filter">%1$s Ölçeklemeyi %2$s olarak ayarlar.</string> + <string name="display_match_screen">Ekrana Uydur (%1$d×%2$d)</string> + <string name="display_follow_screen">Ekranı takip et (katlanıp açıldığında yeniden boyutlanır)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Cihaz ayrıntılarını göster</string> + <string name="device_info_hide">Cihaz ayrıntılarını gizle</string> + <string name="device_info_loading">Cihaz okunuyor…</string> + <string name="device_info_copy">Kopyala</string> + <string name="device_info_copy_hint">Bu ayrıntıları bir hata raporuna kopyalayın.</string> + <string name="device_info_copied">Kopyalandı.</string> + <string name="force_ssbs_title">SSBS\'i zorla</string> + <string name="force_ssbs_hint">Wine\'ın sürekli kapattığı bir CPU hız ayarını açık tutar. Snapdragon 8 Elite\'te Windows oyunlarını hızlandırabilir; daha eski yongalarda pek fark görülmez. Bir sonraki oyun başlatmasında uygulanır.</string> + + <string name="rail_stores">Mağazalar</string> + <string name="source_steam">Steam</string> + <string name="source_added">Özel</string> + <string name="setup_stores">Mağazalar</string> + <string name="setup_stores_open_on">Bir mağazayı şurada aç</string> + <string name="setup_stores_speed">İndirme hızı katmanı</string> + <string name="setup_stores_engine">Mağazalar motoru</string> + <string name="setup_stores_engine_chip_ready">● Motor hazır</string> + <string name="setup_stores_engine_chip_missing">Derlenmedi</string> + <string name="stores_tier_balanced">Dengeli</string> + <string name="stores_tier_fast">Hızlı</string> + <string name="stores_tier_max">Maks</string> + <string name="stores_title">Mağazalar</string> + <string name="stores_settings">Mağaza ayarları</string> + <string name="stores_settings_eyebrow">Ayarlar</string> + <string name="stores_chip_signed_out">(oturum açılmadı)</string> + <string name="stores_tab_store">Mağaza</string> + <string name="stores_tab_installed">Yüklü</string> + <string name="stores_tab_library">Kütüphane</string> + <string name="stores_tab_all">Tümü</string> + <string name="stores_tab_downloads">İndirmeler</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Oturum aç</string> + <string name="stores_signout_named">%1$s · oturumu kapat</string> + <string name="stores_signout_title">%1$s\'ten oturum kapatılsın mı?</string> + <string name="stores_signout_note">Yüklü oyunlar kalır. Tekrar oturum açtığınızda kütüphane yeniden getirilir.</string> + <string name="stores_signout_stay">Oturumu açık tut</string> + <string name="stores_signout_go">Oturumu kapat</string> + <string name="stores_search_catalog">%1$s kataloğunda ara…</string> + <string name="stores_search_installed">Yüklü oyunlarda ara…</string> + <string name="stores_search_library">Kütüphanenizde ara…</string> + <string name="stores_search_open">Ara</string> + <string name="stores_installed_empty">Yüklü bir şey yok</string> + <string name="stores_library_loading">Yükleniyor</string> + <string name="stores_nothing_matches">Oyun yok</string> + <string name="stores_shelves_loading">Mağaza yükleniyor…</string> + <string name="stores_amazon_no_catalog">Katalog yok</string> + <string name="stores_shelf_new">Yenilikler</string> + <string name="stores_shelf_deals">Fırsatlar</string> + <string name="stores_shelf_free">Ücretsiz oyunlar</string> + <string name="stores_shelf_trending">Gündemde</string> + <string name="stores_shelf_library">Kütüphaneniz</string> + <string name="stores_play">Oyna</string> + <string name="stores_install">Yükle</string> + <string name="stores_resume_install">Yüklemeyi sürdür</string> + <string name="stores_dl_interrupted">Kesintiye uğradı</string> + <string name="stores_install_size">Yükle · %1$s</string> + <string name="stores_get_free">Ücretsiz al</string> + <string name="stores_view_on">%1$s\'te görüntüle</string> + <string name="stores_buy_price">Satın al · %1$s</string> + <string name="stores_installed_chip">Yüklü</string> + <string name="stores_in_library">Kütüphanede</string> + <string name="stores_free">Ücretsiz</string> + <string name="stores_resume">Sürdür</string> + <string name="stores_refresh">Yenile</string> + <string name="stores_show_mature">Yetişkin içeriği göster</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Çevrimdışı</string> + <string name="epic_card_none">Kapalı</string> + <string name="epic_eos">Epic oturum açma (EOS)</string> + <string name="epic_offline">Çevrimdışı başlat</string> + <string name="epic_resolve">Epic oturum açmayı düzelt</string> + <string name="store_cloud_saves">Bulut kayıtları</string> + <string name="cloud_upload">Buluta yükle</string> + <string name="cloud_download">Buluttan indir</string> + <string name="cloud_none">Bulut kaydı yok</string> + <string name="cloud_conflict">Bulut çakışması</string> + <string name="cloud_conflict_files">Bulut çakışması · %1$d dosya</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Bulutu koru</string> + <string name="cloud_keep_local">Yerel olanı koru</string> + <string name="cloud_checking">Bulut kontrol ediliyor…</string> + <string name="cloud_last_sync">Son eşitleme %1$s</string> + <string name="cloud_never">Henüz eşitlenmedi</string> + <string name="cloud_done">%1$d dosya</string> + <string name="stores_eyebrow_installed">yüklü</string> + <string name="stores_eyebrow_library">kütüphanenizde</string> + <string name="stores_eyebrow_store">mağaza</string> + <string name="achievements_title">Başarımlar</string> + <string name="achievements_count">%2$d başarımdan %1$d açıldı</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Açık · %1$d</string> + <string name="achievements_group_locked">Kilitli · %1$d</string> + <string name="achievements_group_hidden">Gizli · %1$d</string> + <string name="achievements_hidden">Gizli başarım</string> + <string name="achievements_hidden_detail">Kilidi açıldığında ayrıntılar gösterilir.</string> + <string name="achievements_all">Tüm başarımların kilidi açıldı</string> + <string name="achievements_reading">Yükleniyor…</string> + <string name="achievements_signed_out">Başarımlarını görmek için %1$s hesabına giriş yap.</string> + <string name="achievements_unreachable">%1$s\'e bağlanılamadı. Çevrimiçi olduğunda tekrar kontrol et.</string> + <string name="achievements_none">%1$s bu oyun için başarım listelemiyor.</string> + <string name="achievements_show_all">Tümünü göster (%1$d)</string> + <string name="achievements_dialog_title">Başarımlar · %1$s</string> + <string name="stores_in_steam">✓ Steam\'de</string> + <string name="stores_uninstall">Yüklemeyi kaldır</string> + <string name="stores_uninstalling">Kaldırılıyor…</string> + <string name="stores_uninstall_title">%1$s kaldırılsın mı?</string> + <string name="stores_uninstall_body_size">%1$s\'lık yer açar ve onu Steam\'den kaldırır.</string> + <string name="stores_uninstall_body">Dosyalarını ve Steam kısayolunu kaldırır.</string> + <string name="stores_launch_not_registered">Steam bir sonraki başladığında oyunu fark eder; oradan başlatın veya Steam\'i şimdi başlatın.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Steam\'in oyun başına seçimi</string> + <string name="stores_card_audio">Ses</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Bileşenler</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Motor</string> + <string name="stores_card_engine_value">%1$s · yerel</string> + <string name="stores_card_engine_java">%1$s · yerleşik</string> + <string name="stores_how_it_runs">Nasıl çalışır</string> + <string name="stores_kv_fetched">Şuradan getirildi</string> + <string name="stores_kv_fetched_value">%1$s, hesabınızla</string> + <string name="stores_kv_installed_to">Şuraya yüklendi</string> + <string name="stores_kv_installed_to_value">Oyun depolama alanı (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Şununla başlatılır</string> + <string name="stores_kv_launched_by_value">Steam ve Proton, Steam oyunlarıyla aynı yol</string> + <string name="stores_kv_launched_by_epic">Steam ve Proton · her başlatmada Epic oturum açma, EOS kaplaması kapalı</string> + <string name="stores_kv_launched_by_amazon">Steam ve Proton · Amazon Games Services değişkenleri ayarlı</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">yüklemede Steam\'e eklenir (Deck arayüzü + Steam Input)</string> + <string name="stores_downloads_empty">İndirme yok</string> + <string name="stores_dl_paused">Duraklatıldı</string> + <string name="stores_dl_queued">Kuyrukta</string> + <string name="stores_dl_queued_at">Kuyrukta · #%1$d</string> + <string name="stores_dl_cancelled">İptal edildi</string> + <string name="stores_dl_failed">Başarısız oldu</string> + <string name="stores_dl_failed_reason">Başarısız oldu · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Duraklat</string> + <string name="stores_dl_resume">Sürdür</string> + <string name="stores_dl_cancel">İptal et</string> + <string name="stores_dl_retry">Yeniden dene</string> + <string name="stores_dl_clear">Temizle</string> + <string name="stores_dl_clear_all">Bitenleri temizle</string> + <string name="stores_stage_manifest">Manifest</string> + <string name="stores_stage_download">İndirme</string> + <string name="stores_stage_verify">Doğrulama</string> + <string name="stores_stage_install">Yükleme</string> + <string name="stores_stage_done">Tamamlandı</string> + <string name="stores_stage_checking">Kontrol ediliyor</string> + <string name="stores_stage_downloading">İndiriliyor</string> + <string name="stores_dl_cancel_confirm">İndirme silinsin mi?</string> + <string name="stores_dl_failed_kept">Başarısız oldu · %1$s tutuldu</string> + <string name="stores_stage_verifying">Doğrulanıyor</string> + <string name="stores_stage_installing">Yükleniyor</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d dosya</string> + <string name="stores_download_manager">İndirme yöneticisi</string> + <string name="stores_parallel">Aynı anda indirme sayısı</string> + <string name="stores_install_where">Şuraya yükle</string> + <string name="stores_install_title">%1$s\'i yükle:</string> + <string name="stores_target_internal">Dahili</string> + <string name="stores_target_free">%1$s boş</string> + <string name="stores_notification_channel">Mağaza indirmeleri</string> + <string name="stores_notification_channel_hint">Oyun indirme ilerlemesini gösterir ve indirmelerin arka planda sürmesini sağlar</string> + <string name="stores_notification_one">İndiriliyor</string> + <string name="stores_notification_many">%1$d indirme</string> + <string name="stores_notification_waiting">Hazırlanıyor…</string> + <string name="stores_notification_done_channel">Tamamlanan mağaza indirmeleri</string> + <string name="stores_notification_installed">%1$s yüklendi</string> + <string name="stores_notification_failed">%1$s indirmesi başarısız oldu</string> + <string name="stores_notification_delete">Sil</string> + <string name="stores_notification_keep">Tut</string> + <string name="stores_login_title">%1$s\'e oturum aç</string> + <string name="stores_login_provider_title">Sağlayıcınızla oturum açın</string> + <string name="stores_login_reload">Yeniden yükle</string> + <string name="stores_login_close">Kapat</string> + <string name="stores_login_retry">Yeniden dene</string> + <string name="stores_login_finishing">Oturum açma tamamlanıyor…</string> + <string name="stores_login_error_network">Oturum açma sayfası yüklenemedi: %1$s</string> + <string name="stores_login_error_idp_blocked">Bu sağlayıcı yerleşik bir sayfadan oturum açmayı reddediyor. Bunun yerine mağaza parolanızla oturum açın.</string> + <string name="stores_login_error_http">Oturum açma sayfası bir hatayla yanıt verdi (HTTP %1$d).</string> + <string name="stores_login_error_ssl">Oturum açma sayfasına bağlantı güvenli değil.</string> + <string name="stores_login_error_verification">Oturum açma doğrulanamadı. Yeniden deneyin.</string> + <string name="stores_login_error_generic">Oturum açma başarısız oldu. Yeniden deneyin.</string> + <string name="common_close">Kapat</string> + <string name="common_back">Geri</string> + <string name="common_edit">Düzenle</string> + <string name="games_add_all">Bu klasördeki tüm oyunları ekle</string> + <string name="games_added_none">Bu klasörde yeni bir şey yok</string> + <string name="edit_name">Ad</string> + <string name="edit_target">Hedef</string> + <string name="edit_start_in">Başlangıç yeri</string> + <string name="edit_launch_options">Başlatma seçenekleri</string> + <string name="edit_artwork">Görsel</string> + <string name="edit_other_exe">Diğer exe…</string> + <string name="edit_remove">Kaldır</string> + <string name="art_cover">Kapak</string> + <string name="art_background">Arka plan</string> + <string name="art_logo">Logo</string> + <string name="art_icon">Simge</string> + <string name="art_src_folder">Oyun klasörü</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">Dosyanız</string> + <string name="art_src_exe">Exe\'den</string> + <string name="art_src_auto">Otomatik</string> + <string name="art_pick_image">Görsel seç…</string> + <string name="art_pick_title">Bir görsel seçin: %1$s</string> + <string name="art_reset">Otomatiğe sıfırla</string> + <plurals name="games_added_n"> + <item quantity="one">%1$d oyun eklendi</item> + <item quantity="other">%1$d oyun eklendi</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="one">%1$d zaten Oyunlar\'da</item> + <item quantity="other">%1$d zaten Oyunlar\'da</item> + </plurals> + <string name="hop_back">B · %1$s geri dön</string> + <string name="hop_from_games">oyuna</string> + <string name="hop_from_play">Oyna ayarlarına</string> + <string name="sheet_graphics_drivers">Grafik sürücüleri</string> + <string name="comp_this_proton">Bu Proton</string> + <string name="comp_every_proton">Her Proton</string> + <string name="comp_for_every_proton">Her Proton için</string> + <string name="comp_available_count">Kullanılabilir · %1$d</string> + <string name="comp_checked">Kontrol edildi %1$s</string> + <string name="comp_check_all">Yeni bileşenleri ve sürücüleri kontrol et</string> + <string name="comp_get">Al</string> + <string name="comp_proton_bar">%1$s · aşağıdaki DXVK, VKD3D-Proton ve FEX bu Proton\'a ait</string> + <string name="comp_device_tested">%1$s · test edildi</string> + <string name="comp_device_untested">%1$s · test edilmedi</string> + <string name="comp_device_unsupported">%1$s · desteklenmiyor</string> + <string name="gpu_auto_keeps">Önerilen çifti güncel tutar</string> + <string name="gpu_bar_auto">Otomatik · %1$s</string> + <string name="gpu_separately">Çalışma ortamı ve ekran sürücülerini ayrı ayrı</string> + <string name="gpu_use_title">%1$s kullanılsın mı?</string> + <string name="proton_remove_title">%1$s kaldırılsın mı?</string> + <string name="setup_tab_diagnostics">Tanılama</string> + <string name="setup_ready">Oynamaya hazır</string> + <string name="setup_not_ready">Henüz oynamaya hazır değil</string> + <string name="setup_ready_runtime">çalışma ortamı %1$s</string> + <string name="setup_ready_limit_off">alt süreç sınırı kapalı</string> + <string name="setup_ready_signed_in">%1$s olarak oturum açıldı</string> + <string name="setup_needs_you">Sizden bir şey istendiğinde</string> + <string name="setup_group_look">Görünüm</string> + <string name="setup_group_device">Bu cihaz</string> + <string name="setup_group_art">Kütüphane görselleri</string> + <string name="flathub_eyebrow">LINUX UYGULAMALARI</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Linux masaüstü için masaüstü uygulamaları, Masaüstü\'nden açılır.</string> + <string name="flathub_setup">Flathub\'ı kur</string> + <string name="flathub_remove_title">%1$s kaldırılsın mı?</string> + <string name="flathub_remove_note">Ayarları ve dosyaları saklanır.</string> + <string name="flathub_keep">Tut</string> + <string name="desktop_size">Diskteki boyut</string> + <string name="desktop_uninstall_title">Masaüstü kaldırılsın mı?</string> + <string name="desktop_all_systems">Tümü</string> + <string name="gog_achievements">GOG başarımları</string> + <string name="gog_achievements_hint">Desteklenen GOG oyunları için başarımları eşitler. Sonraki başlatmada uygulanır.</string> + <string name="gog_achievements_off">Başarımlar kapalı</string> + <string name="gog_achievements_on">Başarımlar açık</string> + <string name="gog_achievements_signin">GOG\'a tekrar giriş yap</string> + <string name="gog_achievements_uploaded">Başarımlar yüklendi</string> + <string name="gog_achievements_pending">Başarım yüklemesi beklemede</string> + <string name="gog_achievements_enable_store">Mağaza ayarlarında GOG başarımlarını aç</string> + <string name="gog_achievements_unavailable">Başarımlar kullanılamıyor; oturum günlüklerini kontrol et</string> + <string name="achievement_unlocked">Başarımın kilidi açıldı</string> + <string name="touch_paste">Yapıştır</string> + <string name="touch_dpad">Yön tuşu</string> + <string name="touch_left_stick">Sol analog çubuk</string> + <string name="touch_right_stick">Sağ analog çubuk</string> + <string name="touch_menu">Menü</string> + <string name="touch_keyboard">Klavye</string> + <string name="touch_dpad_up">Yön tuşu yukarı</string> + <string name="touch_dpad_down">Yön tuşu aşağı</string> + <string name="touch_dpad_left">Yön tuşu sol</string> + <string name="touch_dpad_right">Yön tuşu sağ</string> + <string name="touch_title">Dokunmatik kontroller</string> + <string name="touch_controller">Dokunmatik kontrolcü</string> + <string name="touch_mouse">Fare</string> + <string name="touch_on">Açık</string> + <string name="touch_off">Kapalı</string> + <string name="touch_speed_slow">Yavaş</string> + <string name="touch_speed_normal">Normal</string> + <string name="touch_speed_fast">Hızlı</string> + <string name="touch_edit_layout">Düzeni düzenle</string> + <string name="touch_droiddeck_menu">DroidDeck menüsü</string> + <string name="touch_drag_hint">Kontrolleri ekrana sürükle</string> + <string name="touch_arrange">Düzenle</string> + <string name="touch_discard">Vazgeç</string> + <string name="touch_saving">Kaydediliyor…</string> + <string name="touch_icon">Simge</string> + <string name="touch_narrower">G−</string> + <string name="touch_wider">G+</string> + <string name="touch_shorter">Y−</string> + <string name="touch_taller">Y+</string> + <string name="touch_hide">Gizle</string> + <string name="touch_color_opacity">Renk ve opaklık…</string> + <string name="touch_copy_layout">Bu düzeni kopyala</string> + <string name="touch_layout_copied">Düzen kopyalandı; herhangi bir oyunun dokunmatik düzenine yapıştırabilirsin</string> + <string name="touch_paste_layout">Kopyalanan düzeni yapıştır</string> + <string name="touch_reset_layout">Steam\'in varsayılanına sıfırla</string> + <string name="touch_no_binding">Bu kontrolün simge eklenebilecek bir Steam ataması yok</string> + <string name="touch_icon_for">%1$s için simge</string> + <string name="touch_layout_color">Dokunmatik düzen rengi</string> + <string name="touch_done">Bitti</string> + <string name="touch_save_failed">Dokunmatik düzen kaydedilemedi</string> + <string name="touch_saved">Dokunmatik düzen Steam\'e kaydedildi</string> + <string name="touch_app_id">Uygulama %1$d</string> + <string name="touch_label_hint">Etiket (isteğe bağlı)</string> + <string name="touch_icons_missing">Steam\'in simgeleri bulunamadı</string> + <plurals name="touch_icons_found"> + <item quantity="one">Steam\'den %1$d simge</item> + <item quantity="other">Steam\'den %1$d simge</item> + </plurals> + <string name="touch_icon_color">Simge rengi</string> + <string name="touch_button_color">Düğme rengi</string> + <string name="touch_no_icon">Simge yok</string> + + <string name="stores_sync_sign_in">Oturum açma durumu kontrol ediliyor…</string> + <string name="stores_sync_library">Kitaplığın alınıyor…</string> + <string name="stores_sync_game_list">Oyun listesi alınıyor…</string> + <string name="stores_sync_details">Oyun bilgileri yükleniyor… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">%1$d oyun eşitleniyor…</item> + <item quantity="other">%1$d oyun eşitleniyor…</item> + </plurals> + <string name="stores_sync_progress">Eşitleniyor… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Kapak görselleri alınıyor…</string> + <string name="stores_sync_failed">Eşitleme başarısız oldu</string> + <string name="stores_sync_no_games">%1$s kitaplığında oyun bulunamadı</string> + <string name="stores_sync_amazon_no_answer">Amazon Games kitaplığın için yanıt vermedi: yenile veya yeniden oturum aç</string> + <string name="stores_sync_gog_unreachable">GOG\'a bağlanılamadı</string> + <string name="stores_sync_gog_unreadable">GOG kitaplığın okunamadı</string> + <string name="stores_registering">Steam\'e kaydediliyor…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Cardboard modu</string> + <string name="cardboard_mode_hint">Telefon VR gözlüğü için sol ve sağ göz görüntüleri.</string> + <string name="cardboard_source">Görüntü</string> + <string name="cardboard_source_screen">Sanal ekran</string> + <string name="cardboard_source_sbs">Yan yana 3D</string> + <string name="cardboard_viewer">Gözlük</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Lens yok</string> + <string name="cardboard_ipd">Göz arası mesafe</string> + <string name="cardboard_screen_width">Ekran genişliği</string> + <string name="cardboard_screen_distance">Ekran uzaklığı</string> + <string name="cardboard_head_tracking">Kafa takibi</string> + <string name="cardboard_no_sensor">Bu cihazda dönüş sensörü yok.</string> + <string name="cardboard_recenter">Görünümü ortala</string> + <string name="cardboard_recenter_button">Ortala</string> + <string name="cardboard_steamvr">SteamVR (deneysel)</string> + <string name="cardboard_steamvr_install">Yükle</string> + <string name="cardboard_steamvr_install_hint">Valve\'ın ARM64 SteamVR\'ını indirir (571 MB).</string> + <string name="cardboard_steamvr_installing">SteamVR indiriliyor ve açılıyor…</string> + <string name="cardboard_steamvr_start">Başlat</string> + <string name="cardboard_steamvr_ready_hint">SteamVR\'ı başınızı başlık olarak kullanarak çalıştırır; görüntü yan yana 3D\'ye geçer.</string> + <string name="cardboard_steamvr_stop">Durdur</string> + <string name="cardboard_steamvr_running_hint">SteamVR çalışıyor.</string> + <string name="cardboard_steamvr_unavailable">Bu oturumda kullanılamaz.</string> + <string name="cardboard_steamvr_failed">SteamVR yanıt vermedi; oturum günlüğünü kontrol et.</string> + <string name="drawer_component_downloads">Bileşenleri indir</string> + <string name="drawer_component_downloads_hint">İndirmeler Bileşenler sayfasıyla paylaşılır. Bir sonraki oyun başlatmada kullanmak için yukarıdan indirilmiş bir sürüm seç.</string> + <string name="drawer_component_choose_download">İndirilecek sürümü seç</string> + <string name="stores_fact_developer">Geliştirici</string> + <string name="stores_fact_publisher">Yayıncı</string> + <string name="stores_fact_released">Çıkış</string> + <string name="stores_hltb_main">Ana hikâye</string> + <string name="stores_hltb_extras">+ Ekstralar</string> + <string name="stores_hltb_completionist">Tamamlayıcı</string> + <string name="stores_hltb_hours">%1$s sa</string> + <string name="stores_hltb_minutes">%1$d dk</string> +</resources> diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml new file mode 100644 index 000000000..6bee54cd2 --- /dev/null +++ b/app/src/main/res/values-uk/strings.xml @@ -0,0 +1,2285 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>Під час першого запуску завантажується Steam. Це може зайняти кілька хвилин.</item> + <item>За замовчуванням вихід із додатка ставить Steam на паузу; коли ви повернетеся, усе продовжиться з того самого місця.</item> + <item>Ваш контролер відображається як геймпад Xbox 360. Якщо контролера немає, скористайтеся екранним керуванням.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>Під час першого запуску налаштовується робочий стіл. Це може зайняти кілька хвилин.</item> + <item>За замовчуванням вихід із додатка ставить сеанс на паузу; коли ви повернетеся, усе продовжиться з того самого місця.</item> + <item>Екран працює як сенсорна панель: проводьте пальцем, щоб рухати вказівник, і торкайтеся, щоб клацнути.</item> + <item>Ваш контролер відображається як геймпад Xbox 360. Якщо контролера немає, скористайтеся екранним керуванням.</item> + </string-array> + <string name="session_channel">Сеанс</string> + <string name="session_channel_description">Відображається, поки відкрито сеанс Steam або робочого столу</string> + <string name="session_notification">Steam працює</string> + <string name="session_notification_downloads">Триває завантаження в Steam</string> + <string name="session_notification_paused">Сеанс призупинено — торкніться «Відновити», коли будете готові</string> + <string name="resume_session">Відновити</string> + <string name="suspend_now">Призупинити зараз</string> + <string name="stop_session">Завершити сеанс</string> + <string name="frame_gen_title">Генерація кадрів</string> + <string name="frame_gen_hint">Виберіть режим генерації кадрів</string> + <string name="frame_gen_off">Вимк.</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG адаптивний %1$d</string> + <string name="lsfg_needs_steam">Потрібен Lossless Scaling зі Steam</string> + <string name="lsfg_needs_install">Установіть Lossless Scaling у Steam або імпортуйте Lossless.dll</string> + <string name="lsfg_import">Імпортувати Lossless.dll…</string> + <string name="lsfg_using_steam">Використовується копія зі Steam</string> + <string name="lsfg_using_import">Використовується імпортована копія</string> + <string name="lsfg_pick_title">Виберіть Lossless.dll</string> + <string name="lsfg_imported">Lossless.dll імпортовано</string> + <string name="lsfg_import_unchanged">Цей Lossless.dll уже використовується</string> + <string name="lsfg_import_not_owned">У цьому обліковому записі Steam немає Lossless Scaling</string> + <string name="lsfg_import_invalid">Цей файл не є Lossless.dll від Lossless Scaling</string> + <string name="lsfg_import_failed">Не вдалося підготувати LSFG із цього Lossless.dll</string> + <string name="lsfg_problem_missing">Для LSFG потрібен Lossless Scaling зі Steam або імпортований Lossless.dll</string> + <string name="lsfg_problem_failed">LSFG не вдалося підготувати шейдери з Lossless.dll</string> + <string name="hud_fg_unsupported">%1$s: не підтримується</string> + <string name="hud_fg_failed">%1$s: збій рушія</string> + <string name="hud_fg_needs_lossless">%1$s: потрібен Lossless Scaling</string> + <string name="hud_fg_starting">%1$s %2$s · запуск</string> + <string name="second_library_import_hint">Додає бібліотеку Steam і шукає в ній ігри для Windows. Придбані ігри використовують свої записи в Steam, решта стають ярликами сторонніх ігор.</string> + <string name="game_env_title">Середовище гри</string> + <string name="game_env_hint">Змінні середовища для ігор Proton.</string> + <string name="game_env_edit">Змінити</string> + <string name="game_env_shared">Усі ігри Proton</string> + <string name="game_env_scope">Профіль</string> + <string name="game_env_app_id">ID додатка Steam</string> + <string name="game_env_open">Відкрити</string> + <string name="game_env_profile">Гра %1$s</string> + <string name="game_env_add">Додати змінну</string> + <string name="game_env_name">Назва змінної</string> + <string name="game_env_value">Значення</string> + <string name="game_env_save">Зберегти</string> + <string name="game_env_remove">Видалити</string> + <string name="game_env_restore">Знову успадковувати</string> + <string name="game_env_reset">Скинути перевизначення середовища</string> + <string name="game_env_unset">Не задавати в цьому профілі</string> + <string name="game_env_empty">Змінних не задано.</string> + <string name="game_env_inherited">Успадковано</string> + <string name="game_env_custom">Власна змінна</string> + <string name="game_env_error">Не вдалося завантажити або зберегти. Перевірте вільне місце й спробуйте ще раз.</string> + <string name="game_env_invalid">Укажіть коректну назву та значення до 8192 символів.</string> + <string name="game_env_unused">Стандартне середовище Proton не читає цю змінну.</string> + <string name="game_env_loading">Завантаження…</string> + <string name="game_env_feature">Перевизначення рівня функцій D3D12. 12_2 повідомляє можливості DirectX 12 Ultimate; непідтримувані функції можуть давати збої. Не задавайте для автоматичного визначення.</string> + <string name="game_env_shader">Перевизначення моделі шейдерів D3D12. Для 6_9 потрібні досить новий VKD3D і відповідні можливості GPU. Не задавайте для автоматичного визначення.</string> + <string name="game_env_cache">Залиште кешування шейдерів Mesa ввімкненим, щоб зменшити повторну компіляцію. false вмикає кешування.</string> + <string name="game_env_cache_size">Максимальний розмір кешу шейдерів Mesa, наприклад 1G. Більший кеш займає більше місця.</string> + <string name="game_env_vkd3d_config">Параметри VKD3D для D3D12. nodxr вимикає трасування променів; використовуйте лише для гри, якій це потрібно.</string> + <string name="game_env_proton_log">Установіть 1, щоб вести діагностичний журнал Proton. Журналювання може знизити продуктивність і займає місце.</string> + <string name="game_env_wined3d">Установіть 1, щоб використовувати WineD3D на основі OpenGL замість DXVK для перевірки сумісності D3D9–11.</string> + <string name="game_env_no_ntsync">Установіть 1, щоб ця гра не використовувала ntsync (droiddeck-ntsync або драйвер ядра); тоді вона працює на droiddeck-esync, якщо є пакет, інакше на droiddeck-fsync.</string> + <string name="game_env_no_esync">Установіть 1, щоб вимкнути droiddeck-esync для цієї гри; замість нього використовується droiddeck-fsync або droiddeck-ntsync, якщо fsync теж вимкнено.</string> + <string name="game_env_no_fsync">Установіть 1, щоб ця гра не використовувала droiddeck-fsync; тоді вона працює на droiddeck-esync, якщо є пакет, інакше на droiddeck-ntsync.</string> + <string name="game_env_fsync_first">Установіть 1, щоб запускати цю гру на droiddeck-fsync, або 0, щоб надавати перевагу droiddeck-esync, якщо є пакет для її Proton.</string> + <string name="game_env_xalia">Установіть 0, щоб вимкнути в цій грі помічник Proton для навігації геймпадом.</string> + <string name="game_env_hud">Оверлей DXVK для D3D9–11. fps показує частоту кадрів; якщо не задано, оверлей приховано.</string> + <string name="game_env_dxvk_config">Параметри DXVK, розділені крапкою з комою. Нові збірки обмежують частоту кадрів через dxvk.maxFrameRate; для звичайної гри краще використовувати обмеження кадрів сеансу.</string> + <string name="game_env_vkd3d_limit">Обмеження частоти кадрів D3D12. 0 вимикає обмеження.</string> + <string name="game_env_glthread">Багатопотоковість OpenGL у Mesa. true може допомогти іграм OpenGL, які впираються в CPU; перевіряйте для кожної гри окремо.</string> + <string name="sync_backend_title">Синхронізація Wine</string> + <string name="sync_backend_hint">Як ігри синхронізують свої потоки. Застосовується в наступному сеансі.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">Пресет FEX</string> + <string name="fex_next_launch">Застосовується під час наступного запуску гри.</string> + <string name="game_settings_title">Ігри</string> + <string name="fex_default_label">Стандартний FEX</string> + <string name="fex_default_detail">Використовує стандартні налаштування FEX.</string> + <string name="fex_stability_label">Стабільність</string> + <string name="fex_stability_detail">Найбезпечніші й найповільніші налаштування.</string> + <string name="fex_compatibility_label">Сумісність</string> + <string name="fex_compatibility_detail">Повний TSO і multiblock.</string> + <string name="fex_intermediate_label">Середній</string> + <string name="fex_intermediate_detail">Збалансований TSO зі зниженою точністю x87.</string> + <string name="fex_performance_label">Продуктивність</string> + <string name="fex_performance_detail">TSO повністю вимкнено; multiblock.</string> + <string name="fex_performance_tso_label">Продуктивність + TSO</string> + <string name="fex_performance_tso_detail">Налаштування «Продуктивність» з увімкненим основним TSO.</string> + <string name="fex_extreme_label">Екстрим</string> + <string name="fex_extreme_detail">Вимикає перевірки самозмінного коду. Ігри з JIT і DRM можуть не працювати.</string> + <string name="fex_extreme_tso_label">Екстрим + TSO</string> + <string name="fex_extreme_tso_detail">Налаштування «Екстрим» з увімкненим основним TSO.</string> + <string name="fex_extreme_gn_label">Екстрим-gn</string> + <string name="fex_extreme_gn_detail">«Продуктивність» із підказками TSC і метаданих. Спробуйте його перед «Екстримом».</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">«Продуктивність» з повною перевіркою коду та прихованим бітом гіпервізора.</string> + <string name="game_env_custom_value">Власне значення</string> + <string name="game_env_select_value">Виберіть значення</string> + <string name="user_apps_section">Емулятори та додатки</string> + <string name="user_apps_add">Додати</string> + <string name="user_apps_add_detail">Ваш власний додаток або гра</string> + <string name="user_apps_kind_script">Скрипт</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d%%</string> + <string name="user_apps_starting">Запуск…</string> + <string name="user_apps_added">%1$s додано</string> + <string name="user_apps_removed">%1$s видалено</string> + <string name="user_apps_back">Робочий стіл</string> + <string name="user_apps_gone">Цього додатка вже немає. Виберіть його знову на робочому столі.</string> + <string name="user_apps_open">Відкрити %1$s</string> + <string name="user_apps_remove">Видалити</string> + <string name="user_apps_remove_confirm">Натисніть ще раз, щоб видалити</string> + <string name="user_apps_stop_session">Завершіть сеанс, щоб видалити</string> + <string name="user_apps_script_linked">Запускає %1$s з його папки, яку підключено до сеансу там, де вона лежить. Видалення додатка не зачіпає цю папку.</string> + <string name="user_apps_script_copied">Запускає %1$s з копії його папки всередині DroidDeck. Видалення додатка видаляє копію, а не оригінал.</string> + <string name="user_apps_runtime_required">Спершу встановіть середовище Linux</string> + <string name="user_apps_file_gone">Файлу більше немає</string> + <string name="user_apps_no_space">Недостатньо вільного місця: для копіювання папки потрібно %1$s</string> + <string name="user_apps_copying">Копіювання %1$s</string> + <string name="user_apps_failed">Не вдалося додати додаток</string> + <string name="user_apps_github_checking">Пошук випуску %1$s</string> + <string name="user_apps_github_unreachable">Не вдалося прочитати випуски %1$s</string> + <string name="user_apps_github_no_appimage">Жоден випуск %1$s не містить AppImage для ARM64 або x86_64</string> + <string name="user_apps_downloading">Завантаження %1$s</string> + <string name="user_apps_download_failed">Помилка завантаження</string> + <string name="user_apps_update_failed">Не вдалося оновити додаток</string> + <string name="user_apps_icon_failed">Не вдалося прочитати цей значок</string> + <string name="user_apps_saved">%1$s збережено</string> + <string name="user_apps_edit">Змінити</string> + <string name="user_apps_check_updates">Перевірити оновлення</string> + <string name="user_apps_checking_updates">Перевірка оновлень…</string> + <string name="user_apps_update_to">Оновити до %1$s</string> + <string name="user_apps_up_to_date">Актуальна версія · %1$s</string> + <string name="user_apps_updated">%1$s оновлено</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">Додавання додатка</string> + <string name="add_app_tab_script">Скрипт</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">Файл .sh. Запускається зі своєї папки на весь екран через gamescope.</string> + <string name="add_app_appimage_hint">AppImage для ARM64 (aarch64) або для x86_64, що працює через FEX. Його один раз розпаковують у середовище Linux.</string> + <string name="add_app_github_hint">Установлює AppImage для ARM64 з найновішого випуску репозиторію або версію для x86_64 через FEX.</string> + <string name="app_fex_title">Використовувати FEX</string> + <string name="app_fex_auto">Авто</string> + <string name="app_fex_on">Увімк.</string> + <string name="app_fex_off">Вимк.</string> + <string name="app_fex_hint">FEX запускає програми Linux для x86 на цьому пристрої ARM64. У режимі «Авто» він вмикається, якщо програму зібрано для x86.</string> + <string name="app_fex_x86">%1$s через FEX</string> + <string name="app_fex_not_ready">Цю програму зібрано для ПК з архітектурою %1$s, і вона працює через FEX, який ще не налаштовано. Один раз відкрийте Steam, увійшовши в обліковий запис: DroidDeck завантажить FEX і потрібний йому Steam Linux Runtime, після чого програма відкриється.</string> + <string name="session_end_program">Ця програма</string> + <string name="session_end_fex_missing">%1$s зібрано для ПК x86; для запуску потрібен FEX, який ще не встановлено. Один раз запустіть Steam, увійшовши в обліковий запис, дайте йому завантажити FEX і Steam Linux Runtime, а потім запустіть це знову.</string> + <string name="add_app_flatpak_hint">ID додатка Flathub або його сторінка на Flathub. Якщо Flatpak ще не налаштовано, його буде налаштовано спочатку.</string> + <string name="add_app_file">Файл</string> + <string name="add_app_no_file">Файл не вибрано</string> + <string name="add_app_choose_file">Вибрати файл</string> + <string name="add_app_change_file">Змінити</string> + <string name="add_app_pick_script">Виберіть скрипт</string> + <string name="add_app_pick_appimage">Виберіть AppImage</string> + <string name="add_app_pick_icon">Виберіть значок</string> + <string name="add_app_repository">Репозиторій</string> + <string name="add_app_repository_placeholder">власник/репозиторій або посилання на github.com</string> + <string name="add_app_repository_invalid">Це не репозиторій GitHub</string> + <string name="add_app_flatpak_id">ID додатка</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">Це не ID додатка Flathub</string> + <string name="add_app_flatpak_bundle_hint">Або файл .flatpak для додатка, якого немає на Flathub.</string> + <string name="add_app_pick_flatpak">Виберіть файл .flatpak</string> + <string name="add_app_source">Джерело</string> + <string name="add_app_appearance">На вашому робочому столі</string> + <string name="add_app_preview_name">Новий додаток</string> + <string name="add_app_name">Назва</string> + <string name="add_app_name_placeholder">З додатка</string> + <string name="add_app_icon">Значок</string> + <string name="add_app_icon_default">Стандартний значок</string> + <string name="add_app_choose_icon">Вибрати значок</string> + <string name="add_app_clear_icon">Очистити</string> + <string name="add_app_icon_suggestions">З репозиторію</string> + <string name="add_app_icon_searching">Пошук значків…</string> + <string name="add_app_icon_reset">Стандартний</string> + <string name="edit_app_title">Змінити %1$s</string> + <string name="edit_app_save">Зберегти</string> + <string name="add_app_checking_folder">Перевірка папки…</string> + <string name="add_app_folder_linked">Папку додатка підключено до сеансу: %1$s</string> + <string name="add_app_folder_copied">Папка містить програми Linux, які не можна запускати зі спільного сховища, тому її буде скопійовано в DroidDeck (%1$s).</string> + <string name="add_app_confirm">Додати</string> + <string name="add_app_cancel">Скасувати</string> + <string name="add_app_runtime_required">Спершу встановіть середовище Linux у розділі «Налаштування».</string> + <string name="add_app_busy">Зачекайте, поки завершиться %1$s.</string> + <string name="steam_compat_label">Сумісно</string> + <string name="proton_compatible_note">Збірки з позначкою «%1$s» у меню «Сумісність» Steam мають виправлення префіксів від DroidDeck і запускають тут ігри. За замовчуванням використовується Proton Experimental ARM64 (%1$s) або Proton 11.0 ARM64 (%1$s), щойно встановлено цю новішу збірку. Власні збірки Proton від Valve без цієї позначки не можуть запустити гру на цьому пристрої.</string> + <string name="proton_installed_compatible">Установлено %1$s · %2$s</string> + <string name="common_cancel">Скасувати</string> + <string name="common_auto">Авто</string> + <string name="common_never">Ніколи</string> + <string name="common_always">Завжди</string> + <string name="common_uninstall">Видалити</string> + <string name="common_check">Перевірити</string> + <string name="common_update">Оновити</string> + <string name="common_checking">Перевірка…</string> + <string name="common_working">Виконується…</string> + <string name="common_forget">Забути</string> + <string name="common_add_ellipsis">Додати…</string> + <string name="common_use">Використати</string> + <string name="common_applies_next_session">Застосовується в наступному сеансі.</string> + <string name="mode_steam_title">Сеанс Steam</string> + <string name="mode_desktop_title">Сеанс робочого столу</string> + <string name="mode_display">Екран</string> + <string name="mode_resolution">Роздільна здатність</string> + <string name="mode_res_720">До 720p</string> + <string name="mode_res_900">До 900p</string> + <string name="mode_res_1080">До 1080p</string> + <string name="mode_res_native">Рідна</string> + <string name="mode_res_default">%1$s (за замовчуванням)</string> + <string name="mode_res_custom_value">Власна · %1$d×%2$d</string> + <string name="mode_res_custom">Власна…</string> + <string name="mode_res_note">З 720p меню можуть реагувати швидше.</string> + <string name="mode_ratio">Співвідношення сторін</string> + <string name="mode_ratio_custom">Визначається власною роздільною здатністю.</string> + <string name="mode_ratio_auto">«Авто» використовує щонайменше 16:9.</string> + <string name="mode_fps">Обмеження кадрів</string> + <string name="mode_fps_note">Обмежує весь сеанс. Екран перемикається на частоту оновлення, кратну цьому обмеженню.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">Вивід HDR10</string> + <string name="mode_hdr_unavailable">Недоступно: %1$s.</string> + <string name="mode_hdr_restart">Перезапустіть додаток, щоб застосувати.</string> + <string name="mode_drivers">Драйвери</string> + <string name="mode_gpu_drivers">Драйвери GPU</string> + <string name="mode_gpu_drivers_hint">Спільні для Steam і робочого столу. Налаштовуються на сторінці «Компоненти».</string> + <string name="mode_touch_controls">Сенсор і керування</string> + <string name="mode_touch">Сенсорне введення</string> + <string name="mode_touch_touchpad">Сенсорна панель</string> + <string name="mode_touch_direct">Пряме</string> + <string name="mode_touch_note">«Авто»: сенсорна панель на робочому столі, пряме введення в Steam.</string> + <string name="mode_osc">Екранне керування</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">«Авто» показує екранне керування, коли контролер не під’єднано.</string> + <string name="osc_steam_touch">Сенсорне керування Steam (бета)</string> + <string name="osc_style_next_session">Сенсорне керування Steam увімкнеться з наступним сеансом Steam</string> + <string name="osc_steam_layout">Розкладка сенсорного керування Steam</string> + <string name="osc_steam_layout_hint">Переміщуйте, змінюйте розмір або приховуйте сенсорні елементи керування цієї гри; зберігається в Steam</string> + <string name="mode_controller">Контролер</string> + <string name="mode_controller_hint">Яким Steam бачить ваш контролер. Застосовується в наступному сеансі.</string> + <string name="mode_controller_deck">Контролер Steam Deck</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Контролер Xbox 360</string> + <string name="mode_controller_note">Deck: кнопка швидкого доступу й гіроскоп. Steam Controller: те саме плюс датчики хвату на задніх кнопках. DualShock 4 і DualSense: гіроскоп, а трекпади працюють як їхня сенсорна панель. Xbox 360: меню швидкого доступу відкривається комбінацією Guide+A.</string> + <string name="mode_back">Кнопка Back</string> + <string name="back_menu_then_qam">Меню → QAM</string> + <string name="back_qam_then_menu">QAM → Меню</string> + <string name="mode_session">Сеанс</string> + <string name="mode_suspend">Робота у фоні</string> + <string name="mode_suspend_hint">Коли додаток приховано або екран вимикається.</string> + <string name="mode_suspend_manual">Вручну</string> + <string name="mode_suspend_native">Нативний</string> + <string name="mode_suspend_steam_note">«Авто» призупиняє сеанс і відновлює його, коли додаток знову на екрані. «Нативний» спершу готує Steam до сну. «Вручну» чекає на «Відновити». «Ніколи» — сеанс працює далі.</string> + <string name="mode_background_downloads">Не переривати завантаження</string> + <string name="mode_background_downloads_hint">Відкладає призупинення у фоні, поки Steam активно щось завантажує.</string> + <string name="mode_suspend_note">«Авто» призупиняє сеанс у фоні й відновлює, коли додаток знову на екрані. «Вручну» чекає на «Відновити». «Ніколи» — сеанс працює далі.</string> + <string name="mode_startup">Запуск</string> + <string name="mode_steam_startup">Запускати Steam разом із DroidDeck</string> + <string name="mode_steam_startup_hint">Відкриває сеанс Steam під час запуску DroidDeck.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">Пошук випусків…</string> + <string name="mode_decky_ready">Можна встановити %1$s.</string> + <string name="mode_decky_none">Сумісну збірку не знайдено. Спробуйте пізніше.</string> + <string name="mode_decky_installed">Установлено · %1$s</string> + <string name="mode_decky_update">Доступне оновлення · %1$s</string> + <string name="mode_decky_current">Актуальна версія · %1$s</string> + <string name="mode_decky_loader">Завантажувач</string> + <string name="mode_decky_install_latest">Установити останню версію</string> + <string name="mode_decky_on">Запускається разом зі Steam. Поки його ввімкнено, інші додатки на цьому пристрої можуть керувати Steam.</string> + <string name="mode_decky_off">Вимкнено. Порт віддаленого налагодження Steam залишається закритим.</string> + <string name="mode_decky_remove_title">Видалити Decky Loader?</string> + <string name="mode_decky_remove_text">Видаляє завантажувач. Ваші плагіни й налаштування зберігаються.</string> + <string name="mode_client">Клієнт</string> + <string name="mode_mangoapp">Оверлей продуктивності</string> + <string name="mode_mangoapp_hint">Вимкніть, якщо в режимі Deck ігри вилітають або показують чорний екран. Застосовується в наступному сеансі.</string> + <string name="mode_steam_repair">Відновити клієнт Steam</string> + <string name="mode_steam_repair_hint">Повторно завантажує клієнт Steam під час наступного запуску Steam. Скористайтеся цим, якщо Steam не виходить з екрана завантаження. Ігри, вхід і налаштування зберігаються.</string> + <string name="mode_steam_repair_queued">Клієнт Steam буде повторно завантажено під час наступного запуску Steam.</string> + <string name="mode_steam_repair_button">Відновити</string> + <string name="mode_added_art">Графіка</string> + <string name="mode_sgdb_key">Ключ API SteamGridDB</string> + <string name="mode_sgdb_key_yours">Ваш ключ</string> + <string name="mode_sgdb_edit">Змінити…</string> + <string name="mode_added_art_hint">Доповнює відсутню графіку зі Steam, а потім із SteamGridDB. Ваші власні cover.jpg, hero.jpg, logo.png або icon.png у папці гри мають пріоритет.</string> + <string name="summary_remove_game">Видалити %1$s</string> + <string name="mode_added_remove_confirm">Видалити?</string> + <string name="games_add">Додати гру</string> + <string name="games_add_title">Додайте гру:</string> + <string name="games_pick_exe_title">Виберіть exe-файл:</string> + <string name="games_add_unreachable">Це розташування недоступне в сеансі</string> + <string name="mode_added_exe_check">Перевірити цей exe-файл</string> + <string name="wincomp_title">Компоненти Windows</string> + <string name="wincomp_pad_hint">LB / RB — перехід між розділами.</string> + <string name="wincomp_card_none">Немає</string> + <string name="wincomp_applies">Бібліотеки DLL Windows, яких немає в Proton, зі списку компонентів. Увімкніть ті, що потрібні цій грі: вони потрапляють у власну папку Windows гри, а не до її файлів, і залишаються, хоч на якому Proton вона працює.</string> + <string name="wincomp_offline">Не вдалося отримати список компонентів. Уже завантажені компоненти можна вмикати й вимикати.</string> + <string name="wincomp_recommended">Рекомендовано для цієї гри</string> + <string name="wincomp_checking">Перевірка, що потрібно цій грі…</string> + <string name="wincomp_when_online">Доступно, коли є інтернет</string> + <plurals name="wincomp_turning_on"> + <item quantity="one">Вмикається %1$d компонент…</item> + <item quantity="few">Вмикаються %1$d компоненти…</item> + <item quantity="many">Вмикається %1$d компонентів…</item> + <item quantity="other">Вмикається %1$d компонента…</item> + </plurals> + <string name="wincomp_auto_section">Увімкнено для цієї гри</string> + <string name="wincomp_auto_steam">Steam установлює %1$s разом із %2$s (додаток %3$s)</string> + <string name="wincomp_auto_gog">Зі списку GOG для цієї гри</string> + <string name="wincomp_auto_epic">Із необхідних компонентів Epic для цієї гри</string> + <string name="wincomp_auto_amazon">Із кроків установлення Amazon для цієї гри</string> + <string name="wincomp_steam_by_name">Steam установлює %1$s разом із додатком %2$s (збіг за назвою)</string> + <string name="wincomp_suggestions">Також у списку</string> + <string name="wincomp_suggestion_reason">Є в списку %1$s; тут це не компонент</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">Гра постачається з %1$s</string> + <string name="wincomp_found_shipped">У грі є %1$s (часто працює як є)</string> + <string name="wincomp_already_here">Уже є в цій грі</string> + <string name="wincomp_no_recommendation">Ніщо в папці гри не вказує на якийсь компонент.</string> + <string name="wincomp_active">Установлено для цієї гри (%1$d)</string> + <string name="wincomp_all">Усі компоненти (%1$d)</string> + <string name="wincomp_installers">Потрібен інсталятор Windows (%1$d)</string> + <string name="wincomp_installers_note">Це окремі програми встановлення без пакета всередині, який можна розібрати, і для них ще немає запису того, що вони встановлюють. Більшість — старі середовища виконання, які вже замінює Mono у складі Proton; записи додаються в міру створення.</string> + <string name="wincomp_show">Показати</string> + <string name="wincomp_hide">Приховати</string> + <string name="wincomp_needs_installer">Запускає власну програму встановлення (запису ще немає)</string> + <string name="wincomp_unsupported">Тут поки не підтримується</string> + <string name="wincomp_loading">Завантаження компонентів…</string> + <string name="wincomp_failed">Не вдалося встановити: %1$s</string> + <string name="wincomp_downloaded">Завантажено</string> + <string name="wincomp_downloading">Завантаження %1$s</string> + <string name="wincomp_installing">Установлення: %1$s</string> + <string name="wincomp_reverting">Вимкнення…</string> + <string name="wincomp_download_size">%1$s для завантаження</string> + <string name="wincomp_next_launch">Набуде чинності під час наступного запуску гри.</string> + <string name="mode_stretch169">Розтягувати ігри 16:9</string> + <string name="mode_stretch169_hint">На екранах, ближчих до квадрата, ніж 16:9, розтягує ігри на весь екран замість смуг. Застосовується в наступному сеансі.</string> + <string name="mode_audio">Звук</string> + <string name="mode_directaudio">DirectAudio для ігор</string> + <string name="mode_directaudio_hint">Обходить PulseAudio для меншої затримки.</string> + <string name="mode_mic">Мікрофон</string> + <string name="mode_mic_hint">Для голосового чату.</string> + <string name="mode_network">Мережа</string> + <string name="mode_wifi_discovery">Пошук Wi-Fi</string> + <string name="mode_wifi_discovery_hint">Показувати в Steam назви Wi-Fi і мережі поблизу.</string> + <string name="mode_wifi_location_off">Геодані вимкнено.</string> + <string name="mode_wifi_permission_denied">Дозвіл на доступ до геоданих не надано.</string> + <string name="mode_wifi_open_settings">Відкрити налаштування</string> + <string name="mode_wifi_continue">Продовжити</string> + <string name="mode_wifi_explain_title">Увімкнути пошук Wi-Fi?</string> + <string name="mode_wifi_explain_text">Android вимагає дозволу на доступ до геоданих, щоб читати назви Wi-Fi і шукати мережі поблизу. DroidDeck використовує цей доступ, щоб показувати ці мережі в Steam. Ми не відстежуємо ваше місцезнаходження. Доступ до інтернету працює і без цього дозволу.</string> + <string name="mode_storage">Сховище ігор</string> + <string name="mode_storage_auto">Автоматично — SD-карта, якщо вставлена, плюс ваші папки Games</string> + <string name="mode_storage_auto_none">Автоматично — ваші папки Games, плюс SD-карта, якщо вставлена (зараз немає)</string> + <string name="mode_storage_internal">Лише внутрішня пам’ять</string> + <string name="mode_storage_folder">Папка: %1$s</string> + <string name="mode_second_library">Друга бібліотека</string> + <string name="mode_storage_note">Ігри, які підвантажують ресурси з SD-карти або спільного сховища, можуть працювати ривками.</string> + <string name="mode_storage_diagnostics">Діагностика сховища</string> + <string name="mode_storage_diagnostics_hint">Записує результати виділення місця та вибіркові заміри швидкості сховища в «Поділитися журналами». Поки ввімкнено, встановлення може сповільнюватися. Застосовується в наступному сеансі.</string> + <string name="mode_choose_folder">Вибрати папку…</string> + <string name="mode_custom_res_title">Власна роздільна здатність</string> + <string name="mode_custom_res_text">Замінює обмеження та співвідношення сторін. Якщо розмір не збігається з екраном, з’являться смуги.</string> + <string name="mode_width">Ширина</string> + <string name="mode_height">Висота</string> + <string name="mode_custom_res_range">Від 320×240 до 3840×2160.</string> + <plurals name="mode_added_count"> + <item quantity="one">%1$d гра</item> + <item quantity="few">%1$d гри</item> + <item quantity="many">%1$d ігор</item> + <item quantity="other">%1$d гри</item> + </plurals> + <string name="common_ok">OK</string> + <string name="common_delete">Видалити</string> + <string name="common_restore">Відновити</string> + <string name="common_download">Завантажити</string> + <string name="common_delete_named">Видалити %1$s</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Драйвер середовища</string> + <string name="comp_runtime_driver_hint">Використовується Steam, іграми та робочим столом. Застосовується в наступному сеансі.</string> + <string name="comp_import_turnip">Імпортувати zip Turnip…</string> + <string name="comp_display_driver">Драйвер відображення</string> + <string name="comp_display_driver_hint">Виводить кадри на екран в обох режимах. Перезапустіть додаток, щоб застосувати.</string> + <string name="comp_import_adrenotools">Імпортувати zip AdrenoTools…</string> + <string name="comp_never">ніколи</string> + <string name="comp_nightlies_checked">Нічні збірки перевірено %1$s</string> + <string name="comp_title">Компоненти</string> + <string name="comp_about">Про компоненти</string> + <string name="comp_check_drivers">Шукати нові драйвери</string> + <string name="comp_check_nightlies">Шукати нові пакети в нічних збірках</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">Вибраний тут Proton запускає кожну гру, для якої в Steam не вибрано власний, а зміна, зроблена в Steam, теж відображається тут. Під час оновлення Steam замінює Proton від Valve; оригінали зберігаються для кожної збірки.</string> + <string name="comp_scope">Конфігурація</string> + <string name="comp_profile_configure">Налаштувати профіль</string> + <string name="comp_profile">Профіль</string> + <string name="comp_scope_general">Основний профіль</string> + <string name="comp_scope_general_detail">Автоматично використовується іграми без перевизначень.</string> + <string name="comp_profile_game_detail">Збережені налаштування, які автоматично застосовуються під час запуску %1$s.</string> + <string name="comp_game_hint">Вибирайте лише ті налаштування, які потрібні цій грі. Усе інше й надалі береться з основного профілю.</string> + <string name="comp_game_fex">Пресет FEX</string> + <string name="comp_game_fex_inherited">Використовується пресет основного профілю.</string> + <string name="comp_game_fex_override">Для цієї гри збережено власний пресет.</string> + <string name="comp_game_fex_custom">Власні змінні FEX</string> + <string name="comp_game_use_general">Як в основному</string> + <string name="comp_game_compatibility">Сумісність гри</string> + <string name="comp_game_environment_hint">Параметри середовища Proton, DXVK, VKD3D і синхронізації для цієї гри.</string> + <string name="comp_game_proton_inherited">Використовується Proton основного профілю.</string> + <string name="comp_game_proton_override">Для цієї гри збережено власний Proton через зіставлення сумісності Steam.</string> + <string name="comp_no_proton_short">Немає Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Використовує пакет основного профілю з %1$s, якщо ця гра не вибирає інший пакет.</string> + <string name="comp_game_component_general">Використовує пакет компонентів основного профілю.</string> + <string name="comp_game_component_override">Використовує %1$s лише для цієї гри.</string> + <string name="comp_game_runtime_options">Параметри середовища виконання</string> + <string name="comp_game_environment_other_hint">Інші параметри середовища Proton, DXVK, VKD3D-Proton і синхронізації для окремої гри.</string> + <string name="comp_game_saved_next">Профіль %1$s збережено для наступного запуску гри.</string> + <string name="game_no_steam_title">Запустити без Steam</string> + <string name="game_no_steam_detail">Для гри, яка відразу закривається: запускає її без налаштування Steam від Proton. Функції Steam, як-от оверлей, для неї вимкнені.</string> + <string name="game_no_steam_on">Увімкнено</string> + <string name="comp_game_proton_live">Вибір Proton для %1$s збережено, і він уже доступний у Steam.</string> + <string name="comp_game_proton_next">Вибір Proton для %1$s збережено. Перезапустіть Steam перед запуском гри.</string> + <string name="comp_game_proton_not_runnable">%1$s не може використовувати вибраний Proton на цьому пристрої.</string> + <string name="comp_game_load_failed">Не вдалося завантажити профіль %1$s: %2$s</string> + <string name="comp_game_save_failed">Не вдалося зберегти профіль гри: %1$s</string> + <string name="comp_game_reset">Скинути профіль гри</string> + <string name="comp_game_reset_title">Скинути профіль %1$s?</string> + <string name="comp_game_reset_body">Видалити для цієї гри перевизначення Proton, FEX, пакета компонентів, середовища, фільтрації текстур і компонентів Windows? Гра використовуватиме основний профіль.</string> + <string name="comp_game_reset_done">%1$s тепер використовує основний профіль.</string> + <string name="comp_game_reset_failed">Не вдалося повністю скинути профіль гри: %1$s</string> + <string name="comp_default_live">%1$s тепер запускає кожну гру, для якої в Steam не вибрано власний Proton.</string> + <string name="comp_default_next">%1$s запускатиме кожну гру, для якої не вибрано власний Proton, починаючи з наступного запуску Steam.</string> + <string name="comp_default_not_runnable">%1$s не може запускати тут ігри, тому його не можна вибрати за замовчуванням.</string> + <string name="comp_default_failed">Не вдалося зберегти Proton за замовчуванням: %1$s</string> + <string name="comp_swapped">%1$s · замінено: %2$d</string> + <string name="comp_all_original">%1$s · усе оригінальне</string> + <string name="comp_game_running_suffix">%1$s · гра запущена</string> + <string name="comp_esync_pack">%1$s · пакет droiddeck-esync</string> + <string name="comp_esync_wanted">%1$s · пакета droiddeck-esync ще немає</string> + <string name="comp_esync_builtin">%1$s · esync вбудовано, пакет не потрібен</string> + <string name="comp_gpu_drivers">Драйвери GPU</string> + <string name="comp_reapplied">%1$s · повторно застосовано під час запуску %2$s</string> + <string name="comp_next_queued">Далі: %1$s · після закриття гри</string> + <string name="comp_in_use">Використовується %1$s: %2$s</string> + <string name="comp_waits">Очікує, доки гру буде закрито</string> + <string name="comp_applies_next_game">Застосовується під час наступного запуску гри</string> + <string name="comp_import_zip">Імпортувати .zip</string> + <string name="comp_reading">Читання версій Proton…</string> + <string name="comp_no_proton">Proton ще не встановлено. Один раз запустіть Steam, щоб завантажити його Proton для ARM64, або додайте GE-Proton, CachyOS чи DroidDeck-Proton у розділі «Налаштування».</string> + <string name="comp_original">Оригінал · %1$s</string> + <string name="comp_this_build">%1$s · ця збірка</string> + <string name="comp_earlier_build">%1$s · попередня збірка</string> + <string name="comp_installed_count">Установлено · %1$d</string> + <string name="comp_nothing_stored">Для %1$s ще нічого не збережено. Власні файли цього Proton зберігаються як його оригінал.</string> + <string name="comp_waits_long">На цьому Proton запущено гру, тож зміна чекатиме, доки її закриють.</string> + <string name="comp_applies_long">Застосовується під час наступного запуску гри.</string> + <string name="comp_swap_title">Замінити на %1$s?</string> + <string name="comp_swap_body">Помістити %1$s у %2$s? Оригінальні файли залишаться збереженими. %3$s</string> + <string name="comp_restore_title">Відновити %1$s?</string> + <string name="comp_restore_body">Повернути оригінал %1$s у %2$s? %3$s</string> + <string name="comp_delete_package_body">Видаляє пакет із додатка. Ви зможете завантажити його знову.</string> + <string name="comp_delete_original_title">Видалити цей оригінал?</string> + <string name="comp_delete_original_body">Цей оригінал %1$s походить із попередньої збірки %2$s. Оригінал поточної збірки зберігається завжди.</string> + <string name="comp_nightlies_count">У нічних збірках · %1$d</string> + <string name="comp_refresh_to_list">Оновіть, щоб побачити пакети з нічних збірок.</string> + <string name="comp_nothing_new">Нічого нового з часу останньої перевірки.</string> + <string name="comp_import_wcp">Імпортувати .wcp</string> + <string name="comp_restore_original">Відновити оригінал цього Proton</string> + <string name="comp_restore_original_body">Повернути оригінальний %2$s у %1$s?</string> + <string name="comp_swap">Замінити</string> + <string name="comp_about_text">FEX, DXVK і VKD3D-Proton для кожного Proton, на якому Steam запускає ігри.\n\nВиберіть установлений рядок, щоб підставити його. Зміна застосовується під час наступного запуску гри або після закриття запущеної гри.\n\nВаш вибір перевіряється перед кожним запуском і відновлюється, якщо щось його змінило. Оригінальні файли кожного Proton зберігаються, тож оновлення Steam ніколи їх не втратять.</string> + <string name="comp_tag_original">Оригінал</string> + <string name="comp_tag_in_use">Використовується</string> + <string name="comp_tag_new">Нове</string> + <string name="comp_tag_stored">Збережено</string> + <string name="perf_title">Продуктивність</string> + <string name="perf_lede">Ядра CPU, виправлення сеансу та ім’я хоста. Застосовується в наступному сеансі.</string> + <string name="perf_client_cores_group">Ядра клієнта Steam</string> + <string name="perf_override">Перевизначити вибір ядер Steam</string> + <string name="perf_override_hint">Прив’язує Steam, його допоміжний процес інтерфейсу та gamescope до ядер нижче.</string> + <string name="perf_client_cores">Ядра клієнта</string> + <string name="perf_client_cores_on">Ядра для Steam.</string> + <string name="perf_client_cores_off">Увімкніть перевизначення, щоб вибрати.</string> + <string name="perf_game_cores_group">Ядра для ігор</string> + <string name="perf_game_cores">Ядра для ігор</string> + <string name="perf_game_cores_hint">Ядра для ігор. Якщо вибрано всі, Android розподіляє навантаження вільно.</string> + <string name="perf_client_ui">Інтерфейс клієнта</string> + <string name="perf_glthread">Багатопотоковий GL</string> + <string name="perf_glthread_hint">Меню Steam можуть реагувати швидше.</string> + <string name="perf_zink">Zink: ліниві дескриптори</string> + <string name="perf_zink_hint">Рекомендовано для драйверів без буферів дескрипторів.</string> + <string name="perf_noglerror">Пропускати перевірку помилок GL</string> + <string name="perf_noglerror_hint">Вимикає перевірку GL для кожного виклику.</string> + <string name="perf_gsrealtime">gamescope: черга GPU реального часу</string> + <string name="perf_gsrealtime_hint">Надає пріоритет роботі композитора на GPU. Може вирівняти темп кадрів ціною частини продуктивності ігор.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">Тримати GPU на максимальній частоті</string> + <string name="perf_gpuclock_hint">Лише Adreno. Менше запинок, вища витрата заряду та нагрів.</string> + <string name="perf_fixes">Виправлення сеансу</string> + <string name="perf_sysmem">Turnip: рендеринг через sysmem</string> + <string name="perf_sysmem_hint">Швидше для Steam і більшості ігор. Потрібно для Adreno 710/720/722.</string> + <string name="perf_xalia">Пропускати помічник Steam xalia</string> + <string name="perf_fastsync">droiddeck-ntsync (експериментально)</string> + <string name="perf_fastsync_hint">Програмна заміна драйвера ntsync ядра Linux у просторі користувача, для тестування. Поки це увімкнено, Proton використовує її замість droiddeck-esync і droiddeck-fsync. Працює з Proton Experimental ARM64, GE-Proton і proton-cachyos. Застосовується в наступному сеансі.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync першим</string> + <string name="perf_fsync_hint">Запускає власний fsync Proton на стандартних файлах Proton, а futex_waitv надається в просторі користувача, тому не потрібен пакет і все працює відразу після оновлення Proton у Steam. Поки це увімкнено, ігри використовують його раніше за droiddeck-esync, окрім кількох, де Proton тримає fsync вимкненим. Застосовується в наступному сеансі.</string> + <string name="perf_esync_hint">Потоки Windows у грах чекають одне одного через eventfd і спільну пам’ять замість звернень до wineserver. Використовує пакет droiddeck-esync, створений саме для цієї збірки Proton; пакети завантажуються автоматично. Доки такого пакета немає для нової збірки Proton, його замінює droiddeck-fsync. Застосовується в наступному сеансі.</string> + <string name="perf_xalia_hint">Вимикає помічник Proton для навігації геймпадом, який споживає ресурси CPU в кожній грі.</string> + <string name="perf_seccomp">Запускати proot без seccomp</string> + <string name="perf_seccomp_hint">Може усунути помилки відсутніх системних викликів на деяких ядрах. Може знизити продуктивність.</string> + <string name="perf_fastpath">Швидкий шлях proot</string> + <string name="perf_fastpath_needs_seccomp">Потребує фільтра seccomp у proot.</string> + <string name="perf_fastpath_hint">Швидший пошук файлів і менше запинок. Вимкніть, якщо програма не може знайти свої файли.</string> + <string name="perf_identity">Ідентифікація сеансу</string> + <string name="perf_hostname">Ім’я хоста</string> + <string name="perf_hostname_hint">Ім’я, яке Steam бачить для цього пристрою. Порожнє значення відновлює %1$s.</string> + <string name="perf_hostname_invalid">Лише літери, цифри та дефіси всередині, до 63 символів.</string> + <string name="perf_phantom_warning">Android налаштовано завершувати цей сеанс</string> + <string name="driver_installed">Встановлено</string> + <string name="driver_available">Доступно для завантаження</string> + <string name="driver_nothing_new">Нічого нового з часу останньої перевірки.</string> + <string name="driver_restore_builtin">Відновити вбудовані драйвери</string> + <string name="driver_delete_builtin">Вбудовано в додаток: це видаляє його розпаковані файли та приховує його. «Відновити вбудовані драйвери» повертає його.</string> + <string name="driver_delete_bundle">Половина набору Android + Linux: видаляються обидві половини. Драйвер, що використовується, повертається до стандартного.</string> + <string name="driver_delete_plain">Його файли видаляються з додатка.</string> + <string name="driver_delete_in_use">Його файли видаляються. Він зараз використовується, тому його замінює стандартний драйвер.</string> + <string name="drawer_page_display">Екран</string> + <string name="drawer_page_effects">Ефекти</string> + <string name="drawer_page_controls">Керування</string> + <string name="drawer_page_components">Компоненти</string> + <string name="drawer_page_controller">Контролер</string> + <string name="drawer_page_settings">Налаштування</string> + <string name="drawer_page_desc">Сторінка «%1$s»</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Робочий стіл</string> + <string name="drawer_steam_menu">Меню Steam</string> + <string name="drawer_quick_access">Швидкий доступ</string> + <string name="drawer_open_qam">Відкрити меню швидкого доступу</string> + <string name="drawer_prev_page">Попередня сторінка</string> + <string name="drawer_next_page">Наступна сторінка</string> + <string name="drawer_hud">HUD продуктивності</string> + <string name="drawer_scaling">Режим масштабування</string> + <string name="drawer_scaling_sharpness">Чіткість масштабування</string> + <string name="drawer_effects">Ефекти екрана</string> + <string name="drawer_effects_hint">У реальному часі. Трохи навантажує GPU, поки увімкнено будь-який ефект.</string> + <string name="drawer_look">Пресети</string> + <string name="drawer_look_custom">Власний</string> + <string name="drawer_effects_advanced">Додатково</string> + <string name="widgets_expanded">Розгорнуто</string> + <string name="widgets_collapsed">Згорнуто</string> + <string name="drawer_cas">Чіткість (CAS)</string> + <string name="drawer_cas_level">Рівень чіткості</string> + <string name="drawer_fake_hdr">Імітація HDR</string> + <string name="drawer_deband">Усунення смугастості</string> + <string name="drawer_deband_strength">Сила усунення смугастості</string> + <string name="drawer_brightness">Яскравість</string> + <string name="drawer_contrast">Контраст</string> + <string name="drawer_gamma">Гамма</string> + <string name="drawer_saturation">Насиченість</string> + <string name="drawer_fxaa">FXAA (згладжування країв)</string> + <string name="drawer_toon">Мультиплікаційні контури</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Фільтрація текстур</string> + <string name="drawer_texture_hint">Ігри на DirectX 9–11, з наступного запуску.</string> + <string name="drawer_anisotropy">Анізотропна фільтрація</string> + <string name="drawer_texture_sharpness">Чіткість текстур</string> + <string name="drawer_texture_sharpness_note">Негативне зміщення mip-рівня: чіткіші текстури, трохи мерехтіння. «Авто» підлаштовується під те, наскільки сеанс збільшено до розміру екрана.</string> + <string name="drawer_touch_auto">Авто (%1$s)</string> + <string name="drawer_keyboard">Клавіатура</string> + <string name="drawer_pc_keyboard">Клавіатура ПК</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">Другий екран</string> + <string name="drawer_second_screen_mode">Режим</string> + <string name="drawer_second_screen_shows">Показує</string> + <string name="drawer_android_apps">Додатки Android</string> + <string name="drawer_launch_app">Запустити додаток</string> + <string name="common_hide">Приховати</string> + <string name="common_show">Показати</string> + <string name="drawer_no_apps">Немає додатків для запуску</string> + <string name="drawer_session_behavior">Поведінка сеансу</string> + <string name="drawer_suspend_hint">Застосовується зараз і до майбутніх сеансів у цьому режимі.</string> + <string name="drawer_next_session">Наступний сеанс</string> + <string name="drawer_support">Підтримка</string> + <string name="drawer_logs">Журнали сеансу</string> + <string name="drawer_logs_hint">Додайте до звіту про помилку</string> + <string name="drawer_share_logs">Поділитися журналами</string> + <string name="drawer_background">Фон</string> + <string name="drawer_stop">Завершити</string> + <string name="drawer_stop_title">Завершити сеанс?</string> + <string name="drawer_running_now">Зараз виконується</string> + <string name="drawer_no_game">Жодна гра не запущена на Proton</string> + <string name="drawer_selected_profile">Профіль гри</string> + <string name="drawer_selected_profile_none">Дані профілю ще не доступні.</string> + <string name="drawer_profile_refresh_failed_initial">Не вдалося завантажити профілі гри. Повторна спроба виконується автоматично.</string> + <string name="drawer_profile_refresh_failed_stale">Не вдалося оновити профілі гри. Показано останні доступні дані; повторна спроба виконується автоматично.</string> + <string name="drawer_profile_inspect_hint">Переглядайте та редагуйте збережені налаштування, які застосуються під час наступного запуску.</string> + <string name="drawer_profile_default">Використовується профіль «Стандартний»</string> + <string name="drawer_profile_custom">Власний профіль</string> + <string name="drawer_component_inherited">Використовується %1$s зі стандартного профілю.</string> + <string name="drawer_game_components_note">Зміни зберігаються в профілі %1$s і застосовуються під час наступного запуску гри.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="one">%1$d перевизначення гри</item> + <item quantity="few">%1$d перевизначення гри</item> + <item quantity="many">%1$d перевизначень гри</item> + <item quantity="other">%1$d перевизначення гри</item> + </plurals> + <string name="drawer_no_proton">Proton ще не встановлено.</string> + <string name="drawer_game_running">гра запущена</string> + <string name="drawer_reapplied">повторно застосовано під час запуску %1$s</string> + <string name="drawer_original">Оригінал</string> + <string name="drawer_components_note">Імпортуйте та видаляйте на сторінці «Компоненти».</string> + <string name="setup_check_done">Готово</string> + <string name="setup_check_warn">Потрібна увага</string> + <string name="setup_check_busy">Виконується</string> + <string name="setup_limit_adb_failed">Перевірте IP-адресу та порт бездротового налагодження або повторіть поєднання, якщо Android видалив цей пристрій.</string> + <string name="setup_install">Установити</string> + <string name="setup_manage">Керувати</string> + <string name="setup_tab_overview">Огляд</string> + <string name="setup_tab_controller">Контролер</string> + <string name="setup_tab_session">Сеанс</string> + <string name="setup_tab_launcher">Лаунчер</string> + <string name="setup_title">Налаштування</string> + <string name="setup_all_set">● Усе готово</string> + <string name="setup_n_ready">%1$d з %2$d готово</string> + <string name="setup_system_check">Перевірка системи</string> + <string name="setup_gpu_supported">Пристрій підтримується</string> + <string name="setup_gpu_untested">GPU не перевірено</string> + <string name="setup_gpu_unsupported">GPU не підтримується</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">Steam потребує драйвера Adreno; %1$s може показувати чорний екран</string> + <string name="setup_runtime">Середовище Linux</string> + <string name="setup_runtime_progress">%1$s · %2$d%%</string> + <string name="setup_runtime_missing">Не встановлено · приблизно 3 GB, встановлюється під час першого натискання «Грати»</string> + <string name="setup_installed">Встановлено</string> + <string name="setup_runtime_update">%1$s · є оновлення</string> + <string name="setup_runtime_current">%1$s · актуальна версія</string> + <string name="setup_limit">Обмеження дочірніх процесів</string> + <string name="setup_limit_on">Увімкнено · Android може закрити Steam. Вимкнення займає хвилину.</string> + <string name="setup_limit_unset">Не задано · Android може закрити Steam. Вимкнення займає хвилину.</string> + <string name="setup_limit_unknown">Не вдалося перевірити · вимкнення займає хвилину.</string> + <string name="setup_fix_it">Виправити</string> + <string name="setup_details">Подробиці</string> + <string name="setup_dev_options">Параметри розробника</string> + <string name="setup_use_wireless">Використати бездротове налагодження</string> + <string name="setup_check_again">Перевірити ще раз</string> + <string name="setup_limit_turn_on">Увімкнути обмеження</string> + <string name="setup_account">Обліковий запис Steam</string> + <string name="setup_signed_in_offline">Увійшли як %1$s · офлайн-режим</string> + <string name="setup_signed_in">Увійшли як %1$s</string> + <string name="setup_sign_in">Натисніть «Грати» та увійдіть у Steam</string> + <string name="setup_tools">Інструменти</string> + <string name="setup_controller">Контролер</string> + <string name="setup_controller_unavailable">Налаштування контролера недоступні.</string> + <string name="setup_session">Сеанс</string> + <string name="setup_logs">Журнали сеансу</string> + <string name="setup_logs_hint">Зберігаються після кожного сеансу</string> + <string name="setup_agent_commands">Засоби діагностики</string> + <string name="setup_latest_logs">Останні журнали сеансу</string> + <string name="setup_saved_logs">Збережені журнали сеансу</string> + <string name="setup_saved_logs_hint">Зберігає %1$d найновіших</string> + <string name="setup_clear_logs">Очистити журнали</string> + <string name="setup_offline">Офлайн-режим</string> + <string name="setup_sign_in_first">Спершу увійдіть у Steam</string> + <string name="setup_launcher">Лаунчер</string> + <string name="setup_language">Мова</string> + <string name="setup_language_hint">Steam теж використовує її, з наступного запуску.</string> + <string name="setup_language_system">Системна мова (%1$s)</string> + <string name="setup_language_system_short">Системна мова</string> + <string name="setup_app_scale">Масштаб додатка</string> + <string name="setup_app_scale_hint">Текст і елементи керування DroidDeck</string> + <string name="setup_app_scale_default">%1$d%% (стандартно)</string> + <string name="setup_theme">Тема</string> + <string name="setup_theme_hint">Вигляд лаунчера</string> + <string name="setup_home">Використовувати як головний екран</string> + <string name="setup_home_on">DroidDeck може бути додатком головного екрана</string> + <string name="setup_home_off">DroidDeck ніколи не пропонується як додаток головного екрана</string> + <string name="setup_orientation">Орієнтація</string> + <string name="setup_orientation_auto">Автоматично альбомна</string> + <string name="setup_orientation_landscape">Альбомна</string> + <string name="setup_orientation_reverse">Перевернута альбомна</string> + <string name="setup_fullscreen">Повний екран</string> + <string name="setup_fullscreen_on">Приховує рядок стану та панель навігації Android</string> + <string name="setup_fullscreen_off">Показує рядок стану та панель навігації Android</string> + <string name="setup_default_home">Стандартний додаток головного екрана</string> + <string name="setup_choose_home">Виберіть додаток головного екрана</string> + <string name="setup_choose">Вибрати</string> + <string name="setup_linux_apps">Додатки Linux (бета)</string> + <string name="setup_tool_files">Файли</string> + <string name="setup_tool_files_hint">Перегляд і керування файлами</string> + <string name="setup_tool_protons">Версії Proton</string> + <string name="setup_tool_protons_hint">Встановлення збірок Proton для ARM64</string> + <string name="setup_tool_performance">Продуктивність</string> + <string name="setup_tool_performance_hint">Розподіл ядер CPU</string> + <string name="setup_tool_roms">Папка ROMs</string> + <string name="setup_tool_roms_hint">Де зберігаються ігри для емуляторів</string> + <string name="setup_card_components">Компоненти</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Керування</string> + <string name="setup_card_controls_hint">Призначення кнопок</string> + <string name="store_tab_discover">Огляд</string> + <string name="store_tab_search">Пошук</string> + <string name="store_tab_installed">Встановлено</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Магазин</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak готовий</string> + <string name="store_needs_runtime">Спершу встановіть середовище Linux у розділі «Налаштування».</string> + <string name="store_setup_title">Налаштувати Flatpak</string> + <string name="store_setup_text">Додає Flatpak і підключає його до Flathub (приблизно 20 MB). Перший додаток також завантажує своє середовище виконання.</string> + <string name="store_setting_up">Налаштування…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Оновлення</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d%%</string> + <string name="store_starting">Запуск…</string> + <string name="store_categories">Категорії</string> + <string name="store_unreachable">Не вдалося з’єднатися з Flathub. Перевірте з’єднання й спробуйте ще раз.</string> + <string name="store_try_again">Спробувати ще раз</string> + <string name="store_search_label">Пошук у Flathub</string> + <string name="store_search_placeholder">Firefox, ігри, емулятори…</string> + <string name="store_search">Пошук</string> + <string name="store_all">Усі</string> + <string name="store_searching">Пошук…</string> + <string name="store_search_hint">Шукайте за назвою або виберіть категорію. У списку лише додатки ARM64.</string> + <string name="store_no_results">На Flathub немає відповідних додатків ARM64.</string> + <string name="store_results">Результати</string> + <string name="store_check_updates">Перевірити оновлення</string> + <string name="store_update_all">Оновити всі (%1$d)</string> + <string name="store_all_current">● Усе актуально</string> + <string name="store_no_apps">Поки немає додатків. Знайдіть їх у розділах «Огляд» або «Пошук».</string> + <string name="store_setup_first_note">Налаштуйте Flatpak, щоб встановлювати додатки.</string> + <string name="store_update_available_prefix">Є оновлення · %1$s</string> + <string name="store_open">Відкрити</string> + <string name="store_details">Подробиці</string> + <string name="store_remove">Видалити</string> + <string name="store_press_again">Натисніть ще раз, щоб видалити</string> + <string name="store_installing">Встановлення…</string> + <string name="store_install">Установити</string> + <string name="store_setup_first">Спершу налаштуйте Flatpak</string> + <string name="store_download_size">Завантаження %1$s</string> + <string name="store_installed_chip">● Встановлено</string> + <string name="store_details_failed">Не вдалося завантажити сторінку цього додатка на Flathub.</string> + <string name="store_loading">Завантаження з Flathub…</string> + <string name="store_no_arm">На Flathub немає збірки ARM64, тому встановити це тут не можна.</string> + <string name="store_screenshots">Знімки екрана</string> + <string name="store_about">Про додаток</string> + <string name="store_app_id">ID додатка</string> + <string name="store_license">Ліцензія</string> + <string name="store_runtime">Середовище виконання</string> + <string name="store_installed_size">Розмір після встановлення</string> + <string name="store_installed_size_value">%1$s (плюс спільне середовище виконання)</string> + <string name="store_website">Веб-сайт</string> + <string name="store_verified">Перевірено</string> + <string name="upd_title">Оновлення</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Перевірено %1$s</string> + <string name="upd_not_checked">Ще не перевірено</string> + <string name="upd_perm_title">Дозволити DroidDeck оновлюватися самостійно</string> + <string name="upd_perm_text">На наступному екрані увімкніть «Дозволити з цього джерела», а потім поверніться. Оновлення продовжиться самостійно.</string> + <string name="upd_open_settings">Відкрити налаштування</string> + <string name="upd_not_now">Не зараз</string> + <string name="upd_signed_status">Підписано інакше</string> + <string name="upd_signed_headline">Неможливо оновити на місці</string> + <string name="upd_signed_detail">Цю копію не підписано релізним ключем DroidDeck, тому оновлення не можуть установитися над нею. Щоб перейти на іншу, видаліть цю копію.</string> + <string name="upd_available">Є оновлення</string> + <string name="upd_blocked_status">Неможливо встановити</string> + <string name="upd_switch">Готово до переходу</string> + <string name="upd_ahead_status">Попереду стабільного</string> + <string name="upd_ahead_headline">Ви попереду стабільного каналу</string> + <string name="upd_ahead_detail">Ця збірка новіша за останню стабільну. Ви перейдете на стабільний канал з його наступним випуском.</string> + <string name="upd_gone_status">Тест завершено</string> + <string name="upd_gone_headline">Цей тест завершено</string> + <string name="upd_gone_detail">Його виправлення об’єднано або відхилено. Перейдіть на попередній канал, щоб отримувати найновіші виправлення.</string> + <string name="upd_current_status">Актуальна версія</string> + <string name="upd_current_headline">У вас найновіша версія %1$s</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Оновити%1$s</string> + <string name="upd_install_button">Установити%1$s</string> + <string name="upd_install_anyway">Усе одно встановити стабільну %1$s</string> + <string name="upd_follow_preview">Перейти на попередній канал</string> + <string name="upd_stop_session">Завершіть поточний сеанс, щоб оновити.</string> + <string name="upd_less">Згорнути</string> + <string name="upd_more">Розгорнути</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Нова попередня збірка</string> + <string name="upd_new_test">Тестова збірка PR #%1$d</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">КАНАЛ ОНОВЛЕНЬ</string> + <string name="upd_stable">Стабільний</string> + <string name="upd_stable_hint">Перевірені випуски, для більшості людей</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Попередній</string> + <string name="upd_preview_hint">Найновіші виправлення, до стабільного каналу</string> + <string name="upd_preview_current">Актуальна версія</string> + <string name="upd_tests">Тестові збірки</string> + <string name="upd_tests_hint">Спробуйте виправлення до його випуску</string> + <string name="upd_tests_none">Поки що немає</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Не зібрано для цієї копії DroidDeck</string> + <string name="upd_updated">Оновлено %1$s</string> + <string name="upd_test_name">тест PR #%1$d</string> + <string name="upd_while_ago">деякий час тому</string> + <string name="upd_just_now">щойно</string> + <plurals name="upd_tests_count"> + <item quantity="one">%1$d на спробу</item> + <item quantity="few">%1$d на спробу</item> + <item quantity="many">%1$d на спробу</item> + <item quantity="other">%1$d на спробу</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="one">Зміни з часу вашої збірки · позаду на %1$d збірку</item> + <item quantity="few">Зміни з часу вашої збірки · позаду на %1$d збірки</item> + <item quantity="many">Зміни з часу вашої збірки · позаду на %1$d збірок</item> + <item quantity="other">Зміни з часу вашої збірки · позаду на %1$d збірки</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="one">Останні зміни · %1$d збірка</item> + <item quantity="few">Останні зміни · %1$d збірки</item> + <item quantity="many">Останні зміни · %1$d збірок</item> + <item quantity="other">Останні зміни · %1$d збірки</item> + </plurals> + <string name="gate_title">Один параметр Android, який потрібно змінити</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Android зупиняє додатки, які запускають багато фонових процесів, а Steam запускає їх десятки. Доки це не змінити, ігри закриватимуться без помилки.</string> + <string name="gate_fix_for_me">Виправити за мене</string> + <string name="gate_toggle_steps">Увімкніть «Вимкнути обмеження дочірніх процесів», а потім поверніться. Залиште «Параметри розробника» увімкненими, інакше перемикач вимкнеться знову.</string> + <string name="gate_open_dev">Відкрити «Параметри розробника»</string> + <string name="gate_no_toggle">У цій версії Android немає перемикача для цього, тож DroidDeck змінює налаштування через бездротове налагодження. Займає приблизно хвилину, комп’ютер не потрібен.</string> + <string name="gate_needs_wifi">Бездротове налагодження потребує Wi-Fi. Підійде будь-яка мережа; інтернет не потрібен.</string> + <string name="gate_needs_notifications">Код поєднання приходить у сповіщенні. Дозвольте сповіщення для DroidDeck або скористайтеся розділом «Інші способи».</string> + <string name="gate_allow_notifications">Дозволити сповіщення</string> + <string name="gate_step1">Відкрийте «Налаштування» → «Параметри розробника» → «Бездротове налагодження» і увімкніть його</string> + <string name="gate_step2">Натисніть «Поєднати пристрій за кодом»</string> + <string name="gate_step3">Введіть код у сповіщення DroidDeck</string> + <string name="gate_waiting">Очікування вікна поєднання…</string> + <string name="gate_code_needed">Вікно знайдено. Введіть його код у сповіщенні.</string> + <string name="gate_status">Статус</string> + <string name="gate_polling">Перевірка кожні 2 секунди. Ця сторінка закриється, коли це вимкнено.</string> + <string name="gate_other_ways">Інші способи</string> + <string name="gate_other_toggle">Бездротове налагодження, адреса вручну, ADB з комп’ютера</string> + <string name="gate_other_no_toggle">Адреса вручну, ADB з комп’ютера, параметри розробника</string> + <string name="gate_use_wireless_instead">Використати бездротове налагодження замість цього</string> + <string name="gate_some_roms">Деякі прошивки все одно додають цей перемикач. Пошукайте «дочірній процес» у параметрах розробника.</string> + <string name="gate_manual_hint">Якщо сповіщення не працює, введіть адресу та код із вікна вручну.</string> + <string name="gate_enter_manually">Ввести адресу вручну</string> + <string name="gate_from_computer">З комп’ютера з ADB:</string> + <string name="gate_copy_command">Копіювати команду</string> + <string name="gate_choose_display">Виберіть, на якому екрані відкрити налаштування Android.</string> + <string name="gate_main_screen">Головний екран</string> + <string name="wadb_finding_port">Поєднано. Пошук порту бездротового налагодження…</string> + <string name="wadb_enter_port">Поєднано. Введіть порт із налаштувань.</string> + <string name="wadb_applying">Пристрій знайдено. Застосування налаштування…</string> + <string name="wadb_apply_failed">Не вдалося застосувати автоматично. Перевірте адресу нижче і повторіть спробу. %1$s</string> + <string name="wadb_title_wireless">Бездротове налагодження</string> + <string name="wadb_title_done">Обмеження дочірніх процесів оновлено</string> + <string name="wadb_title">Змінити обмеження дочірніх процесів</string> + <string name="wadb_lede_compact0">Якщо цього параметра немає, поєднайтеся через бездротове налагодження.</string> + <string name="wadb_lede_compact1">Поєднано. Перевірте IP-адресу і порт у бездротовому налагодженні.</string> + <string name="wadb_lede_done">Обмеження дочірніх процесів оновлено.</string> + <string name="wadb_lede0">Використайте крок 2, якщо в параметрах розробника немає налаштування дочірніх процесів. Відкрийте «Параметри розробника» → «Бездротове налагодження» → «Поєднати пристрій за кодом».</string> + <string name="wadb_lede1">Поєднано. Використайте адресу нижче або оновіть її в розділі «Бездротове налагодження» → «IP-адреса та порт».</string> + <string name="wadb_lede_done_long">Обмеження дочірніх процесів оновлено через бездротове налагодження.</string> + <string name="wadb_confirmed_on">Android підтвердив, що обмеження увімкнено.</string> + <string name="wadb_confirmed_off">Android підтвердив, що обмеження вимкнено.</string> + <string name="wadb_pair_group">2 · Поєднати бездротове налагодження</string> + <string name="wadb_pair_address">IP-адреса і порт із вікна поєднання</string> + <string name="wadb_pin">Код поєднання (PIN)</string> + <string name="wadb_pin_placeholder">6 цифр</string> + <string name="wadb_pair">Поєднати</string> + <string name="wadb_apply_group">2 · Застосувати через бездротове налагодження</string> + <string name="wadb_connect_address">IP-адреса і порт з’єднання</string> + <string name="wadb_pair_again">Поєднати ще раз</string> + <string name="wadb_turn_off">Вимкнути обмеження</string> + <string name="wadb_other_ways">Інші способи змінити це</string> + <string name="wadb_dev_try_first">1 · Параметри розробника (спробуйте спершу)</string> + <string name="wadb_restrict_on">Увімкніть «Обмежити дочірні процеси».</string> + <string name="wadb_restrict_off">Вимкніть «Обмежити дочірні процеси».</string> + <string name="wadb_computer_last">3 · ADB з комп’ютера (останній варіант)</string> + <string name="wadb_only_if">Лише якщо бездротове налагодження недоступне.</string> + <string name="wadb_computer_group">3 · ADB з комп’ютера (останній варіант)</string> + <string name="ctrl_osc_hint">Коли сенсорне керування з’являється в сеансі</string> + <string name="ctrl_color">Колір</string> + <string name="ctrl_color_hint">Відтінок екранних кнопок</string> + <string name="ctrl_custom">Власний</string> + <string name="ctrl_opacity">Непрозорість</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">Розмір кнопок</string> + <string name="ctrl_size_hint" formatted="false">100% — стандартний розмір</string> + <string name="ctrl_stick_click">Натискання стика</string> + <string name="ctrl_stick_click_hint">Двічі торкніться і затримайте стик, щоб отримати L3 або R3</string> + <string name="ctrl_adaptive">Адаптивні стики</string> + <string name="ctrl_rumble">Вібрація гри</string> + <string name="ctrl_steam_button">Кнопка Steam</string> + <string name="ctrl_qam_button">Кнопка QAM</string> + <string name="ctrl_keyboard_button">Кнопка клавіатури</string> + <string name="ctrl_select_steam">Натисніть Select, щоб відкрити меню Steam</string> + <string name="ctrl_select_steam_hint">Замість View/Select.</string> + <string name="setup_animations">Анімації</string> + <string name="ctrl_adaptive_hint">Стики з’являються там, де ви торкаєтеся, і зникають після відпускання</string> + <string name="ctrl_layout">Розкладка</string> + <string name="ctrl_layout_custom">Збережено власне розташування</string> + <string name="ctrl_layout_default">Підлаштовано під цей екран</string> + <string name="ctrl_edit">Змінити</string> + <string name="ctrl_reset">Скинути</string> + <string name="ctrl_mapping">Призначення кнопок</string> + <string name="ctrl_mapping_default">Кожна кнопка надсилає власне введення</string> + <string name="ctrl_mapping_count">Перепризначено %1$d з %2$d кнопок</string> + <string name="ctrl_configure">Налаштувати</string> + <string name="ctrl_reset_all">Скинути контролер</string> + <string name="ctrl_reset_all_hint">Відновлює всі налаштування контролера та розкладку</string> + <string name="ctrl_mapping_lede">Що кожна екранна кнопка надсилає грі. «Приховано» прибирає кнопку.</string> + <string name="ctrl_onscreen_buttons">Екранні кнопки</string> + <string name="ctrl_defaults">Стандартні</string> + <string name="ctrl_reset_mapping">Скинути призначення</string> + <string name="content_android_apps">Додатки Android</string> + <string name="content_apps_count">%1$d додатків</string> + <string name="content_no_android_apps">Не знайдено жодного додатка Android для запуску.</string> + <string name="content_desktop_installed">● Робочий стіл встановлено</string> + <string name="content_desktop_first_open">Встановлюється під час першого відкриття</string> + <string name="content_available">Доступно для встановлення</string> + <string name="content_not_installed">Не встановлено.</string> + <string name="content_not_installed_suffix">%1$s · не встановлено</string> + <string name="content_open_named">Відкрити %1$s</string> + <string name="content_games">Ігри</string> + <string name="content_choose_roms">Виберіть папку ROMs.</string> + <string name="content_retroarch_roms">Перейдіть до /root/ROMs у RetroArch.</string> + <string name="content_add_roms">Додайте ігри %1$s у ROMs/%2$s.</string> + <string name="content_install_named">Установити %1$s</string> + <string name="content_stop_to_install">Завершіть сеанс, щоб установити</string> + <string name="content_runtime_required">Потрібне середовище Linux</string> + <string name="content_loading_details">Завантаження відомостей про встановлення…</string> + <string name="content_details_unavailable">Відомості про встановлення недоступні. Спробуйте пізніше.</string> + <string name="content_needs_runtime">Спершу встановіть середовище Linux у розділі «Налаштування».</string> + <string name="content_stop_first">Завершіть поточний сеанс, щоб установлювати додатки робочого столу.</string> + <string name="content_install_into">Установлює %1$s у робочий стіл Linux.</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">Приховування вилучає його з робочого столу. Його файли залишаються в середовищі Linux.</string> + <string name="content_rom_gone">Цієї гри вже немає в папці ROMs.</string> + <string name="content_desktop_system">Робочий стіл · %1$s</string> + <string name="content_launch_in">Запустити в %1$s</string> + <string name="content_also_in">Також у %1$s</string> + <string name="content_page_gone">Цієї сторінки більше немає. Виберіть розділ зліва.</string> + <string name="content_open_desktop">Відкрити робочий стіл</string> + <string name="content_install_open_desktop">Установити й відкрити робочий стіл</string> + <string name="desktop_remove_title">Видалити робочий стіл</string> + <string name="desktop_remove_text">Видаляє робочий стіл та вбудовані в нього додатки. Ігри, додані додатки, налаштування та спільні бібліотеки залишаються.</string> + <string name="desktop_removing">Видалення робочого столу</string> + <string name="desktop_removed">Робочий стіл видалено</string> + <string name="desktop_cleaning_legacy">Видалення старих файлів LXQt</string> + <string name="desktop_removal_unknown">Оновіть DroidDeck, перш ніж видаляти цю версію робочого столу.</string> + <string name="content_linux_desktop">Робочий стіл Linux</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox і ваші емулятори</string> + <string name="games_empty">Ігри, які ви встановите в Steam, з’являться тут після сеансу.</string> + <string name="games_play_steam">Грати в Steam</string> + <string name="games_launch_settings">Налаштування запуску</string> + <string name="games_launch">Запустити</string> + <string name="games_files">Файли гри</string> + <string name="games_prefix">Префікс Proton</string> + <string name="games_saves_looking">Пошук збережень…</string> + <string name="games_saves_none">Папку збережень за назвою не знайдено. Експорт візьме всю папку користувача.</string> + <string name="games_saves_folders">%1$d папок збережень</string> + <string name="games_saves_summary">%1$s · %2$d файлів · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">Керування збереженнями</string> + <string name="games_saves">Збереження гри</string> + <string name="games_import_saves">Імпортувати збереження…</string> + <string name="games_export_gamehub">Експортувати як zip GameHub…</string> + <string name="games_export_winlator">Експортувати як zip Winlator…</string> + <string name="games_open_save_folder">Відкрити папку збережень</string> + <string name="games_never_played">Ще не грали</string> + <string name="games_runtime_busy_percent">Середовище зайняте · %1$d%%</string> + <string name="games_runtime_busy">Середовище зайняте</string> + <string name="games_runtime_first_play">Середовище встановлюється під час першого натискання «Грати»</string> + <string name="games_runtime_update">Є оновлення середовища</string> + <string name="games_runtime_ready">● Середовище готове</string> + <string name="games_last_played">Останній раз грали %1$s</string> + <string name="games_internal">Внутрішнє сховище</string> + <string name="games_added">Додана гра</string> + <string name="games_custom">Власна гра</string> + <string name="games_store_storage">Сховище ігор</string> + <string name="gpu_auto_hint">Найкраща пара для цього GPU, завжди в актуальному стані</string> + <string name="gpu_manual_hint">Виберіть пару самостійно</string> + <string name="gpu_checking">Перевірка драйверів…</string> + <string name="gpu_none_listed">Драйверів у списку ще немає. Оновіть, щоб їх знайти.</string> + <string name="gpu_hide_other">Приховати драйвери для інших GPU</string> + <string name="gpu_show_other">Показати драйвери для інших GPU</string> + <string name="gpu_advanced">Додатково</string> + <string name="gpu_set_with_display">Задається разом із драйвером дисплея: %1$s містить обидва.</string> + <string name="gpu_set_with_runtime">Задається разом із драйвером середовища: %1$s містить обидва.</string> + <string name="gpu_runtime_hint">Використовується іграми та робочим столом.</string> + <string name="gpu_runtime_default">Стандартний для середовища</string> + <string name="gpu_display_hint">Виводить кадри на екран. Застосовується після перезапуску DroidDeck.</string> + <string name="gpu_auto_by_gpu">Авто — вибирається за GPU</string> + <string name="gpu_manage">Завантажити, імпортувати або видалити…</string> + <string name="gpu_in_use_caps">ВИКОРИСТОВУЄТЬСЯ</string> + <string name="gpu_builtin">Драйвери, вбудовані в DroidDeck</string> + <string name="gpu_matched_pair">Драйвери середовища і дисплея задаються разом як узгоджена пара.</string> + <string name="gpu_recommended_for">Рекомендовано для цього GPU: %1$s.</string> + <string name="gpu_refresh_hint">Оновіть, щоб знайти драйвери для цього GPU.</string> + <string name="gpu_recommended_caps">РЕКОМЕНДОВАНО</string> + <string name="gpu_unavailable">Недоступно</string> + <string name="load_runtime">Середовище Linux</string> + <string name="load_runtime_detail">Підготовка середовища Linux</string> + <string name="load_session">Сеанс</string> + <string name="load_session_detail">Запуск відео та звуку</string> + <string name="load_steam">Клієнт Steam</string> + <string name="load_steam_detail">Перевірка Steam та його засобів сумісності</string> + <string name="load_opening_steam">Відкриття Steam</string> + <string name="load_opening_steam_detail">Очікування першого кадру Steam</string> + <string name="load_desktop">Робочий стіл</string> + <string name="load_desktop_detail">Підготовка робочого столу</string> + <string name="load_opening">Відкриття</string> + <string name="load_opening_detail">Очікування першого кадру</string> + <string name="load_starting_steam">Запуск Steam</string> + <string name="load_ended">Сеанс завершено</string> + <string name="load_step">Крок %1$d з %2$d</string> + <string name="load_steam_paused">Steam призупинено</string> + <string name="deck_tab_grips">Задні кнопки</string> + <string name="deck_tab_left">Ліва сторона</string> + <string name="deck_tab_right">Права сторона</string> + <string name="deck_tab_both">Обидві</string> + <string name="deck_tab_gamepad">Геймпад</string> + <string name="deck_surface_stick">Стік</string> + <string name="deck_surface_trackpad">Трекпад</string> + <string name="deck_surface_switch">Перемкнути між стіком і трекпадом</string> + <string name="deck_more_controls">Більше елементів керування Deck</string> + <string name="deck_panels">ПАНЕЛІ</string> + <string name="deck_close">Закрити керування на другому екрані</string> + <string name="deck_open_steam_menu">Відкрити меню Steam</string> + <string name="deck_left_upper">Ліва · верхня</string> + <string name="deck_left_lower">Ліва · нижня</string> + <string name="deck_right_upper">Права · верхня</string> + <string name="deck_right_lower">Права · нижня</string> + <string name="deck_click">Натискання</string> + <string name="deck_right_trackpad">Правий трекпад</string> + <string name="deck_left_trackpad">Лівий трекпад</string> + <string name="second_trackpad">Трекпад</string> + <string name="second_show_keyboard">Показати клавіатуру</string> + <string name="second_hide_keyboard">Сховати клавіатуру</string> + <string name="second_terminal">Термінал Linux</string> + <string name="second_terminal_not_ready">Сеанс Linux ще не готовий до роботи з терміналом.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Ігри</string> + <string name="rail_desktop">Робочий стіл</string> + <string name="rail_store">Магазин</string> + <string name="rail_components">Компоненти</string> + <string name="rail_apps">Додатки</string> + <string name="rail_setup">Налаштування</string> + <string name="rail_updates">Оновлення</string> + <string name="rail_percent">%1$d%%</string> + <string name="rail_resume_named">Продовжити %1$s</string> + <string name="roms_text">У кожному сеансі з’являється як /root/ROMs. Внутрішня пам’ять доступна в /root/Storage.</string> + <string name="roms_none">Папку не вибрано</string> + <string name="roms_applies">Діє з наступного сеансу. SD-карти підтримуються.</string> + <string name="roms_choose">Вибрати папку</string> + <string name="roms_change">Змінити папку</string> + <string name="proton_lede">Встановіть збірку Proton для ARM64, а потім виберіть її для гри в Steam → Властивості → Сумісність.</string> + <string name="proton_stop_first">Зупиніть поточний сеанс, щоб встановити або видалити інструмент сумісності.</string> + <string name="proton_available">Доступні збірки</string> + <string name="proton_installed">Встановлено %1$s</string> + <string name="proton_queued">Очікує з попереднього запиту</string> + <string name="proton_not_installed">Не встановлено</string> + <string name="proton_update_available">Доступне оновлення → %1$s</string> + <string name="proton_remove_older">Видалити старі версії %1$s</string> + <string name="proton_remove_older_body">Буде видалено %1$s. Для ігор, налаштованих на ці версії, доведеться вибрати інший Proton.</string> + <string name="proton_remove_body">Для ігор, налаштованих на цю версію, доведеться вибрати інший Proton.</string> + <string name="proton_install_now">Встановити зараз</string> + <string name="proton_cancel_queued">Прибрати з черги</string> + <string name="pckb_android">Клавіатура Android</string> + <string name="pckb_hide">Сховати ✕</string> + <string name="main_launch_named">Запустити %1$s</string> + <string name="main_choose_screen">Виберіть екран. Додатковий: %1$s.</string> + <string name="main_secondary_gone">Додатковий дисплей більше не доступний.</string> + <string name="main_primary">Основний екран</string> + <string name="main_secondary">Додатковий екран</string> + <string name="widgets_prev_tab">Попередня вкладка</string> + <string name="widgets_next_tab">Наступна вкладка</string> + <string name="widgets_on">Увімк.</string> + <string name="widgets_all_n">Усі %1$d</string> + <string name="widgets_none">Жодного</string> + <string name="widgets_n_of_m">%1$d з %2$d</string> + <string name="widgets_off">Вимк.</string> + <string name="screen_bottom">Нижній екран</string> + <string name="game_env_applies">Діє з наступного запуску гри. Профіль гри має пріоритет над спільним.</string> + <string name="game_env_other">Інша гра за ID додатка…</string> + <string name="game_env_done">Готово</string> + <string name="game_env_default">За умовчанням</string> + <string name="game_env_off">Вимк.</string> + <string name="game_env_change">Змінити…</string> + <string name="game_env_change_title">Змінити змінну</string> + <string name="game_env_restore_default">Повернути значення за умовчанням</string> + <string name="game_env_eyebrow">Ігри Proton</string> + <string name="pip_title">Картинка в картинці</string> + <string name="pip_auto">Автоматично вмикати режим PiP</string> + <string name="unfolded_controls">Керування на розгорнутому екрані</string> + <string name="unfolded_controls_hint">У розгорнутому стані гра розташована над трекпадами й задніми кнопками Steam Deck. У складеному — гра на весь екран.</string> + <string name="pip_suspended">Призупинено</string> + <string name="pip_not_ready">Режим «Картинка в картинці» стане доступним, щойно сеанс буде готовий.</string> + <string name="pip_permission">Дозвольте DroidDeck режим «Картинка в картинці» в налаштуваннях Android.</string> + <string name="pip_unavailable">Android не вдалося відкрити режим «Картинка в картинці».</string> + <string name="game_shortcut">Ярлик</string> + <string name="game_shortcut_add">Додати на головний екран</string> + <string name="game_shortcut_choose">Вибрати гру</string> + <string name="game_shortcut_scanning">Пошук встановлених ігор…</string> + <string name="game_link_copy">Копіювати посилання для запуску</string> + <string name="game_link_copied">Посилання для запуску скопійовано</string> + <string name="game_shortcut_unsupported">Цей лаунчер не підтримує ярлики. Скористайтеся пунктом «Копіювати посилання для запуску».</string> + <string name="game_link_invalid">Недійсне посилання для запуску гри.</string> + <string name="game_link_missing">Цієї гри більше немає у вашій бібліотеці DroidDeck.</string> + <string name="game_link_session_busy">Зупиніть поточний сеанс, перш ніж запускати гру Steam.</string> + <string name="game_link_failed">Не вдалося передати гру в Steam. Спробуйте запустити її ще раз.</string> + <string name="setup_retry_removal">Повторити видалення</string> + <string name="runtime_removing">Видалення</string> + <string name="runtime_removal_incomplete">Видалення не завершено</string> + <string name="game_file_export">Експортувати файл гри</string> + <string name="game_file_sync">Синхронізувати всі файли ігор із папкою</string> + <string name="game_file_stop_sync">Зупинити синхронізацію файлів ігор</string> + <string name="game_file_folder">Виберіть папку для файлів ігор для фронтендів</string> + <string name="game_file_exported">Файли ігор збережено в %1$s</string> + <string name="game_frontend_files">Файли для фронтендів</string> + + <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> + <string name="main_saves_importing">Імпорт у %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="one">Імпортовано %1$d файл з %2$s у %3$s</item> + <item quantity="few">Імпортовано %1$d файли з %2$s у %3$s</item> + <item quantity="many">Імпортовано %1$d файлів з %2$s у %3$s</item> + <item quantity="other">Імпортовано %1$d файлу з %2$s у %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="one">Імпортовано %1$d файл з %2$s у %3$s. Резервну копію старих збережень записано в Download/DroidDeck/Saves/backups</item> + <item quantity="few">Імпортовано %1$d файли з %2$s у %3$s. Резервну копію старих збережень записано в Download/DroidDeck/Saves/backups</item> + <item quantity="many">Імпортовано %1$d файлів з %2$s у %3$s. Резервну копію старих збережень записано в Download/DroidDeck/Saves/backups</item> + <item quantity="other">Імпортовано %1$d файлу з %2$s у %3$s. Резервну копію старих збережень записано в Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Виберіть zip-архів збережень для %1$s</string> + <string name="main_saves_exporting">Експорт %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="one">Експортовано %1$d файл як %2$s: %3$s</item> + <item quantity="few">Експортовано %1$d файли як %2$s: %3$s</item> + <item quantity="many">Експортовано %1$d файлів як %2$s: %3$s</item> + <item quantity="other">Експортовано %1$d файлу як %2$s: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Виберіть, куди зберегти %1$s (%2$s)</string> + <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> + <plurals name="main_logs_cleared"> + <item quantity="one">Очищено %1$d папку журналів сеансів.</item> + <item quantity="few">Очищено %1$d папки журналів сеансів.</item> + <item quantity="many">Очищено %1$d папок журналів сеансів.</item> + <item quantity="other">Очищено %1$d папки журналів сеансів.</item> + </plurals> + <string name="main_wadb_pair_failed">Не вдалося створити пару через бездротове налагодження</string> + <string name="main_wadb_command_failed">Помилка команди бездротового налагодження</string> + <string name="main_wadb_unavailable">Бездротове налагодження недоступне</string> + <string name="main_adb_copied">Команду ADB скопійовано</string> + <string name="main_pick_roms">Виберіть папку ROMs</string> + <string name="main_non_adreno_title">GPU не Adreno</string> + <string name="main_non_adreno_text">Turnip підтримує GPU Adreno. На %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> + <string name="main_applied">Застосовано: %1$s</string> + <string name="android_app_open_failed_primary">Не вдалося відкрити %1$s на основному екрані</string> + <string name="android_app_open_failed_on">Не вдалося відкрити %1$s на %2$s</string> + <string name="android_app_open_failed_display">Не вдалося відкрити %1$s на дисплеї %2$d</string> + <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> + <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> + <string name="comp_stored">Збережено %1$s</string> + <string name="comp_download_failed">Помилка завантаження: %1$s</string> + <string name="drivers_pick_linux">Виберіть драйвер для середовища Linux (zip -Linux або набір Android + Linux)</string> + <string name="drivers_pick_android">Виберіть драйвер дисплея (zip AdrenoTools або набір Android + Linux)</string> + <string name="drivers_pick_any">Виберіть zip-файл драйвера або набір Android + Linux</string> + <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">Виберіть ZIP плагіна Decky</string> + <string name="main_storage_not_usable">Не можна використати: %1$s</string> + <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> + + <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> + <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> + + <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> + + <string name="drivers_not_checked">Ще не перевірено — натисніть «Оновити», щоб пошукати нові драйвери</string> + <string name="drivers_bundle_sets_display">Android + Linux: заодно встановлює драйвер дисплея</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: заодно встановлює драйвер середовища</string> + <string name="drivers_runtime_builtin">вбудований у середовище Turnip</string> + <string name="drivers_system_vulkan">системний Vulkan: немає вбудованої збірки для цього GPU</string> + <string name="drivers_bundled">%1$s (вбудований)</string> + <string name="drivers_manual">Вручну</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">Наразі опубліковано лише одну частину</string> + <string name="drivers_pair_size">%1$.0f MB за обидва</string> + <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> + <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> + <string name="drivers_switched_to">Перемкнено на %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Перемкнено на %1$s %2$s. Драйвер дисплея запрацює після перезапуску DroidDeck.</string> + <string name="drivers_using">Використовується %1$s %2$s.</string> + <string name="drivers_using_restart">Використовується %1$s %2$s. Драйвер дисплея запрацює після перезапуску DroidDeck.</string> + <string name="drivers_not_a_driver">Це не zip-файл драйвера: не драйвер AdrenoTools, не Turnip -Linux і не набір Android + Linux</string> + <string name="drivers_import_failed">Не вдалося імпортувати: %1$s</string> + <string name="drivers_imported_unnamed">Імпортований драйвер</string> + <string name="drivers_using_bundle">%1$s використовується і як драйвер середовища, і як драйвер дисплея.</string> + <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> + <string name="drivers_release_status">Найновіші: %1$s · перевірено %2$s</string> + <string name="drivers_release_status_failed">Найновіші: %1$s · %2$s недоступно · перевірено %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="one">%1$d хв тому</item> + <item quantity="few">%1$d хв тому</item> + <item quantity="many">%1$d хв тому</item> + <item quantity="other">%1$d хв тому</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="one">%1$d год тому</item> + <item quantity="few">%1$d год тому</item> + <item quantity="many">%1$d год тому</item> + <item quantity="other">%1$d год тому</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="one">%1$d день тому</item> + <item quantity="few">%1$d дні тому</item> + <item quantity="many">%1$d днів тому</item> + <item quantity="other">%1$d дня тому</item> + </plurals> + <string name="drivers_checking_sources">Перевірка Banners-Turnip, WinNative і DroidDeck…</string> + <string name="drivers_check_failed">помилка перевірки</string> + <string name="drivers_installed_pick_either">Встановлено %1$s — виберіть його в будь-якому зі списків, щоб задати обидва драйвери</string> + <string name="drivers_installed_pick">Встановлено %1$s — виберіть його в меню</string> + + <string name="session_wait_removal">Дочекайтеся завершення видалення середовища</string> + <string name="session_title_game">Гра</string> + <string name="session_touch_auto_touchpad">тачпад</string> + <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> + <string name="session_named_paused">%1$s призупинено</string> + <string name="session_program_paused">Програму призупинено</string> + <string name="session_starting_desktop">Запуск робочого столу</string> + <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> + <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> + <string name="session_runtime_unpacking">розпакування середовища Linux</string> + <string name="session_desktop_downloading">завантаження робочого столу</string> + <string name="session_desktop_downloading_mb">завантаження робочого столу · %1$d з %2$d MB</string> + <string name="session_desktop_checking">перевірка робочого столу</string> + <string name="session_desktop_unpacking">розпакування робочого столу</string> + <string name="session_proton_downloading">завантаження Proton Experimental (ARM64)</string> + <string name="session_proton_downloading_mb">завантаження Proton Experimental (ARM64) · %1$d з %2$d MB</string> + <string name="session_proton_checking">перевірка Proton Experimental (ARM64)</string> + <string name="session_proton_unpacking">розпакування Proton Experimental (ARM64)</string> + <string name="session_runtime_catalog_unreachable">Не вдалося отримати каталог середовищ. Перевірте з’єднання й натисніть «Грати» ще раз.</string> + <string name="session_runtime_install_failed">Середовище Linux не встановилося. Перевірте його в розділі «Налаштування» й натисніть «Грати» ще раз.</string> + <string name="session_desktop_install_failed">Робочий стіл не встановився (%1$s). Перевірте з’єднання й натисніть «Робочий стіл» ще раз.</string> + <string name="session_ended_named">Роботу %1$s несподівано завершено. Додайте журнали до звіту про помилку або спробуйте ще раз.</string> + <string name="session_ended_program">Програма несподівано зупинилася. Додайте журнали до звіту про помилку або спробуйте ще раз.</string> + <string name="session_ended_desktop">Робочий стіл несподівано зупинився. Додайте журнали до звіту про помилку або спробуйте ще раз.</string> + <string name="session_ended_detail">Код завершення %1$d · %2$s</string> + <string name="session_ended_no_log">немає журналу</string> + <plurals name="session_hint_enosys"> + <item quantity="one">У журналі %1$d помилка «Function not implemented»: прискорення seccomp у proot порушує роботу допоміжної програми на цьому пристрої. Спробуйте %2$s → «%3$s» (або «%4$s») і запустіть знову.</item> + <item quantity="few">У журналі %1$d помилки «Function not implemented»: прискорення seccomp у proot порушує роботу допоміжної програми на цьому пристрої. Спробуйте %2$s → «%3$s» (або «%4$s») і запустіть знову.</item> + <item quantity="many">У журналі %1$d помилок «Function not implemented»: прискорення seccomp у proot порушує роботу допоміжної програми на цьому пристрої. Спробуйте %2$s → «%3$s» (або «%4$s») і запустіть знову.</item> + <item quantity="other">У журналі %1$d помилки «Function not implemented»: прискорення seccomp у proot порушує роботу допоміжної програми на цьому пристрої. Спробуйте %2$s → «%3$s» (або «%4$s») і запустіть знову.</item> + </plurals> + + <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> + <string name="store_cat_games">Ігри</string> + <string name="store_cat_audio_video">Аудіо й відео</string> + <string name="store_cat_graphics">Графіка</string> + <string name="store_cat_internet">Інтернет</string> + <string name="store_cat_office">Офіс</string> + <string name="store_cat_developer">Розробка</string> + <string name="store_cat_education">Освіта</string> + <string name="store_cat_science">Наука</string> + <string name="store_cat_system">Система</string> + <string name="store_cat_utilities">Утиліти</string> + <string name="store_section_popular">Популярне</string> + <string name="store_section_trending">У тренді</string> + <string name="store_section_new">Нове</string> + <string name="store_section_updated">Оновлене</string> + <string name="store_section_loading">завантаження…</string> + <string name="store_flatpak_setup">Налаштування Flatpak</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Не вдалося перевірити оновлення на Flathub</string> + <string name="content_rom_installed">встановлено</string> + <string name="content_rom_folder">папка</string> + <string name="theme_graphite">Графіт</string> + <string name="theme_graphite_detail">Графітові поверхні й синій колір кулі з логотипа для всього вибраного, у фокусі чи натиснутого.</string> + <string name="theme_paper">Папір на чорному</string> + <string name="theme_paper_detail">Основний колір — білий, а синій з іконки позначає лінії, кільця й точки.</string> + <string name="theme_icon_blue">Синій з іконки</string> + <string name="theme_icon_blue_detail">Чорне тло, білий текст і синій для всього вибраного, у фокусі чи натиснутого.</string> + <string name="theme_electric">Електричний темно-синій</string> + <string name="theme_electric_detail">Яскравіший синій на поверхнях із темно-синім відтінком замість сірого.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Пробіл</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <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> + <string name="content_system_many">Багато систем</string> + <string name="fm_drive_internal">Внутрішня пам’ять</string> + <string name="fm_drive_linux">Система Linux</string> + <string name="fm_drive_sd">SD-карта</string> + <string name="fm_drive_storage">Сховище</string> + <string name="fm_select">Вибрати</string> + <string name="fm_properties">Властивості</string> + <string name="fm_favorite_add">Додати в обране</string> + <string name="fm_favorite_remove">Вилучити з обраного</string> + <string name="fm_rename">Перейменувати</string> + <string name="fm_copy">Копіювати</string> + <string name="fm_cut">Вирізати</string> + <string name="fm_actions">Дії</string> + <string name="fm_favorites">Обране</string> + <string name="fm_pin_current">Закріпити поточну папку</string> + <string name="fm_no_favorites">Обраного ще немає — закріпіть папку через її меню ⋮, щоб швидко повертатися до неї.</string> + <string name="fm_unpin_favorite">Вилучити з обраного</string> + <string name="fm_size_zero">0 Б</string> + <string name="fm_unit_bytes">Б</string> + <string name="fm_prop_location">Розташування</string> + <string name="fm_prop_type">Тип</string> + <string name="fm_prop_size">Розмір</string> + <string name="fm_prop_modified">Змінено</string> + <string name="fm_type_folder">Папка</string> + <string name="fm_type_file">Файл</string> + <string name="fm_type_ext_file">Файл %1$s</string> + <string name="fm_read_only">Лише читання</string> + <string name="fm_read_only_hint">Не дає іграм (і Wine) перезаписати або видалити цей файл.</string> + <string name="fm_read_only_failed">Не вдалося змінити атрибут «Лише читання»</string> + <string name="fm_hidden">Прихований</string> + <string name="fm_hidden_unsupported">Це сховище не підтримує атрибут «Прихований».</string> + <string name="fm_hidden_hint">Позначає файл як прихований у Windows (атрибут «Прихований» у Wine).</string> + <string name="fm_hidden_failed">Не вдалося змінити атрибут «Прихований» у цьому сховищі</string> + <string name="fm_done">Готово</string> + <string name="fm_op_deleting">Видалення...</string> + <string name="fm_delete_failed">Не вдалося видалити</string> + <string name="fm_op_moving">Переміщення...</string> + <string name="fm_op_copying">Копіювання...</string> + <string name="fm_op_moving_item">Переміщення — %1$s</string> + <string name="fm_op_copying_item">Копіювання — %1$s</string> + <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> + <string name="fm_paste_failed">Виконано: %1$d, не вдалося: %2$d</string> + <string name="fm_paste_skipped">Виконано: %1$d, пропущено: %2$d</string> + <plurals name="fm_paste_moved"> + <item quantity="one">Переміщено %1$d елемент</item> + <item quantity="few">Переміщено %1$d елементи</item> + <item quantity="many">Переміщено %1$d елементів</item> + <item quantity="other">Переміщено %1$d елемента</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="one">Скопійовано %1$d елемент</item> + <item quantity="few">Скопійовано %1$d елементи</item> + <item quantity="many">Скопійовано %1$d елементів</item> + <item quantity="other">Скопійовано %1$d елемента</item> + </plurals> + <string name="fm_already_exists">«%1$s» уже існує</string> + <string name="fm_op_renaming">Перейменування...</string> + <string name="fm_rename_failed">Не вдалося перейменувати</string> + <string name="fm_op_creating_folder">Створення папки...</string> + <string name="fm_create_folder_failed">Не вдалося створити папку</string> + <string name="fm_new_folder">Нова папка</string> + <string name="fm_folder_name">Назва папки</string> + <string name="fm_create">Створити</string> + <string name="fm_new_name">Нова назва</string> + <string name="fm_delete_title">Видалити?</string> + <string name="fm_delete_text">Остаточно видалити «%1$s»?</string> + <plurals name="fm_delete_items_title"> + <item quantity="one">Видалити %1$d елемент?</item> + <item quantity="few">Видалити %1$d елементи?</item> + <item quantity="many">Видалити %1$d елементів?</item> + <item quantity="other">Видалити %1$d елемента?</item> + </plurals> + <string name="fm_cannot_undo">Цю дію не можна скасувати.</string> + <plurals name="fm_and_more"> + <item quantity="one">…і ще %1$d</item> + <item quantity="few">…і ще %1$d</item> + <item quantity="many">…і ще %1$d</item> + <item quantity="other">…і ще %1$d</item> + </plurals> + <string name="fm_op_deleting_batch">Видалення %1$d/%2$d — %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="one">Не вдалося видалити %1$d елемент</item> + <item quantity="few">Не вдалося видалити %1$d елементи</item> + <item quantity="many">Не вдалося видалити %1$d елементів</item> + <item quantity="other">Не вдалося видалити %1$d елемента</item> + </plurals> + <string name="fm_conflict_merge_hint">«Об’єднати» додає та замінює файли в наявній папці.</string> + <string name="fm_conflict_overwrite_hint">«Перезаписати» замінює наявний файл.</string> + <string name="fm_conflict_apply_all">Застосувати до всіх конфліктів</string> + <string name="fm_conflict_merge">Об’єднати</string> + <string name="fm_conflict_overwrite">Перезаписати</string> + <string name="fm_conflict_keep_both">Залишити обидва</string> + <string name="fm_conflict_skip">Пропустити</string> + <string name="fm_select_this_folder">Вибрати цю папку</string> + <string name="fm_back">Назад</string> + <string name="fm_drive_not_readable">%1$s: підключено, але читання зараз недоступне</string> + <string name="fm_list_view">Список</string> + <string name="fm_grid_view">Сітка</string> + <string name="fm_search">Пошук</string> + <string name="fm_sort">Сортування</string> + <string name="fm_sort_name">Назва</string> + <string name="fm_sort_date">Дата зміни</string> + <string name="fm_rows_comfortable">Просторі рядки</string> + <string name="fm_rows_compact">Компактні рядки</string> + <string name="fm_hide_hidden">Сховати приховані файли</string> + <string name="fm_show_hidden">Показати приховані файли</string> + <string name="fm_hide_favorites">Сховати обране</string> + <string name="fm_show_favorites">Показати обране</string> + <string name="fm_filter_folder">Фільтрувати цю папку</string> + <string name="fm_free_space">Вільно: %1$s</string> + <string name="fm_selected_count">Вибрано: %1$d</string> + <string name="fm_select_none">Нічого</string> + <string name="fm_select_all">Усі</string> + <string name="fm_paste">Вставити</string> + <plurals name="fm_items"> + <item quantity="one">%1$d елемент</item> + <item quantity="few">%1$d елементи</item> + <item quantity="many">%1$d елементів</item> + <item quantity="other">%1$d елемента</item> + </plurals> + <string name="fm_paste_here">Вставити %1$s сюди</string> + <string name="fm_paste_move_here">Вставити %1$s сюди (перемістити)</string> + <string name="fm_rail_title">Файли</string> + <string name="fm_rail_storage">СХОВИЩЕ</string> + <string name="fm_rail_quick">ШВИДКИЙ ДОСТУП</string> + <string name="fm_rail_favorites">ОБРАНЕ</string> + <string name="fm_rail_downloads">Завантаження</string> + <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> + <string name="fm_favorite_added">«%1$s» додано в обране</string> + <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> + <string name="ctrl_tint_steam_blue">Синій Steam</string> + <string name="ctrl_tint_sky">Небесний</string> + <string name="ctrl_tint_white">Білий</string> + <string name="ctrl_tint_violet">Фіолетовий</string> + <string name="ctrl_tint_green">Зелений</string> + <string name="ctrl_tint_amber">Бурштиновий</string> + <string name="ctrl_tint_red">Червоний</string> + <string name="ctrl_tint_pink">Рожевий</string> + <string name="ctrl_button_a">Кнопка A</string> + <string name="ctrl_button_b">Кнопка B</string> + <string name="ctrl_button_x">Кнопка X</string> + <string name="ctrl_button_y">Кнопка Y</string> + <string name="ctrl_button_lb">Лівий бампер</string> + <string name="ctrl_button_rb">Правий бампер</string> + <string name="ctrl_button_lt">Лівий тригер</string> + <string name="ctrl_button_rt">Правий тригер</string> + <string name="ctrl_button_view">Кнопка «Перегляд»</string> + <string name="ctrl_button_menu">Кнопка «Меню»</string> + <string name="ctrl_target_view">Перегляд</string> + <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> + <string name="ctrl_target_hidden">Приховано</string> + <string name="ctrl_editor_drag">Перетягніть, щоб перемістити</string> + <string name="ctrl_editor_snap">Прив’язка</string> + <string name="ctrl_editor_save">Зберегти</string> + <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> + + <string name="rtinst_removing">Видалення середовища Linux</string> + <plurals name="rtinst_removing_entries"> + <item quantity="one">Видалення середовища Linux · %1$d запис</item> + <item quantity="few">Видалення середовища Linux · %1$d записи</item> + <item quantity="many">Видалення середовища Linux · %1$d записів</item> + <item quantity="other">Видалення середовища Linux · %1$d запису</item> + </plurals> + <string name="rtinst_removed">Середовище Linux видалено</string> + <string name="rtinst_removal_failed">Не вдалося завершити видалення середовища Linux. Повторіть видалення в розділі «Налаштування».</string> + <string name="rtinst_removing_leftovers">Видалення залишків середовища</string> + <string name="rtinst_downloading">Завантаження</string> + <string name="rtinst_verifying">Перевірка</string> + <string name="rtinst_extracting">Розпакування</string> + <string name="rtinst_channel_description">Показується під час встановлення або видалення середовища Linux</string> + <string name="rtinst_title_installing">Встановлення середовища Linux</string> + <string name="rtinst_title_removing">Видалення середовища Linux</string> + + <string name="deskpkg_runtime_missing">Середовище Linux не встановлено</string> + <string name="deskpkg_no_checksum">У каталозі немає контрольної суми для %1$s</string> + <string name="deskpkg_checksum_mismatch">Контрольна сума не збігається — нічого не змінено</string> + <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> + + <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> + <string name="flatpakmgr_installing">Встановлення %1$s</string> + <string name="flatpakmgr_updating">Оновлення %1$s</string> + <string name="flatpakmgr_removing">Видалення %1$s</string> + <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> + <string name="flatpakmgr_ref_translations">перекладів</string> + <string name="flatpakmgr_ref_graphics_drivers">графічних драйверів</string> + <string name="flatpakmgr_ref_media_codecs">медіакодеків</string> + <string name="flatpakmgr_ref_runtime">середовища виконання %1$s</string> + <string name="flatpakmgr_ref_runtime_branch">середовища виконання %1$s %2$s</string> + + <string name="appimg_too_small">Файл замалий для AppImage</string> + <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">Можна імпортувати лише AppImage типу 2 (цей має тип %1$d)</string> + <string name="appimg_no_space_import">Недостатньо вільного місця: для імпорту потрібно близько %1$s</string> + <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> + <string name="appimg_extracting">Розпакування %1$s</string> + <string name="appimg_extract_failed">Не вдалося розпакувати AppImage</string> + <string name="appimg_extract_failed_detail">Не вдалося розпакувати AppImage: %1$s</string> + + <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> + <string name="deckymgr_checksum_download_failed">Не вдалося завантажити контрольну суму випуску</string> + <string name="deckymgr_checksum_missing">Контрольна сума випуску відсутня</string> + <string name="deckymgr_checksum_invalid">Контрольна сума випуску недійсна</string> + <string name="deckymgr_checksum_mismatch">Контрольна сума Decky Loader не збіглася</string> + <string name="deckymgr_not_elf">Файл випуску не є сумісним 64-бітним виконуваним файлом Linux</string> + <string name="deckymgr_session_started_loader">Під час завантаження розпочався сеанс; зупиніть його, перш ніж встановлювати Decky Loader</string> + <string name="deckymgr_loader_not_executable">Не вдалося позначити PluginLoader як виконуваний файл</string> + <string name="deckymgr_cef_failed">Не вдалося увімкнути віддалене налагодження Steam CEF</string> + <string name="deckymgr_stage_failed">Не вдалося підготувати PluginLoader</string> + <string name="deckymgr_loader_preserve_failed">Не вдалося зберегти наявний PluginLoader</string> + <string name="deckymgr_loader_activate_failed">Не вдалося активувати новий PluginLoader</string> + <string name="deckymgr_stop_session_plugin">Зупиніть активний сеанс, перш ніж встановлювати плагін</string> + <string name="deckymgr_loader_required">Встановіть Decky Loader, перш ніж імпортувати плагін</string> + <string name="deckymgr_zip_unreadable">Не вдалося прочитати вибраний zip-файл</string> + <string name="deckymgr_plugins_dir_failed">Не вдалося створити папку плагінів Decky</string> + <string name="deckymgr_extracting_zip">Розпакування zip-файлу плагіна</string> + <string name="deckymgr_zip_one_folder">Zip-файл плагіна повинен містити одну папку плагіна верхнього рівня</string> + <string name="deckymgr_zip_missing_files">У zip-файлі плагіна відсутні plugin.json, package.json або dist/index.js</string> + <string name="deckymgr_plugin_name_missing">Назва плагіна відсутня</string> + <string name="deckymgr_session_started_plugin">Під час встановлення розпочався сеанс; зупиніть його, перш ніж встановлювати плагін</string> + <string name="deckymgr_plugin_installed">Плагін встановлено · перезапустіть Steam, щоб завантажити його</string> + <string name="deckymgr_plugin_install_failed">Встановлення zip-файлу плагіна не вдалося</string> + <string name="deckymgr_zip_too_many">Zip-файл плагіна містить занадто багато елементів</string> + <string name="deckymgr_zip_invalid_path">Zip-файл плагіна містить недійсний шлях</string> + <string name="deckymgr_zip_duplicate_paths">Zip-файл плагіна містить шляхи, що повторюються</string> + <string name="deckymgr_zip_unsafe_path">Zip-файл плагіна містить небезпечний шлях</string> + <string name="deckymgr_folder_failed">Не вдалося створити папку плагіна</string> + <string name="deckymgr_zip_too_large">Zip-файл плагіна розпаковується в обсяг понад обмеження 1 GiB</string> + <string name="deckymgr_bundled_not_executable">Не вдалося позначити вбудований бінарний файл плагіна як виконуваний</string> + <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> + <string name="deckymgr_remote_hash_invalid">Віддалений бінарний файл %1$s має недійсний хеш SHA-256</string> + <string name="deckymgr_bin_dir_failed">Не вдалося створити папку bin плагіна</string> + <string name="deckymgr_downloading_binary">Завантаження бінарного файлу плагіна · %1$s</string> + <string name="deckymgr_binary_download_failed">Не вдалося завантажити бінарний файл плагіна %1$s</string> + <string name="deckymgr_binary_checksum_mismatch">Контрольна сума бінарного файлу плагіна не збіглася · %1$s</string> + <string name="deckymgr_binary_not_executable">Не вдалося позначити бінарний файл плагіна як виконуваний · %1$s</string> + <string name="deckymgr_binary_replace_failed">Не вдалося замінити бінарний файл плагіна · %1$s</string> + <string name="deckymgr_binary_install_failed">Не вдалося встановити бінарний файл плагіна · %1$s</string> + <string name="deckymgr_folder_taken">Папка плагіна належить іншому плагіну</string> + <string name="deckymgr_preserve_failed">Не вдалося зберегти наявний плагін</string> + <string name="deckymgr_activate_failed">Не вдалося активувати імпортований плагін</string> + <string name="deckymgr_settings_failed">Не вдалося оновити налаштування плагінів Decky</string> + <string name="deckymgr_restore_failed">Не вдалося відновити наявний плагін</string> + + <string name="adbfix_save_failed">Не вдалося зберегти ідентифікаційні дані бездротового налагодження</string> + <string name="adbfix_command_failed">Команда ADB завершилася помилкою (%1$d)</string> + <string name="adbfix_not_confirmed">Android не підтвердив зміну обмеження дочірніх процесів: %1$s</string> + <string name="adbfix_no_pairing">Немає збереженої прив’язки бездротового налагодження</string> + <string name="adbfix_enter_address">Введіть поточну IP-адресу та порт бездротового налагодження</string> + + <string name="adbpair_timed_out">Час очікування вікна прив’язки минув. Спробуйте ще раз із DroidDeck.</string> + <string name="adbpair_discovery_failed_code">Не вдалося знайти вікно прив’язки (%1$d). Перевірте, що Wi-Fi увімкнено.</string> + <string name="adbpair_discovery_failed">Не вдалося знайти вікно прив’язки: %1$s</string> + <string name="adbpair_six_digits">Введіть усі 6 цифр коду</string> + <string name="adbpair_pairing">Прив’язка…</string> + <string name="adbpair_code_rejected">Цей код не підійшов. Відкрийте вікно прив’язки знову і введіть новий код.</string> + <string name="adbpair_connecting">Прив’язано. Підключення…</string> + <string name="adbpair_port_not_found">Прив’язано, але порт бездротового налагодження не знайдено. Залиште бездротове налагодження увімкненим і спробуйте ще раз.</string> + <string name="adbpair_applying">Застосування налаштування…</string> + <string name="adbpair_command_failed">Помилка команди бездротового налагодження</string> + <string name="adbpair_channel_name">Налаштування бездротового налагодження</string> + <string name="adbpair_channel_description">Запитує код прив’язки бездротового налагодження, коли відкрито «Налаштування»</string> + <string name="adbpair_title_pair">Прив’язати бездротове налагодження</string> + <string name="adbpair_waiting_text">Увімкніть бездротове налагодження, потім торкніться «Прив’язати пристрій за кодом прив’язки».</string> + <string name="adbpair_waiting_long">Увімкніть бездротове налагодження, потім торкніться «Прив’язати пристрій за кодом прив’язки». Залишайтеся в «Налаштуваннях»: код з’явиться в цьому сповіщенні.</string> + <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> + <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> + + <string name="selfinst_http">Завантаження відповіло HTTP %1$d</string> + <string name="selfinst_size_mismatch">Розмір завантаження не збігся з опублікованим APK, тож його відхилено - спробуйте ще раз</string> + <string name="selfinst_checksum_mismatch">Завантаження не збіглося з контрольною сумою, тож його відхилено - спробуйте ще раз</string> + <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> + <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> + <string name="selfinst_no_signing">Завантажений APK не містить інформації про підпис</string> + <string name="selfinst_wrong_signer">Завантажений APK не підписано ключем випуску DroidDeck</string> + <string name="appupd_http">Каталог оновлень відповів HTTP %1$d</string> + + <string name="loadstate_starting">Запуск сеансу…</string> + <string name="loadstate_elapsed">%1$d:%2$02d минуло · досі триває</string> + <string name="loadstate_steam_update">Завантаження оновлення клієнта Steam · %1$d%%</string> + <string name="loadstate_steam_client">Завантаження клієнта Steam · %1$s</string> + + <string name="pextra_busy">Встановлення інструмента сумісності вже триває</string> + <string name="pextra_stop_session">Зупиніть активний сеанс, перш ніж встановлювати інструменти сумісності</string> + <string name="pextra_no_release">Не вдалося знайти випуск ARM64 для %1$s</string> + <string name="pextra_no_space">Щоб встановити %1$s, потрібно щонайменше 4 GB вільного місця плюс розмір завантаження</string> + <string name="pextra_downloading">Завантаження %1$s %2$s</string> + <string name="pextra_retrying">Повтор завантаження · спроба %1$d з %2$d</string> + <string name="pextra_interrupted">Завантаження перервано</string> + <string name="pextra_download_failed">Помилка завантаження; частковий файл збережено для відновлення спроби</string> + <string name="pextra_too_big">Завантаження перевищило розмір випуску і було видалено</string> + <string name="pextra_incomplete">Завантаження не завершилося; частковий файл збережено для відновлюваної повторної спроби</string> + <string name="pextra_verifying">Перевірка завантаження</string> + <string name="pextra_checksum_unreadable">Не вдалося прочитати контрольну суму випуску; спробуйте ще раз</string> + <string name="pextra_no_checksum">%1$s %2$s не публікує контрольну суму; нічого не встановлено</string> + <string name="pextra_checksum_mismatch">Контрольна сума не збіглася; завантаження видалено</string> + <string name="pextra_session_started">Під час завантаження розпочався сеанс; зупиніть його, перш ніж встановлювати інструменти сумісності</string> + <string name="pextra_installing">Встановлення %1$s</string> + <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> + + <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> + <string name="cmgr_not_present">відсутній</string> + <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> + <string name="cmgr_restore_queued">%1$s із %2$s повернеться до оригіналу, коли гра, що на ньому працює, закриється.</string> + <string name="cmgr_restore_done">%1$s із %2$s відновлено з оригіналу %3$s.</string> + <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> + <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> + + <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> + + <string name="look_off_desc">Нічого не застосовано.</string> + <string name="look_game_clarity">Ігрова чіткість</string> + <string name="look_game_clarity_desc">Різкість із ноткою контрасту - варіант на щодень.</string> + <string name="look_vivid">Яскравий</string> + <string name="look_vivid_desc">Колір підсилено максимально. Усунення смугастості увімкнено, щоб небо не смугастилося.</string> + <string name="look_cinematic">Кінематографічний</string> + <string name="look_cinematic_desc">Глибший контраст, холодніші кольори, згладжені краї.</string> + <string name="look_competitive">Змагальний</string> + <string name="look_competitive_desc">Підняті тіні та посилена різкість - щоб першим помічати суперників.</string> + <string name="look_adaptive_sharpen">Адаптивна різкість</string> + <string name="look_adaptive_sharpen_desc">Лише різкість, колір не змінюється.</string> + <string name="look_filmic">Фільмовий</string> + <string name="look_filmic_desc">Крива в стилі кіноплівки, трохи приглушена.</string> + <string name="look_arcade">Аркадний</string> + <string name="look_arcade_desc">Соковито та яскраво.</string> + <string name="look_retro_crt">Ретро CRT</string> + <string name="look_retro_crt_desc">Ефекти трубки CRT та аналогового сигналу разом.</string> + <string name="look_upscale_sharp">Різке масштабування</string> + <string name="look_upscale_sharp_desc">Для запуску в роздільній здатності, нижчій за панель.</string> + <string name="look_pixel_clean">Чисті пікселі</string> + <string name="look_pixel_clean_desc">Чіткі пікселі для 2D та старих ігор.</string> + <string name="look_anime_edge">Аніме-контур</string> + <string name="look_anime_edge_desc">Контури у стилі мультфільму плюс різкість.</string> + + <string name="gpuinfo_not_adreno">Не GPU Adreno</string> + <string name="gpuinfo_supported">Підтримується</string> + <string name="gpuinfo_experimental">Експериментально: його драйвери - тестові збірки</string> + <string name="gpuinfo_below_tested">Нижче за протестоване обладнання (Adreno 725 і новіші): може не запрацювати</string> + <string name="gpuinfo_unsupported">Не підтримується: DroidDeck потребує GPU Adreno (Snapdragon)</string> + + <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> + <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> + <string name="turnip_reject_library_missing">meta.json вказує %1$s, якого немає в zip-файлі</string> + <string name="turnip_reject_not_aarch64">%1$s не є 64-бітною спільною бібліотекою ELF для AArch64</string> + <string name="turnip_reject_glibc">%1$s пов’язаний з glibc - це Turnip «-Linux», який належить до розділу «Драйвер середовища Linux»</string> + <string name="lvd_no_library">У цьому zip немає libvulkan_freedreno*.so. Zip Android (AdrenoTools) або -Wayland Turnip не є драйвером середовища Linux.</string> + <string name="lvd_not_aarch64">%1$s не є 64-бітною спільною бібліотекою ELF для AArch64.</string> + <string name="lvd_not_glibc">%1$s не є драйвером glibc - він пов’язаний з libc Android. Середовищу Linux потрібен zip «-Linux»; звичайний Turnip або Turnip «-Wayland» клієнт Steam взагалі не може завантажити.</string> + <string name="bundle_no_manifest">Не є набором драйверів: немає manifest.json із вказаними драйверами android і linux</string> + <string name="bundle_entry_outside">Не є набором драйверів: %1$s вказує за його межі</string> + <string name="bundle_too_large">Не є набором драйверів: розпаковується в обсяг понад %1$d MB</string> + + <string name="gstore_sd_card">SD-карта</string> + <string name="gstore_folder">Папка</string> + + <string name="logshare_chooser">Поділитися журналами сеансу</string> + + <string name="hdr_needs_android_10">потрібен Android 10 або новіший (шари дисплея з кольоровим простором)</string> + <string name="hdr_none">цей дисплей не повідомляє про підтримку HDR</string> + <string name="hdr_not_hdr10">цей дисплей підтримує %1$s, а не HDR10</string> + + <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> + <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> + <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> + <string name="upd_fail_with_message">Оновлення не встановилося: %1$s</string> + <string name="upd_fail_generic">Оновлення не встановилося.</string> + <string name="saves_layout_zip">zip-архів %1$s</string> + <string name="saves_zip">zip-архів</string> + <string name="saves_not_launched">%1$s ще не запускали</string> + <string name="saves_none_found">Не знайдено файлів збережень для %1$s</string> + <string name="saves_not_save_zip">%1$s не містить папки drive_c, тож це не zip-архів збережень GameHub або Winlator</string> + <string name="gstore_cannot_create">не вдається створити папки в %1$s</string> + <string name="gstore_cannot_write">не вдається записати в %1$s</string> + <string name="gstore_cannot_write_detail">не вдається записати в %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">Папка недоступна</string> + <string name="gfiles_name_taken">Інший файл уже використовує %1$s</string> + <string name="gfiles_art_unavailable">Папка графіки недоступна</string> + <string name="gfiles_unrelated_inventory">Папка містить непов’язаний перелік синхронізації</string> + <string name="gfiles_remove_failed">Не вдалося видалити %1$s</string> + <string name="gfiles_outside_folder">Файл виходить за межі папки експорту</string> + <string name="cmgr_fex_unnamed">FEX (збірка без назви версії)</string> + <string name="cmgr_no_profile">немає profile.json у %1$s</string> + <string name="cmgr_not_component_package">%1$s не є пакетом компонента Linux (тип %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s вільно</string> + <string name="mode_storage_auto_short">Автоматично</string> + <string name="appupd_catalog_rejected">Каталог оновлень не пройшов перевірку (%1$s)</string> + <string name="main_adb_clip_label">Параметр дочірніх процесів DroidDeck</string> + <string name="setup_signed_in_offline_unnamed">Виконано вхід · офлайн-режим</string> + <string name="setup_signed_in_unnamed">Виконано вхід</string> + <string name="driver_tag_bundled">ВБУДОВАНО</string> + <string name="driver_tag_downloaded">ЗАВАНТАЖЕНО</string> + <string name="driver_tag_imported">ІМПОРТОВАНО</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Дисплей сеансу</string> + <string name="display_resolution">Роздільна здатність</string> + <string name="display_game_fit">Вписування гри 16:9</string> + <string name="display_game_fit_hint">Як ігри 16:9 вписуються в цей дисплей. З наступного сеансу.</string> + <string name="display_game_fit_unavailable">Виберіть «Під екран» або власний розмір, вужчий за 16:9.</string> + <string name="display_preserve">Зберігати пропорції</string> + <string name="display_stretch">Розтягнути на весь дисплей</string> + <string name="display_image_scaling">Масштабування зображення</string> + <string name="display_filter">Фільтр масштабування</string> + <string name="display_filter_hint">Спільний для Steam і Робочого столу.</string> + <string name="display_filter_live">Застосовується відразу. Спільний для Steam і Робочого столу.</string> + <string name="display_filter_note">FSR, GSR і NIS застосовуються під час збільшення зображення сеансу. «Лише різкість» працює і без збільшення.</string> + <string name="display_sharpness">Різкість фільтра</string> + <string name="display_frame_rate">Частота кадрів</string> + <string name="display_look_filter">%1$s Встановлює масштабування на %2$s.</string> + <string name="display_match_screen">Під екран (%1$d×%2$d)</string> + <string name="display_follow_screen">Слідувати за екраном (змінює розмір при згортанні чи розгортанні)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Показати відомості про пристрій</string> + <string name="device_info_hide">Сховати відомості про пристрій</string> + <string name="device_info_loading">Читання даних пристрою…</string> + <string name="device_info_copy">Копіювати</string> + <string name="device_info_copy_hint">Скопіюйте ці відомості у звіт про помилку.</string> + <string name="device_info_copied">Скопійовано.</string> + <string name="force_ssbs_title">Примусовий SSBS</string> + <string name="force_ssbs_hint">Утримує увімкненим налаштування швидкості CPU, яке Wine постійно вимикає. Може прискорити ігри Windows на Snapdragon 8 Elite; на старших чипах зміни майже непомітні. Застосовується під час наступного запуску гри.</string> + + <string name="rail_stores">Магазини</string> + <string name="source_steam">Steam</string> + <string name="source_added">Власна</string> + <string name="setup_stores">Магазини</string> + <string name="setup_stores_open_on">Відкривати магазин на</string> + <string name="setup_stores_speed">Рівень швидкості завантаження</string> + <string name="setup_stores_engine">Рушій магазинів</string> + <string name="setup_stores_engine_chip_ready">● Рушій готовий</string> + <string name="setup_stores_engine_chip_missing">Не зібрано</string> + <string name="stores_tier_balanced">Збалансований</string> + <string name="stores_tier_fast">Швидкий</string> + <string name="stores_tier_max">Максимальний</string> + <string name="stores_title">Магазини</string> + <string name="stores_settings">Налаштування магазинів</string> + <string name="stores_settings_eyebrow">Налаштування</string> + <string name="stores_chip_signed_out">(вхід не виконано)</string> + <string name="stores_tab_store">Магазин</string> + <string name="stores_tab_installed">Встановлені</string> + <string name="stores_tab_library">Бібліотека</string> + <string name="stores_tab_all">Усі</string> + <string name="stores_tab_downloads">Завантаження</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Увійти</string> + <string name="stores_signout_named">%1$s · вийти</string> + <string name="stores_signout_title">Вийти з %1$s?</string> + <string name="stores_signout_note">Встановлені ігри залишаються. Бібліотеку буде отримано знову, коли ви увійдете знову.</string> + <string name="stores_signout_stay">Не виходити</string> + <string name="stores_signout_go">Вийти</string> + <string name="stores_search_catalog">Пошук у каталозі %1$s…</string> + <string name="stores_search_installed">Пошук встановлених ігор…</string> + <string name="stores_search_library">Пошук у вашій бібліотеці…</string> + <string name="stores_search_open">Пошук</string> + <string name="stores_installed_empty">Нічого не встановлено</string> + <string name="stores_library_loading">Завантаження</string> + <string name="stores_nothing_matches">Немає ігор</string> + <string name="stores_shelves_loading">Завантаження магазину…</string> + <string name="stores_amazon_no_catalog">Немає каталогу</string> + <string name="stores_shelf_new">Нове</string> + <string name="stores_shelf_deals">Знижки</string> + <string name="stores_shelf_free">Безкоштовні ігри</string> + <string name="stores_shelf_trending">Популярне</string> + <string name="stores_shelf_library">Ваша бібліотека</string> + <string name="stores_play">Грати</string> + <string name="stores_install">Встановити</string> + <string name="stores_resume_install">Продовжити встановлення</string> + <string name="stores_dl_interrupted">Перервано</string> + <string name="stores_install_size">Встановити · %1$s</string> + <string name="stores_get_free">Отримати безкоштовно</string> + <string name="stores_view_on">Переглянути на %1$s</string> + <string name="stores_buy_price">Купити · %1$s</string> + <string name="stores_installed_chip">Встановлено</string> + <string name="stores_in_library">У бібліотеці</string> + <string name="stores_free">Безкоштовно</string> + <string name="stores_resume">Продовжити</string> + <string name="stores_refresh">Оновити</string> + <string name="stores_show_mature">Показувати контент для дорослих</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Офлайн</string> + <string name="epic_card_none">Вимк.</string> + <string name="epic_eos">Вхід Epic (EOS)</string> + <string name="epic_offline">Запускати офлайн</string> + <string name="epic_resolve">Виправити вхід Epic</string> + <string name="store_cloud_saves">Хмарні збереження</string> + <string name="cloud_upload">Завантажити в хмару</string> + <string name="cloud_download">Завантажити з хмари</string> + <string name="cloud_none">Немає хмарних збережень</string> + <string name="cloud_conflict">Конфлікт у хмарі</string> + <string name="cloud_conflict_files">Конфлікт у хмарі · %1$d файлів</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Залишити хмарну</string> + <string name="cloud_keep_local">Залишити локальну</string> + <string name="cloud_checking">Перевірка хмари…</string> + <string name="cloud_last_sync">Остання синхронізація %1$s</string> + <string name="cloud_never">Ще не синхронізовано</string> + <string name="cloud_done">%1$d файлів</string> + <string name="stores_eyebrow_installed">встановлено</string> + <string name="stores_eyebrow_library">у вашій бібліотеці</string> + <string name="stores_eyebrow_store">магазин</string> + <string name="achievements_title">Досягнення</string> + <string name="achievements_count">%1$d з %2$d відкрито</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Відкрито · %1$d</string> + <string name="achievements_group_locked">Заблоковано · %1$d</string> + <string name="achievements_group_hidden">Сховано · %1$d</string> + <string name="achievements_hidden">Сховане досягнення</string> + <string name="achievements_hidden_detail">Деталі з\'являться після відкриття.</string> + <string name="achievements_all">Усі досягнення відкрито</string> + <string name="achievements_reading">Завантаження…</string> + <string name="achievements_signed_out">Увійдіть у %1$s, щоб побачити свої досягнення.</string> + <string name="achievements_unreachable">Не вдалося з\'єднатися з %1$s. Перевірте ще раз, коли будете онлайн.</string> + <string name="achievements_none">%1$s не має досягнень для цієї гри.</string> + <string name="achievements_show_all">Показати всі %1$d</string> + <string name="achievements_dialog_title">Досягнення · %1$s</string> + <string name="stores_in_steam">✓ У Steam</string> + <string name="stores_uninstall">Видалити</string> + <string name="stores_uninstalling">Видалення…</string> + <string name="stores_uninstall_title">Видалити %1$s?</string> + <string name="stores_uninstall_body_size">Звільнить %1$s і видалить гру зі Steam.</string> + <string name="stores_uninstall_body">Видалить її файли та ярлик у Steam.</string> + <string name="stores_launch_not_registered">Steam побачить гру під час наступного запуску; запустіть її звідти або відкрийте Steam зараз.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Вибір Steam для кожної гри</string> + <string name="stores_card_audio">Звук</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Компоненти</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Рушій</string> + <string name="stores_card_engine_value">%1$s · нативний</string> + <string name="stores_card_engine_java">%1$s · вбудований</string> + <string name="stores_how_it_runs">Як це працює</string> + <string name="stores_kv_fetched">Отримано з</string> + <string name="stores_kv_fetched_value">%1$s, з вашим обліковим записом</string> + <string name="stores_kv_installed_to">Встановлено в</string> + <string name="stores_kv_installed_to_value">Сховище ігор (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Запускається через</string> + <string name="stores_kv_launched_by_value">Steam і Proton, так само як ігри Steam</string> + <string name="stores_kv_launched_by_epic">Steam і Proton · вхід Epic під час кожного запуску, оверлей EOS вимкнено</string> + <string name="stores_kv_launched_by_amazon">Steam і Proton · встановлено змінні Amazon Games Services</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">додається до Steam під час встановлення (інтерфейс Deck + Steam Input)</string> + <string name="stores_downloads_empty">Немає завантажень</string> + <string name="stores_dl_paused">Призупинено</string> + <string name="stores_dl_queued">У черзі</string> + <string name="stores_dl_queued_at">У черзі · #%1$d</string> + <string name="stores_dl_cancelled">Скасовано</string> + <string name="stores_dl_failed">Помилка</string> + <string name="stores_dl_failed_reason">Помилка · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Пауза</string> + <string name="stores_dl_resume">Продовжити</string> + <string name="stores_dl_cancel">Скасувати</string> + <string name="stores_dl_retry">Повторити</string> + <string name="stores_dl_clear">Очистити</string> + <string name="stores_dl_clear_all">Очистити завершені</string> + <string name="stores_stage_manifest">Маніфест</string> + <string name="stores_stage_download">Завантаження</string> + <string name="stores_stage_verify">Перевірка</string> + <string name="stores_stage_install">Встановлення</string> + <string name="stores_stage_done">Готово</string> + <string name="stores_stage_checking">Перевірка</string> + <string name="stores_stage_downloading">Завантаження</string> + <string name="stores_dl_cancel_confirm">Видалити завантаження?</string> + <string name="stores_dl_failed_kept">Помилка · %1$s збережено</string> + <string name="stores_stage_verifying">Перевіряється</string> + <string name="stores_stage_installing">Встановлюється</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d файлів</string> + <string name="stores_download_manager">Менеджер завантажень</string> + <string name="stores_parallel">Одночасних завантажень</string> + <string name="stores_install_where">Встановити в</string> + <string name="stores_install_title">Встановити %1$s:</string> + <string name="stores_target_internal">Внутрішня пам’ять</string> + <string name="stores_target_free">%1$s вільно</string> + <string name="stores_notification_channel">Завантаження магазинів</string> + <string name="stores_notification_channel_hint">Показує перебіг завантаження ігор і підтримує завантаження у фоновому режимі</string> + <string name="stores_notification_one">Завантаження</string> + <string name="stores_notification_many">%1$d завантажень</string> + <string name="stores_notification_waiting">Підготовка…</string> + <string name="stores_notification_done_channel">Завершені завантаження магазинів</string> + <string name="stores_notification_installed">%1$s встановлено</string> + <string name="stores_notification_failed">Помилка завантаження %1$s</string> + <string name="stores_notification_delete">Видалити</string> + <string name="stores_notification_keep">Залишити</string> + <string name="stores_login_title">Увійти в %1$s</string> + <string name="stores_login_provider_title">Увійдіть через свого провайдера</string> + <string name="stores_login_reload">Перезавантажити</string> + <string name="stores_login_close">Закрити</string> + <string name="stores_login_retry">Спробувати ще раз</string> + <string name="stores_login_finishing">Завершення входу…</string> + <string name="stores_login_error_network">Не вдалося завантажити сторінку входу: %1$s</string> + <string name="stores_login_error_idp_blocked">Цей провайдер не дозволяє вхід із вбудованої сторінки. Увійдіть за допомогою пароля магазину.</string> + <string name="stores_login_error_http">Сторінка входу відповіла помилкою (HTTP %1$d).</string> + <string name="stores_login_error_ssl">З’єднання зі сторінкою входу не захищене.</string> + <string name="stores_login_error_verification">Не вдалося підтвердити вхід. Спробуйте ще раз.</string> + <string name="stores_login_error_generic">Вхід не вдався. Спробуйте ще раз.</string> + <string name="common_close">Закрити</string> + <string name="common_back">Назад</string> + <string name="common_edit">Редагувати</string> + <string name="games_add_all">Додати всі ігри в цій папці</string> + <string name="games_added_none">Нічого нового в цій папці</string> + <string name="edit_name">Назва</string> + <string name="edit_target">Ціль</string> + <string name="edit_start_in">Робоча папка</string> + <string name="edit_launch_options">Параметри запуску</string> + <string name="edit_artwork">Графіка</string> + <string name="edit_other_exe">Інший exe…</string> + <string name="edit_remove">Вилучити</string> + <string name="art_cover">Обкладинка</string> + <string name="art_background">Фон</string> + <string name="art_logo">Логотип</string> + <string name="art_icon">Значок</string> + <string name="art_src_folder">Папка гри</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">Ваш файл</string> + <string name="art_src_exe">З exe-файлу</string> + <string name="art_src_auto">Автоматично</string> + <string name="art_pick_image">Вибрати зображення…</string> + <string name="art_pick_title">Виберіть зображення: %1$s</string> + <string name="art_reset">Скинути до автоматичного</string> + <plurals name="games_added_n"> + <item quantity="one">Додано %1$d гру</item> + <item quantity="few">Додано %1$d гри</item> + <item quantity="many">Додано %1$d ігор</item> + <item quantity="other">Додано %1$d гри</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="one">%1$d вже в Іграх</item> + <item quantity="few">%1$d вже в Іграх</item> + <item quantity="many">%1$d вже в Іграх</item> + <item quantity="other">%1$d вже в Іграх</item> + </plurals> + <string name="hop_back">B · Назад до %1$s</string> + <string name="hop_from_games">гри</string> + <string name="hop_from_play">налаштувань запуску</string> + <string name="sheet_graphics_drivers">Графічні драйвери</string> + <string name="comp_this_proton">Цей Proton</string> + <string name="comp_every_proton">Кожен Proton</string> + <string name="comp_for_every_proton">Для кожного Proton</string> + <string name="comp_available_count">Доступно · %1$d</string> + <string name="comp_checked">Перевірено %1$s</string> + <string name="comp_check_all">Перевірити наявність нових компонентів і драйверів</string> + <string name="comp_get">Отримати</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton і FEX нижче належать цьому Proton</string> + <string name="comp_device_tested">%1$s · протестовано</string> + <string name="comp_device_untested">%1$s · не протестовано</string> + <string name="comp_device_unsupported">%1$s · не підтримується</string> + <string name="gpu_auto_keeps">Підтримує рекомендовану пару актуальною</string> + <string name="gpu_bar_auto">Авто · %1$s</string> + <string name="gpu_separately">Драйвери середовища і дисплея окремо</string> + <string name="gpu_use_title">Використати %1$s?</string> + <string name="proton_remove_title">Видалити %1$s?</string> + <string name="setup_tab_diagnostics">Діагностика</string> + <string name="setup_ready">Готово до гри</string> + <string name="setup_not_ready">Ще не готово до гри</string> + <string name="setup_ready_runtime">середовище %1$s</string> + <string name="setup_ready_limit_off">обмеження дочірніх процесів вимкнено</string> + <string name="setup_ready_signed_in">вхід виконано як %1$s</string> + <string name="setup_needs_you">Коли потрібна ваша увага</string> + <string name="setup_group_look">Вигляд</string> + <string name="setup_group_device">Цей пристрій</string> + <string name="setup_group_art">Графіка бібліотеки</string> + <string name="flathub_eyebrow">ПРОГРАМИ LINUX</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Програми для стільниці Linux, які відкриваються з Робочого столу.</string> + <string name="flathub_setup">Налаштувати Flathub</string> + <string name="flathub_remove_title">Видалити %1$s?</string> + <string name="flathub_remove_note">Його налаштування та файли зберігаються.</string> + <string name="flathub_keep">Залишити</string> + <string name="desktop_size">Розмір на диску</string> + <string name="desktop_uninstall_title">Видалити Робочий стіл?</string> + <string name="desktop_all_systems">Усі</string> + <string name="gog_achievements">Досягнення GOG</string> + <string name="gog_achievements_hint">Синхронізує досягнення для підтримуваних ігор GOG. Застосується під час наступного запуску.</string> + <string name="gog_achievements_off">Досягнення вимкнено</string> + <string name="gog_achievements_on">Досягнення увімкнено</string> + <string name="gog_achievements_signin">Увійдіть у GOG знову</string> + <string name="gog_achievements_uploaded">Досягнення завантажено</string> + <string name="gog_achievements_pending">Завантаження досягнення очікує</string> + <string name="gog_achievements_enable_store">Увімкніть досягнення GOG у налаштуваннях магазинів</string> + <string name="gog_achievements_unavailable">Досягнення недоступні; перевірте журнали сесії</string> + <string name="achievement_unlocked">Досягнення відкрито</string> + <string name="touch_paste">Вставити</string> + <string name="touch_dpad">Хрестовина</string> + <string name="touch_left_stick">Лівий стик</string> + <string name="touch_right_stick">Правий стик</string> + <string name="touch_menu">Меню</string> + <string name="touch_keyboard">Клавіатура</string> + <string name="touch_dpad_up">Хрестовина вгору</string> + <string name="touch_dpad_down">Хрестовина вниз</string> + <string name="touch_dpad_left">Хрестовина вліво</string> + <string name="touch_dpad_right">Хрестовина вправо</string> + <string name="touch_title">Сенсорне керування</string> + <string name="touch_controller">Сенсорний контролер</string> + <string name="touch_mouse">Миша</string> + <string name="touch_on">Увімк.</string> + <string name="touch_off">Вимк.</string> + <string name="touch_speed_slow">Повільна</string> + <string name="touch_speed_normal">Звичайна</string> + <string name="touch_speed_fast">Швидка</string> + <string name="touch_edit_layout">Змінити розкладку</string> + <string name="touch_droiddeck_menu">Меню DroidDeck</string> + <string name="touch_drag_hint">Перетягніть елементи керування на екран</string> + <string name="touch_arrange">Розташувати</string> + <string name="touch_discard">Відхилити</string> + <string name="touch_saving">Збереження…</string> + <string name="touch_icon">Значок</string> + <string name="touch_narrower">Ш−</string> + <string name="touch_wider">Ш+</string> + <string name="touch_shorter">В−</string> + <string name="touch_taller">В+</string> + <string name="touch_hide">Сховати</string> + <string name="touch_color_opacity">Колір і непрозорість…</string> + <string name="touch_copy_layout">Копіювати цю розкладку</string> + <string name="touch_layout_copied">Розкладку скопійовано; вставте її в сенсорну розкладку будь-якої гри</string> + <string name="touch_paste_layout">Вставити скопійовану розкладку</string> + <string name="touch_reset_layout">Відновити типову розкладку Steam</string> + <string name="touch_no_binding">У цього елемента немає прив\'язки в Steam, щоб призначити йому значок</string> + <string name="touch_icon_for">Значок для %1$s</string> + <string name="touch_layout_color">Колір сенсорної розкладки</string> + <string name="touch_done">Готово</string> + <string name="touch_save_failed">Не вдалося зберегти сенсорну розкладку</string> + <string name="touch_saved">Сенсорну розкладку збережено в Steam</string> + <string name="touch_app_id">Застосунок %1$d</string> + <string name="touch_label_hint">Підпис (необов\'язково)</string> + <string name="touch_icons_missing">Значки Steam не знайдено</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d значок зі Steam</item> + <item quantity="few">%1$d значки зі Steam</item> + <item quantity="many">%1$d значків зі Steam</item> + <item quantity="other">%1$d значка зі Steam</item> + </plurals> + <string name="touch_icon_color">Колір значка</string> + <string name="touch_button_color">Колір кнопки</string> + <string name="touch_no_icon">Без значка</string> + + <string name="stores_sync_sign_in">Перевірка входу…</string> + <string name="stores_sync_library">Отримання вашої бібліотеки…</string> + <string name="stores_sync_game_list">Отримання списку ігор…</string> + <string name="stores_sync_details">Завантаження відомостей про ігри… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">Синхронізація %1$d гри…</item> + <item quantity="few">Синхронізація %1$d ігор…</item> + <item quantity="many">Синхронізація %1$d ігор…</item> + <item quantity="other">Синхронізація %1$d гри…</item> + </plurals> + <string name="stores_sync_progress">Синхронізація… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Отримання обкладинок…</string> + <string name="stores_sync_failed">Синхронізація не вдалася</string> + <string name="stores_sync_no_games">У вашій бібліотеці %1$s не знайдено ігор</string> + <string name="stores_sync_amazon_no_answer">Amazon Games не відповів щодо вашої бібліотеки: оновіть або увійдіть знову</string> + <string name="stores_sync_gog_unreachable">Не вдалося з\'єднатися з GOG</string> + <string name="stores_sync_gog_unreadable">Не вдалося прочитати вашу бібліотеку GOG</string> + <string name="stores_registering">Реєстрація в Steam…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Режим Cardboard</string> + <string name="cardboard_mode_hint">Зображення для лівого й правого ока для VR-окулярів під телефон.</string> + <string name="cardboard_source">Зображення</string> + <string name="cardboard_source_screen">Віртуальний екран</string> + <string name="cardboard_source_sbs">Стереопара 3D</string> + <string name="cardboard_viewer">Окуляри</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Без лінз</string> + <string name="cardboard_ipd">Відстань між очима</string> + <string name="cardboard_screen_width">Ширина екрана</string> + <string name="cardboard_screen_distance">Відстань до екрана</string> + <string name="cardboard_head_tracking">Відстеження голови</string> + <string name="cardboard_no_sensor">На цьому пристрої немає датчика повороту.</string> + <string name="cardboard_recenter">Відцентрувати вигляд</string> + <string name="cardboard_recenter_button">Відцентрувати</string> + <string name="cardboard_steamvr">SteamVR (експериментально)</string> + <string name="cardboard_steamvr_install">Установити</string> + <string name="cardboard_steamvr_install_hint">Завантажує SteamVR від Valve для ARM64 (571 МБ).</string> + <string name="cardboard_steamvr_installing">Завантаження й розпакування SteamVR…</string> + <string name="cardboard_steamvr_start">Запустити</string> + <string name="cardboard_steamvr_ready_hint">Запускає SteamVR, де гарнітурою слугує ваша голова; зображення перемикається на стереопару 3D.</string> + <string name="cardboard_steamvr_stop">Зупинити</string> + <string name="cardboard_steamvr_running_hint">SteamVR працює.</string> + <string name="cardboard_steamvr_unavailable">Недоступно в цій сесії.</string> + <string name="cardboard_steamvr_failed">SteamVR не відповідає; перевірте журнал сесії.</string> + <string name="drawer_component_downloads">Завантажити компоненти</string> + <string name="drawer_component_downloads_hint">Завантаження спільні зі сторінкою «Компоненти». Виберіть вище завантажену версію, щоб використати її під час наступного запуску гри.</string> + <string name="drawer_component_choose_download">Виберіть версію для завантаження</string> + <string name="stores_fact_developer">Розробник</string> + <string name="stores_fact_publisher">Видавець</string> + <string name="stores_fact_released">Дата виходу</string> + <string name="stores_hltb_main">Сюжет</string> + <string name="stores_hltb_extras">+ Додатково</string> + <string name="stores_hltb_completionist">100%</string> + <string name="stores_hltb_hours">%1$s год</string> + <string name="stores_hltb_minutes">%1$d хв</string> +</resources> diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml new file mode 100644 index 000000000..e481ecfb4 --- /dev/null +++ b/app/src/main/res/values-vi/strings.xml @@ -0,0 +1,2210 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">DroidDeck</string> + <string-array name="loading_hints"> + <item>Lần chạy đầu tải Steam. Quá trình này có thể mất vài phút.</item> + <item>Theo mặc định, rời khỏi ứng dụng sẽ tạm dừng Steam; nó tiếp tục từ nơi bạn dừng lại khi bạn quay lại.</item> + <item>Tay cầm của bạn hiện ra như tay cầm Xbox 360. Nếu không, hãy dùng các nút điều khiển trên màn hình.</item> + </string-array> + <string-array name="loading_hints_desktop"> + <item>Lần chạy đầu thiết lập desktop. Quá trình này có thể mất vài phút.</item> + <item>Theo mặc định, rời khỏi ứng dụng sẽ tạm dừng phiên; nó tiếp tục từ nơi bạn dừng lại khi bạn quay lại.</item> + <item>Chạm hoạt động như touchpad: kéo để di chuyển con trỏ, chạm để nhấp.</item> + <item>Tay cầm của bạn hiện ra như tay cầm Xbox 360. Nếu không, hãy dùng các nút điều khiển trên màn hình.</item> + </string-array> + <string name="session_channel">Phiên</string> + <string name="session_channel_description">Hiển thị khi có phiên Steam hoặc desktop đang mở</string> + <string name="session_notification">Steam đang chạy</string> + <string name="session_notification_downloads">Đang tải Steam</string> + <string name="session_notification_paused">Phiên đã tạm dừng - nhấn Tiếp tục khi sẵn sàng</string> + <string name="resume_session">Tiếp tục</string> + <string name="suspend_now">Tạm dừng ngay</string> + <string name="stop_session">Dừng phiên</string> + <string name="frame_gen_title">Tạo khung hình</string> + <string name="frame_gen_hint">Chọn chế độ tạo khung hình</string> + <string name="frame_gen_off">Tắt</string> + <string name="frame_gen_winfg">Win-FG %1$d×</string> + <string name="frame_gen_lsfg">LSFG %1$d×</string> + <string name="frame_gen_lsfg_adaptive">LSFG thích ứng %1$d</string> + <string name="lsfg_needs_steam">Cần Lossless Scaling trên Steam</string> + <string name="lsfg_needs_install">Cài Lossless Scaling trong Steam hoặc nhập Lossless.dll</string> + <string name="lsfg_import">Nhập Lossless.dll…</string> + <string name="lsfg_using_steam">Đang dùng bản sao từ Steam</string> + <string name="lsfg_using_import">Đang dùng bản đã nhập</string> + <string name="lsfg_pick_title">Chọn Lossless.dll</string> + <string name="lsfg_imported">Đã nhập Lossless.dll</string> + <string name="lsfg_import_unchanged">Lossless.dll này đã đang được dùng</string> + <string name="lsfg_import_not_owned">Tài khoản Steam này không sở hữu Lossless Scaling</string> + <string name="lsfg_import_invalid">Tệp này không phải là Lossless.dll của Lossless Scaling</string> + <string name="lsfg_import_failed">Không thể chuẩn bị LSFG từ Lossless.dll này</string> + <string name="lsfg_problem_missing">LSFG cần Lossless Scaling từ Steam hoặc một Lossless.dll đã nhập</string> + <string name="lsfg_problem_failed">LSFG không thể chuẩn bị shader từ Lossless.dll</string> + <string name="hud_fg_unsupported">%1$s: không hỗ trợ</string> + <string name="hud_fg_failed">%1$s: engine lỗi</string> + <string name="hud_fg_needs_lossless">%1$s: cần Lossless Scaling</string> + <string name="hud_fg_starting">%1$s %2$s · đang khởi động</string> + <string name="second_library_import_hint">Thêm một thư viện Steam và quét để tìm game Windows. Game đã sở hữu dùng mục Steam của nó; phần còn lại trở thành shortcut ngoài Steam.</string> + <string name="game_env_title">Môi trường game</string> + <string name="game_env_hint">Biến môi trường cho game Proton.</string> + <string name="game_env_edit">Sửa</string> + <string name="game_env_shared">Tất cả game Proton</string> + <string name="game_env_scope">Hồ sơ</string> + <string name="game_env_app_id">ID ứng dụng Steam</string> + <string name="game_env_open">Mở</string> + <string name="game_env_profile">Game %1$s</string> + <string name="game_env_add">Thêm biến</string> + <string name="game_env_name">Tên biến</string> + <string name="game_env_value">Giá trị</string> + <string name="game_env_save">Lưu</string> + <string name="game_env_remove">Xóa</string> + <string name="game_env_restore">Kế thừa lại</string> + <string name="game_env_reset">Xóa các ghi đè môi trường</string> + <string name="game_env_unset">Bỏ đặt cho hồ sơ này</string> + <string name="game_env_empty">Chưa có biến nào được đặt.</string> + <string name="game_env_inherited">Được kế thừa</string> + <string name="game_env_custom">Biến tùy chỉnh</string> + <string name="game_env_error">Không thể tải hoặc lưu. Kiểm tra dung lượng trống và thử lại.</string> + <string name="game_env_invalid">Dùng tên hợp lệ và giá trị tối đa 8192 ký tự.</string> + <string name="game_env_unused">Runtime Proton chuẩn không đọc biến này.</string> + <string name="game_env_loading">Đang tải…</string> + <string name="game_env_feature">Ghi đè cấp tính năng D3D12. 12_2 báo cáo khả năng DirectX 12 Ultimate; các tính năng không được hỗ trợ có thể gây lỗi. Để trống để tự động phát hiện.</string> + <string name="game_env_shader">Ghi đè shader model D3D12. 6_9 cần VKD3D đủ mới và tính năng GPU phù hợp. Để trống để tự động phát hiện.</string> + <string name="game_env_cache">Giữ bộ nhớ đệm shader của Mesa luôn bật để giảm biên dịch lặp lại. false sẽ bật bộ nhớ đệm.</string> + <string name="game_env_cache_size">Kích thước tối đa của bộ nhớ đệm shader Mesa, ví dụ 1G. Bộ nhớ đệm lớn hơn dùng nhiều dung lượng hơn.</string> + <string name="game_env_vkd3d_config">Tùy chọn VKD3D cho D3D12. nodxr tắt dò tia; chỉ dùng cho game cần nó.</string> + <string name="game_env_proton_log">Đặt thành 1 để có log chẩn đoán Proton. Ghi log có thể giảm hiệu năng và dùng dung lượng.</string> + <string name="game_env_wined3d">Đặt thành 1 để dùng WineD3D dựa trên OpenGL thay cho DXVK khi kiểm tra tương thích D3D9–11.</string> + <string name="game_env_no_ntsync">Đặt thành 1 để giữ game này không dùng ntsync (droiddeck-ntsync hoặc driver kernel); nó sẽ dùng droiddeck-esync nếu có pack phù hợp, nếu không thì droiddeck-fsync.</string> + <string name="game_env_no_esync">Đặt thành 1 để tắt droiddeck-esync cho game này; nó sẽ dùng droiddeck-fsync thay vào đó, hoặc droiddeck-ntsync nếu fsync cũng tắt.</string> + <string name="game_env_no_fsync">Đặt thành 1 để giữ game này không dùng droiddeck-fsync; nó sẽ dùng droiddeck-esync nếu có pack phù hợp, nếu không thì droiddeck-ntsync.</string> + <string name="game_env_fsync_first">Đặt thành 1 để chạy game này trên droiddeck-fsync, hoặc 0 để ưu tiên droiddeck-esync nếu có pack phù hợp với Proton.</string> + <string name="game_env_xalia">Đặt thành 0 để tắt trình hỗ trợ điều hướng bằng tay cầm của Proton cho game này.</string> + <string name="game_env_hud">Lớp phủ DXVK cho D3D9–11. fps hiển thị tốc độ khung hình; để trống để ẩn.</string> + <string name="game_env_dxvk_config">Các thiết lập DXVK ngăn cách bằng dấu chấm phẩy. Các bản build gần đây dùng dxvk.maxFrameRate để giới hạn khung hình; nên dùng bộ giới hạn khung hình của phiên khi chơi thông thường.</string> + <string name="game_env_vkd3d_limit">Giới hạn tốc độ khung hình D3D12. 0 tắt giới hạn.</string> + <string name="game_env_glthread">Luồng OpenGL của Mesa. true có thể giúp các game OpenGL bị giới hạn bởi CPU; hãy thử trên từng game.</string> + <string name="sync_backend_title">Đồng bộ Wine</string> + <string name="sync_backend_hint">Cách game giữ các luồng đồng bộ. Áp dụng từ phiên sau.</string> + <string name="sync_backend_esync">esync</string> + <string name="sync_backend_ntsync">ntsync</string> + <string name="sync_backend_fsync">fsync</string> + <string name="sync_backend_wineserver">wineserver</string> + <string name="fex_preset_title">Thiết lập trước FEX</string> + <string name="fex_next_launch">Áp dụng vào lần khởi chạy game kế tiếp.</string> + <string name="game_settings_title">Game</string> + <string name="fex_default_label">Mặc định FEX</string> + <string name="fex_default_detail">Dùng thiết lập mặc định của FEX.</string> + <string name="fex_stability_label">Ổn định</string> + <string name="fex_stability_detail">Thiết lập an toàn nhất, chậm nhất.</string> + <string name="fex_compatibility_label">Tương thích</string> + <string name="fex_compatibility_detail">TSO đầy đủ với multiblock.</string> + <string name="fex_intermediate_label">Trung bình</string> + <string name="fex_intermediate_detail">TSO cân bằng với độ chính xác x87 giảm.</string> + <string name="fex_performance_label">Hiệu năng</string> + <string name="fex_performance_detail">Tắt toàn bộ TSO; multiblock.</string> + <string name="fex_performance_tso_label">Hiệu năng + TSO</string> + <string name="fex_performance_tso_detail">Thiết lập hiệu năng với TSO chính được bật.</string> + <string name="fex_extreme_label">Cực hạn</string> + <string name="fex_extreme_detail">Tắt kiểm tra mã tự sửa đổi. Game dùng JIT và DRM có thể lỗi.</string> + <string name="fex_extreme_tso_label">Cực hạn + TSO</string> + <string name="fex_extreme_tso_detail">Thiết lập cực hạn với TSO chính được bật.</string> + <string name="fex_extreme_gn_label">Cực hạn-gn</string> + <string name="fex_extreme_gn_detail">Hiệu năng với gợi ý TSC và metadata. Thử trước khi dùng Cực hạn.</string> + <string name="fex_denuvo_label">Denuvo</string> + <string name="fex_denuvo_detail">Hiệu năng với kiểm tra mã đầy đủ và ẩn bit hypervisor.</string> + <string name="game_env_custom_value">Giá trị tùy chỉnh</string> + <string name="game_env_select_value">Chọn giá trị</string> + <string name="user_apps_section">Trình giả lập & ứng dụng</string> + <string name="user_apps_add">Thêm</string> + <string name="user_apps_add_detail">Ứng dụng hoặc game của riêng bạn</string> + <string name="user_apps_kind_script">Script</string> + <string name="user_apps_kind_appimage">AppImage</string> + <string name="user_apps_kind_flatpak">Flatpak</string> + <string name="user_apps_progress">%1$s · %2$s</string> + <string name="user_apps_progress_percent">%1$s · %2$s · %3$d%%</string> + <string name="user_apps_starting">Đang khởi động…</string> + <string name="user_apps_added">Đã thêm %1$s</string> + <string name="user_apps_removed">Đã xóa %1$s</string> + <string name="user_apps_back">Desktop</string> + <string name="user_apps_gone">Ứng dụng đó không còn nữa. Chọn lại từ Desktop.</string> + <string name="user_apps_open">Mở %1$s</string> + <string name="user_apps_remove">Xóa</string> + <string name="user_apps_remove_confirm">Nhấn lại để xóa</string> + <string name="user_apps_stop_session">Dừng phiên để xóa</string> + <string name="user_apps_script_linked">Chạy %1$s từ thư mục của nó, được liên kết vào phiên tại chính vị trí đó. Xóa ứng dụng sẽ không ảnh hưởng đến thư mục đó.</string> + <string name="user_apps_script_copied">Chạy %1$s từ một bản sao thư mục của nó bên trong DroidDeck. Xóa ứng dụng sẽ xóa bản sao, không phải bản gốc.</string> + <string name="user_apps_runtime_required">Cài runtime Linux trước</string> + <string name="user_apps_file_gone">Tệp không còn tồn tại</string> + <string name="user_apps_no_space">Không đủ dung lượng trống: sao chép thư mục cần %1$s</string> + <string name="user_apps_copying">Đang sao chép %1$s</string> + <string name="user_apps_failed">Thêm ứng dụng không thành công</string> + <string name="user_apps_github_checking">Đang tìm bản phát hành của %1$s</string> + <string name="user_apps_github_unreachable">Không thể đọc bản phát hành của %1$s</string> + <string name="user_apps_github_no_appimage">Không có bản phát hành nào của %1$s có AppImage ARM64 hoặc x86_64</string> + <string name="user_apps_downloading">Đang tải %1$s</string> + <string name="user_apps_download_failed">Tải xuống không thành công</string> + <string name="user_apps_update_failed">Cập nhật ứng dụng không thành công</string> + <string name="user_apps_icon_failed">Không thể đọc biểu tượng đó</string> + <string name="user_apps_saved">Đã lưu %1$s</string> + <string name="user_apps_edit">Sửa</string> + <string name="user_apps_check_updates">Kiểm tra cập nhật</string> + <string name="user_apps_checking_updates">Đang kiểm tra cập nhật…</string> + <string name="user_apps_update_to">Cập nhật lên %1$s</string> + <string name="user_apps_up_to_date">Đã mới nhất · %1$s</string> + <string name="user_apps_updated">Đã cập nhật %1$s</string> + <string name="user_apps_github_version">%1$s · %2$s</string> + <string name="add_app_title">Thêm ứng dụng</string> + <string name="add_app_tab_script">Script</string> + <string name="add_app_tab_appimage">AppImage</string> + <string name="add_app_tab_github">GitHub</string> + <string name="add_app_tab_flatpak">Flatpak</string> + <string name="add_app_script_hint">Một tệp .sh. Nó chạy từ thư mục của chính nó, toàn màn hình dưới gamescope.</string> + <string name="add_app_appimage_hint">Một AppImage ARM64 (aarch64), hoặc một bản x86_64 chạy qua FEX. Nó được giải nén vào runtime Linux một lần.</string> + <string name="add_app_github_hint">Cài AppImage ARM64 từ bản phát hành mới nhất của repository, hoặc bản x86_64 của nó qua FEX.</string> + <string name="app_fex_title">Dùng FEX</string> + <string name="app_fex_auto">Tự động</string> + <string name="app_fex_on">Bật</string> + <string name="app_fex_off">Tắt</string> + <string name="app_fex_hint">FEX chạy các chương trình Linux x86 trên thiết bị ARM64 này. Tự động sẽ dùng nó khi chương trình được build cho x86.</string> + <string name="app_fex_x86">%1$s qua FEX</string> + <string name="app_fex_not_ready">Chương trình này được build cho PC %1$s và chạy qua FEX, hiện chưa được thiết lập. Mở Steam một lần khi đã đăng nhập: DroidDeck sẽ tải FEX và Steam Linux Runtime cần thiết, sau đó ứng dụng này sẽ mở.</string> + <string name="session_end_program">Chương trình này</string> + <string name="session_end_fex_missing">%1$s được build cho PC x86 và chạy qua FEX, hiện chưa được cài. Khởi động Steam một lần khi đã đăng nhập và để nó tải FEX cùng Steam Linux Runtime, sau đó khởi động lại ứng dụng này.</string> + <string name="add_app_flatpak_hint">Một app ID Flathub hoặc trang Flathub của nó. Flatpak sẽ được thiết lập trước nếu chưa có.</string> + <string name="add_app_file">Tệp</string> + <string name="add_app_no_file">Chưa chọn tệp</string> + <string name="add_app_choose_file">Chọn tệp</string> + <string name="add_app_change_file">Đổi</string> + <string name="add_app_pick_script">Chọn một script</string> + <string name="add_app_pick_appimage">Chọn một AppImage</string> + <string name="add_app_pick_icon">Chọn một biểu tượng</string> + <string name="add_app_repository">Repository</string> + <string name="add_app_repository_placeholder">owner/repo hoặc một liên kết github.com</string> + <string name="add_app_repository_invalid">Không phải một repository GitHub</string> + <string name="add_app_flatpak_id">ID ứng dụng</string> + <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> + <string name="add_app_flatpak_invalid">Không phải một app ID Flathub</string> + <string name="add_app_flatpak_bundle_hint">Hoặc một tệp .flatpak, cho ứng dụng không có trên Flathub.</string> + <string name="add_app_pick_flatpak">Chọn một tệp .flatpak</string> + <string name="add_app_source">Nguồn</string> + <string name="add_app_appearance">Trên Desktop của bạn</string> + <string name="add_app_preview_name">Ứng dụng mới</string> + <string name="add_app_name">Tên</string> + <string name="add_app_name_placeholder">Từ ứng dụng</string> + <string name="add_app_icon">Biểu tượng</string> + <string name="add_app_icon_default">Biểu tượng mặc định</string> + <string name="add_app_choose_icon">Chọn biểu tượng</string> + <string name="add_app_clear_icon">Xóa</string> + <string name="add_app_icon_suggestions">Từ repository</string> + <string name="add_app_icon_searching">Đang tìm biểu tượng…</string> + <string name="add_app_icon_reset">Mặc định</string> + <string name="edit_app_title">Sửa %1$s</string> + <string name="edit_app_save">Lưu</string> + <string name="add_app_checking_folder">Đang kiểm tra thư mục…</string> + <string name="add_app_folder_linked">Thư mục của nó được liên kết vào phiên: %1$s</string> + <string name="add_app_folder_copied">Thư mục của nó chứa các chương trình Linux, mà bộ nhớ dùng chung không thể chạy, nên nó được sao chép vào DroidDeck (%1$s).</string> + <string name="add_app_confirm">Thêm</string> + <string name="add_app_cancel">Hủy</string> + <string name="add_app_runtime_required">Cài runtime Linux từ Thiết lập trước.</string> + <string name="add_app_busy">Đợi %1$s hoàn tất.</string> + <string name="steam_compat_label">Tương thích</string> + <string name="proton_compatible_note">Các bản build được đánh dấu “%1$s” trong menu Compatibility của Steam có các bản sửa prefix của DroidDeck và chạy game được ở đây. Mặc định là Proton Experimental ARM64 (%1$s), hoặc Proton 11.0 ARM64 (%1$s) khi bản mới hơn đó đã được cài. Các mục Proton gốc của Valve không có dấu này không thể chạy game trên thiết bị này.</string> + <string name="proton_installed_compatible">Đã cài %1$s · %2$s</string> + <string name="common_cancel">Hủy</string> + <string name="common_auto">Tự động</string> + <string name="common_never">Không bao giờ</string> + <string name="common_always">Luôn luôn</string> + <string name="common_uninstall">Gỡ cài đặt</string> + <string name="common_check">Kiểm tra</string> + <string name="common_update">Cập nhật</string> + <string name="common_checking">Đang kiểm tra…</string> + <string name="common_working">Đang xử lý…</string> + <string name="common_forget">Quên</string> + <string name="common_add_ellipsis">Thêm…</string> + <string name="common_use">Dùng</string> + <string name="common_applies_next_session">Áp dụng từ phiên sau.</string> + <string name="mode_steam_title">Phiên Steam</string> + <string name="mode_desktop_title">Phiên Desktop</string> + <string name="mode_display">Hiển thị</string> + <string name="mode_resolution">Độ phân giải</string> + <string name="mode_res_720">Tối đa 720p</string> + <string name="mode_res_900">Tối đa 900p</string> + <string name="mode_res_1080">Tối đa 1080p</string> + <string name="mode_res_native">Gốc</string> + <string name="mode_res_default">%1$s (mặc định)</string> + <string name="mode_res_custom_value">Tùy chỉnh · %1$d×%2$d</string> + <string name="mode_res_custom">Tùy chỉnh…</string> + <string name="mode_res_note">720p có thể giúp menu phản hồi nhanh hơn.</string> + <string name="mode_ratio">Tỷ lệ màn hình</string> + <string name="mode_ratio_custom">Được đặt theo độ phân giải tùy chỉnh.</string> + <string name="mode_ratio_auto">Tự động dùng tối thiểu 16:9.</string> + <string name="mode_fps">Giới hạn khung hình</string> + <string name="mode_fps_note">Giới hạn toàn bộ phiên. Màn hình chuyển sang tốc độ làm mới mà giới hạn chia hết.</string> + <string name="mode_hdr">HDR</string> + <string name="mode_hdr10">Xuất HDR10</string> + <string name="mode_hdr_unavailable">Không khả dụng: %1$s.</string> + <string name="mode_hdr_restart">Khởi động lại ứng dụng để áp dụng.</string> + <string name="mode_drivers">Driver</string> + <string name="mode_gpu_drivers">Driver GPU</string> + <string name="mode_gpu_drivers_hint">Dùng chung cho Steam và Desktop. Đặt ở trang Components.</string> + <string name="mode_touch_controls">Chạm & điều khiển</string> + <string name="mode_touch">Chạm</string> + <string name="mode_touch_touchpad">Touchpad</string> + <string name="mode_touch_direct">Trực tiếp</string> + <string name="mode_touch_note">Tự động: touchpad ở Desktop, trực tiếp ở Steam.</string> + <string name="mode_osc">Điều khiển trên màn hình</string> + <string name="mode_osc_qam">Steam + QAM</string> + <string name="mode_osc_note">Tự động hiện các nút điều khiển khi không có tay cầm kết nối.</string> + <string name="osc_steam_touch">Chạm Steam (beta)</string> + <string name="osc_style_next_session">Các nút chạm của Steam bắt đầu từ phiên Steam kế tiếp</string> + <string name="osc_steam_layout">Bố cục chạm Steam</string> + <string name="osc_steam_layout_hint">Di chuyển, đổi kích thước hoặc ẩn các nút chạm của game này; được lưu vào Steam</string> + <string name="mode_controller">Tay cầm</string> + <string name="mode_controller_hint">Cách Steam nhận dạng tay cầm của bạn. Áp dụng từ phiên sau.</string> + <string name="mode_controller_deck">Tay cầm Steam Deck</string> + <string name="mode_controller_steam">Steam Controller</string> + <string name="mode_controller_ds4">DualShock 4</string> + <string name="mode_controller_dualsense">DualSense</string> + <string name="mode_controller_x360">Tay cầm Xbox 360</string> + <string name="mode_controller_note">Deck: nút Quick Access và con quay hồi chuyển. Steam Controller: tương tự, cộng với cảm biến nắm ở các nút phía sau. DualShock 4 và DualSense: con quay hồi chuyển, và touchpad dùng làm bàn di chuột. Xbox 360: mở Quick Access bằng Guide+A.</string> + <string name="mode_back">Quay lại</string> + <string name="back_menu_then_qam">Menu → QAM</string> + <string name="back_qam_then_menu">QAM → Menu</string> + <string name="mode_session">Phiên</string> + <string name="mode_suspend">Hành vi khi ở nền</string> + <string name="mode_suspend_hint">Khi ứng dụng bị ẩn hoặc màn hình tắt.</string> + <string name="mode_suspend_manual">Thủ công</string> + <string name="mode_suspend_native">Gốc</string> + <string name="mode_suspend_steam_note">Tự động tạm dừng và tiếp tục khi được hiển thị lại. Gốc chuẩn bị Steam để ngủ trước. Thủ công chờ nhấn Tiếp tục. Không bao giờ giữ nó luôn chạy.</string> + <string name="mode_background_downloads">Giữ tải xuống tiếp tục chạy</string> + <string name="mode_background_downloads_hint">Trì hoãn tạm dừng nền khi Steam đang tải xuống.</string> + <string name="mode_suspend_note">Tự động tạm dừng ở nền và tiếp tục khi hiển thị lại. Thủ công chờ nhấn Tiếp tục. Không bao giờ giữ nó luôn chạy.</string> + <string name="mode_startup">Khởi động</string> + <string name="mode_steam_startup">Khởi động Steam cùng DroidDeck</string> + <string name="mode_steam_startup_hint">Mở phiên Steam khi DroidDeck khởi chạy.</string> + <string name="mode_decky">Decky</string> + <string name="mode_decky_checking">Đang kiểm tra bản phát hành…</string> + <string name="mode_decky_ready">Sẵn sàng cài %1$s.</string> + <string name="mode_decky_none">Không tìm thấy bản build tương thích. Hãy thử lại sau.</string> + <string name="mode_decky_installed">Đã cài · %1$s</string> + <string name="mode_decky_update">Có cập nhật · %1$s</string> + <string name="mode_decky_current">Đã mới nhất · %1$s</string> + <string name="mode_decky_loader">Loader</string> + <string name="mode_decky_install_latest">Cài bản mới nhất</string> + <string name="mode_decky_on">Khởi động cùng Steam. Khi đang bật, các ứng dụng khác trên thiết bị này có thể điều khiển được Steam.</string> + <string name="mode_decky_off">Tắt. Cổng gỡ lỗi từ xa của Steam luôn đóng.</string> + <string name="mode_decky_remove_title">Gỡ cài đặt Decky Loader?</string> + <string name="mode_decky_remove_text">Gỡ bỏ loader. Plugin và thiết lập của bạn được giữ lại.</string> + <string name="mode_client">Client</string> + <string name="mode_mangoapp">Lớp phủ hiệu năng</string> + <string name="mode_mangoapp_hint">Tắt nếu game bị crash hoặc màn hình đen ở Chế độ Deck. Áp dụng từ phiên sau.</string> + <string name="mode_steam_repair">Sửa chữa client Steam</string> + <string name="mode_steam_repair_hint">Tải lại client Steam vào lần Steam khởi động kế tiếp. Dùng khi Steam bị kẹt ở màn hình tải. Game, đăng nhập và thiết lập được giữ lại.</string> + <string name="mode_steam_repair_queued">Client Steam sẽ được tải lại vào lần Steam khởi động kế tiếp.</string> + <string name="mode_steam_repair_button">Sửa chữa</string> + <string name="mode_added_art">Hình ảnh</string> + <string name="mode_sgdb_key">API key SteamGridDB</string> + <string name="mode_sgdb_key_yours">Key của bạn</string> + <string name="mode_sgdb_edit">Sửa…</string> + <string name="mode_added_art_hint">Điền hình ảnh còn thiếu từ Steam, sau đó từ SteamGridDB. Tệp cover.jpg, hero.jpg, logo.png hoặc icon.png của riêng bạn trong thư mục game được ưu tiên.</string> + <string name="summary_remove_game">Xóa %1$s</string> + <string name="mode_added_remove_confirm">Xóa?</string> + <string name="games_add">Thêm game</string> + <string name="games_add_title">Thêm một game:</string> + <string name="games_pick_exe_title">Chọn tệp exe:</string> + <string name="games_add_unreachable">Vị trí này không khả dụng trong phiên</string> + <string name="mode_added_exe_check">Kiểm tra tệp exe này</string> + <string name="wincomp_title">Thành phần Windows</string> + <string name="wincomp_pad_hint">LB / RB để chuyển giữa các mục.</string> + <string name="wincomp_card_none">Không có</string> + <string name="wincomp_applies">Các DLL Windows mà Proton không bao gồm, từ danh sách thành phần. Bật những thành phần game này cần: chúng được đặt vào thư mục Windows riêng của game, không phải tệp của nó, và được giữ lại dù nó chạy trên Proton nào.</string> + <string name="wincomp_offline">Không thể kết nối danh sách thành phần. Các thành phần đã tải trước đó vẫn có thể chuyển đổi.</string> + <string name="wincomp_recommended">Được đề xuất cho game này</string> + <string name="wincomp_checking">Đang kiểm tra game này cần gì…</string> + <string name="wincomp_when_online">Khả dụng khi có mạng</string> + <plurals name="wincomp_turning_on"> + <item quantity="other">Đang bật %1$d thành phần…</item> + </plurals> + <string name="wincomp_auto_section">Đang bật cho game này</string> + <string name="wincomp_auto_steam">Steam cài %1$s cùng %2$s (app %3$s)</string> + <string name="wincomp_auto_gog">Từ danh sách của GOG cho game này</string> + <string name="wincomp_auto_epic">Từ các yêu cầu của Epic cho game này</string> + <string name="wincomp_auto_amazon">Từ các bước cài đặt của Amazon cho game này</string> + <string name="wincomp_steam_by_name">Steam cài %1$s cùng app %2$s, khớp theo tên</string> + <string name="wincomp_suggestions">Cũng được liệt kê</string> + <string name="wincomp_suggestion_reason">Được %1$s liệt kê; không phải thành phần ở đây</string> + <string name="wincomp_src_steam">Steam</string> + <string name="wincomp_src_gog">GOG</string> + <string name="wincomp_src_epic">Epic Games</string> + <string name="wincomp_src_amazon">Amazon Games</string> + <string name="wincomp_found_bundled">Game đi kèm %1$s</string> + <string name="wincomp_found_shipped">Game có %1$s (thường hoạt động được ngay)</string> + <string name="wincomp_already_here">Đã có trong game này</string> + <string name="wincomp_no_recommendation">Không có gì trong thư mục game chỉ đến một thành phần.</string> + <string name="wincomp_active">Đã cài cho game này (%1$d)</string> + <string name="wincomp_all">Tất cả thành phần (%1$d)</string> + <string name="wincomp_installers">Cần bộ cài Windows (%1$d)</string> + <string name="wincomp_installers_note">Đây là các chương trình cài đặt riêng, không có gói bên trong để phân tích, và chưa có ghi nhận về những gì chúng cài. Hầu hết là các runtime cũ mà Mono trong Proton đã thay thế; các ghi nhận được bổ sung khi có.</string> + <string name="wincomp_show">Hiện</string> + <string name="wincomp_hide">Ẩn</string> + <string name="wincomp_needs_installer">Chạy chương trình cài đặt riêng (chưa có ghi nhận)</string> + <string name="wincomp_unsupported">Chưa được hỗ trợ ở đây</string> + <string name="wincomp_loading">Đang tải các thành phần…</string> + <string name="wincomp_failed">Không thể cài: %1$s</string> + <string name="wincomp_downloaded">Đã tải</string> + <string name="wincomp_downloading">Đang tải %1$s</string> + <string name="wincomp_installing">Đang cài: %1$s</string> + <string name="wincomp_reverting">Đang tắt…</string> + <string name="wincomp_download_size">Tải %1$s</string> + <string name="wincomp_next_launch">Có hiệu lực vào lần game khởi động kế tiếp.</string> + <string name="mode_stretch169">Kéo giãn game 16:9</string> + <string name="mode_stretch169_hint">Trên màn hình vuông hơn 16:9, kéo giãn game để lấp đầy thay vì hiện viền đen. Áp dụng từ phiên sau.</string> + <string name="mode_audio">Âm thanh</string> + <string name="mode_directaudio">DirectAudio cho game</string> + <string name="mode_directaudio_hint">Bỏ qua PulseAudio để giảm độ trễ.</string> + <string name="mode_mic">Microphone</string> + <string name="mode_mic_hint">Dùng cho chat bằng giọng nói.</string> + <string name="mode_network">Mạng</string> + <string name="mode_wifi_discovery">Dò tìm Wi-Fi</string> + <string name="mode_wifi_discovery_hint">Hiện tên Wi-Fi và các mạng gần đó trong Steam.</string> + <string name="mode_wifi_location_off">Dịch vụ vị trí đang tắt.</string> + <string name="mode_wifi_permission_denied">Chưa cấp quyền vị trí.</string> + <string name="mode_wifi_open_settings">Mở thiết lập</string> + <string name="mode_wifi_continue">Tiếp tục</string> + <string name="mode_wifi_explain_title">Bật dò tìm Wi-Fi?</string> + <string name="mode_wifi_explain_text">Android yêu cầu quyền Vị trí để đọc tên Wi-Fi và dò tìm các mạng gần đó. DroidDeck dùng quyền này để hiện các mạng đó trong Steam. Chúng tôi không theo dõi vị trí của bạn. Truy cập Internet vẫn hoạt động mà không cần quyền này.</string> + <string name="mode_storage">Lưu trữ game</string> + <string name="mode_storage_auto">Tự động - thẻ SD khi có, cùng các thư mục Games của bạn</string> + <string name="mode_storage_auto_none">Tự động - các thư mục Games của bạn, cùng thẻ SD khi có (hiện không có)</string> + <string name="mode_storage_internal">Chỉ bộ nhớ trong</string> + <string name="mode_storage_folder">Thư mục: %1$s</string> + <string name="mode_second_library">Thư viện thứ hai</string> + <string name="mode_storage_note">Game truyền dữ liệu từ thẻ SD hoặc bộ nhớ dùng chung có thể bị giật.</string> + <string name="mode_storage_diagnostics">Chẩn đoán lưu trữ</string> + <string name="mode_storage_diagnostics_hint">Ghi lại kết quả cấp phát và thời gian lưu trữ mẫu vào Share logs. Có thể làm chậm cài đặt khi đang bật. Áp dụng từ phiên sau.</string> + <string name="mode_choose_folder">Chọn một thư mục…</string> + <string name="mode_custom_res_title">Độ phân giải tùy chỉnh</string> + <string name="mode_custom_res_text">Thay thế giới hạn và tỷ lệ. Kích thước không khớp màn hình sẽ có viền đen.</string> + <string name="mode_width">Chiều rộng</string> + <string name="mode_height">Chiều cao</string> + <string name="mode_custom_res_range">Từ 320×240 đến 3840×2160.</string> + <plurals name="mode_added_count"> + <item quantity="other">%1$d game</item> + </plurals> + <string name="common_ok">OK</string> + <string name="common_delete">Xóa</string> + <string name="common_restore">Khôi phục</string> + <string name="common_download">Tải xuống</string> + <string name="common_delete_named">Xóa %1$s</string> + <string name="common_size_mb">%1$.1f MB</string> + <string name="comp_runtime_driver">Driver runtime</string> + <string name="comp_runtime_driver_hint">Dùng bởi Steam, game và desktop. Áp dụng từ phiên sau.</string> + <string name="comp_import_turnip">Nhập tệp zip Turnip…</string> + <string name="comp_display_driver">Driver hiển thị</string> + <string name="comp_display_driver_hint">Vẽ khung hình lên màn hình ở cả hai chế độ. Khởi động lại ứng dụng để áp dụng.</string> + <string name="comp_import_adrenotools">Nhập tệp zip AdrenoTools…</string> + <string name="comp_never">chưa bao giờ</string> + <string name="comp_nightlies_checked">Đã kiểm tra Nightlies %1$s</string> + <string name="comp_title">Thành phần</string> + <string name="comp_about">Về Thành phần</string> + <string name="comp_check_drivers">Kiểm tra driver mới</string> + <string name="comp_check_nightlies">Kiểm tra Nightlies để tìm gói mới</string> + <string name="comp_proton">Proton</string> + <string name="comp_proton_note">Proton được chọn ở đây chạy mọi game chưa chọn Proton riêng trong Steam, và một thay đổi thực hiện trong Steam cũng hiện ở đây. Steam thay thế Proton của Valve khi nó cập nhật; các bản gốc được giữ lại theo từng build.</string> + <string name="comp_scope">Cấu hình</string> + <string name="comp_profile_configure">Cấu hình hồ sơ</string> + <string name="comp_profile">Hồ sơ</string> + <string name="comp_scope_general">Hồ sơ mặc định</string> + <string name="comp_scope_general_detail">Được dùng tự động cho game không có ghi đè.</string> + <string name="comp_profile_game_detail">Thiết lập đã lưu được dùng tự động khi %1$s khởi chạy.</string> + <string name="comp_game_hint">Chỉ chọn những thiết lập game này cần. Mọi thứ khác tiếp tục dùng Hồ sơ mặc định.</string> + <string name="comp_game_fex">Thiết lập trước FEX</string> + <string name="comp_game_fex_inherited">Đang dùng thiết lập trước của Hồ sơ mặc định.</string> + <string name="comp_game_fex_override">Thiết lập trước tùy chỉnh đã lưu cho game này.</string> + <string name="comp_game_fex_custom">Biến FEX tùy chỉnh</string> + <string name="comp_game_use_general">Dùng Mặc định</string> + <string name="comp_game_compatibility">Tương thích game</string> + <string name="comp_game_environment_hint">Các tùy chọn môi trường Proton, DXVK, VKD3D và đồng bộ hóa cho game này.</string> + <string name="comp_game_proton_inherited">Đang dùng Proton của Hồ sơ mặc định.</string> + <string name="comp_game_proton_override">Proton tùy chỉnh đã lưu cho game này qua ánh xạ tương thích của Steam.</string> + <string name="comp_no_proton_short">Không có Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">Dùng gói của Hồ sơ mặc định từ %1$s trừ khi game này chọn gói khác.</string> + <string name="comp_game_component_general">Dùng gói thành phần của Hồ sơ mặc định.</string> + <string name="comp_game_component_override">Chỉ dùng %1$s cho game này.</string> + <string name="comp_game_runtime_options">Tùy chọn runtime</string> + <string name="comp_game_environment_other_hint">Các tùy chọn môi trường Proton, DXVK, VKD3D-Proton và đồng bộ hóa khác cho từng game.</string> + <string name="comp_game_saved_next">Hồ sơ của %1$s được lưu cho lần khởi chạy game kế tiếp.</string> + <string name="game_no_steam_title">Khởi động không cần Steam</string> + <string name="game_no_steam_detail">Dành cho game đóng ngay lập tức: khởi động game mà không cần thiết lập Steam của Proton. Các tính năng Steam như overlay sẽ tắt đối với game này.</string> + <string name="game_no_steam_on">Bật</string> + <string name="comp_game_proton_live">Lựa chọn Proton của %1$s đã được lưu và khả dụng ngay trong Steam.</string> + <string name="comp_game_proton_next">Lựa chọn Proton của %1$s đã được lưu. Khởi động lại Steam trước khi chạy game.</string> + <string name="comp_game_proton_not_runnable">%1$s không thể dùng Proton đã chọn trên thiết bị này.</string> + <string name="comp_game_load_failed">Không thể tải hồ sơ của %1$s: %2$s</string> + <string name="comp_game_save_failed">Không thể lưu hồ sơ game: %1$s</string> + <string name="comp_game_reset">Đặt lại hồ sơ game</string> + <string name="comp_game_reset_title">Đặt lại hồ sơ của %1$s?</string> + <string name="comp_game_reset_body">Xóa Proton, FEX, gói thành phần, môi trường, lọc texture và các ghi đè thành phần Windows của game này? Nó sẽ dùng Hồ sơ mặc định.</string> + <string name="comp_game_reset_done">%1$s giờ dùng Hồ sơ mặc định.</string> + <string name="comp_game_reset_failed">Không thể đặt lại hoàn toàn hồ sơ game: %1$s</string> + <string name="comp_default_live">%1$s giờ chạy mọi game chưa chọn Proton riêng trong Steam.</string> + <string name="comp_default_next">%1$s sẽ chạy mọi game chưa chọn Proton riêng, từ lần Steam khởi động kế tiếp.</string> + <string name="comp_default_not_runnable">%1$s không thể khởi chạy game ở đây, nên không thể là mặc định.</string> + <string name="comp_default_failed">Không thể lưu Proton mặc định: %1$s</string> + <string name="comp_swapped">%1$s · %2$d đã thay</string> + <string name="comp_all_original">%1$s · toàn bộ gốc</string> + <string name="comp_game_running_suffix">%1$s · game đang chạy</string> + <string name="comp_esync_pack">%1$s · pack droiddeck-esync</string> + <string name="comp_esync_wanted">%1$s · chưa có pack droiddeck-esync</string> + <string name="comp_esync_builtin">%1$s · esync có sẵn, không cần pack</string> + <string name="comp_gpu_drivers">Driver GPU</string> + <string name="comp_reapplied">%1$s · đã áp dụng lại lúc khởi chạy %2$s</string> + <string name="comp_next_queued">Tiếp theo: %1$s · sau khi game đóng</string> + <string name="comp_in_use">%1$s đang dùng: %2$s</string> + <string name="comp_waits">Chờ đến khi game đóng</string> + <string name="comp_applies_next_game">Áp dụng vào lần game khởi động kế tiếp</string> + <string name="comp_import_zip">Nhập tệp .zip</string> + <string name="comp_reading">Đang đọc Proton…</string> + <string name="comp_no_proton">Chưa cài Proton nào. Khởi động Steam một lần để tải Proton ARM64 của nó, hoặc thêm GE-Proton, CachyOS hay DroidDeck-Proton từ Thiết lập.</string> + <string name="comp_original">Gốc · %1$s</string> + <string name="comp_this_build">%1$s · build này</string> + <string name="comp_earlier_build">%1$s · build trước</string> + <string name="comp_installed_count">Đã cài · %1$d</string> + <string name="comp_nothing_stored">Chưa có gì được lưu cho %1$s. Các tệp gốc của Proton này được lưu làm bản gốc.</string> + <string name="comp_waits_long">Một game đang chạy trên Proton này, nên việc này chờ đến khi nó đóng.</string> + <string name="comp_applies_long">Áp dụng vào lần game khởi động kế tiếp.</string> + <string name="comp_swap_title">Thay %1$s?</string> + <string name="comp_swap_body">Đưa %1$s vào %2$s? Các tệp gốc của nó vẫn được giữ lại. %3$s</string> + <string name="comp_restore_title">Khôi phục %1$s?</string> + <string name="comp_restore_body">Đưa bản gốc %1$s trở lại %2$s? %3$s</string> + <string name="comp_delete_package_body">Xóa gói khỏi ứng dụng. Bạn có thể tải lại nó.</string> + <string name="comp_delete_original_title">Xóa bản gốc này?</string> + <string name="comp_delete_original_body">Bản gốc %1$s này là từ một build trước của %2$s. Bản gốc của build hiện tại luôn được giữ lại.</string> + <string name="comp_nightlies_count">Trên Nightlies · %1$d</string> + <string name="comp_refresh_to_list">Làm mới để liệt kê các gói trên Nightlies.</string> + <string name="comp_nothing_new">Không có gì mới từ lần kiểm tra trước.</string> + <string name="comp_import_wcp">Nhập tệp .wcp</string> + <string name="comp_restore_original">Khôi phục bản gốc của Proton này</string> + <string name="comp_restore_original_body">Đưa %2$s gốc của %1$s trở lại?</string> + <string name="comp_swap">Thay</string> + <string name="comp_about_text">FEX, DXVK và VKD3D-Proton cho mỗi Proton mà Steam dùng để chạy game.\n\nChọn một dòng đã cài để thay vào. Áp dụng vào lần game khởi động kế tiếp, hoặc sau khi game đang chạy đóng lại.\n\nCác lựa chọn của bạn được kiểm tra lại trước mỗi lần khởi chạy và được đặt lại nếu có gì thay đổi chúng. Các tệp gốc của mỗi Proton được giữ lại, nên các bản cập nhật Steam không bao giờ làm mất chúng.</string> + <string name="comp_tag_original">Gốc</string> + <string name="comp_tag_in_use">Đang dùng</string> + <string name="comp_tag_new">Mới</string> + <string name="comp_tag_stored">Đã lưu</string> + <string name="perf_title">Hiệu năng</string> + <string name="perf_lede">Lõi CPU, các bản sửa phiên và tên máy chủ. Áp dụng từ phiên sau.</string> + <string name="perf_client_cores_group">Lõi client Steam</string> + <string name="perf_override">Ghi đè lựa chọn lõi của Steam</string> + <string name="perf_override_hint">Ghim Steam, trình trợ giúp giao diện của nó và gamescope vào các lõi dưới đây.</string> + <string name="perf_client_cores">Lõi client</string> + <string name="perf_client_cores_on">Lõi dành cho Steam.</string> + <string name="perf_client_cores_off">Bật ghi đè để chọn.</string> + <string name="perf_game_cores_group">Lõi game</string> + <string name="perf_game_cores">Lõi game</string> + <string name="perf_game_cores_hint">Lõi dành cho game. Khi chọn tất cả, Android tự do lập lịch.</string> + <string name="perf_client_ui">Giao diện client</string> + <string name="perf_glthread">GL đa luồng</string> + <string name="perf_glthread_hint">Có thể giúp menu Steam phản hồi nhanh hơn.</string> + <string name="perf_zink">Zink: descriptor trễ (lazy)</string> + <string name="perf_zink_hint">Khuyến nghị cho driver không có descriptor buffer.</string> + <string name="perf_noglerror">Bỏ qua kiểm tra lỗi GL</string> + <string name="perf_noglerror_hint">Tắt kiểm tra hợp lệ GL theo từng lệnh gọi.</string> + <string name="perf_gsrealtime">gamescope: hàng đợi GPU thời gian thực</string> + <string name="perf_gsrealtime_hint">Ưu tiên công việc GPU của compositor. Có thể làm nhịp khung hình mượt hơn với một phần chi phí cho game.</string> + <string name="perf_gpu">GPU</string> + <string name="perf_gpuclock">Giữ GPU ở xung nhịp cao nhất</string> + <string name="perf_gpuclock_hint">Chỉ dành cho Adreno. Ít giật hơn, dùng nhiều pin và nhiệt hơn.</string> + <string name="perf_fixes">Các bản sửa phiên</string> + <string name="perf_sysmem">Turnip: render sysmem</string> + <string name="perf_sysmem_hint">Nhanh hơn cho Steam và hầu hết game. Bắt buộc trên Adreno 710/720/722.</string> + <string name="perf_xalia">Bỏ qua trình trợ giúp xalia của Steam</string> + <string name="perf_fastsync">droiddeck-ntsync (thử nghiệm)</string> + <string name="perf_fastsync_hint">Một bản thay thế ở không gian người dùng cho driver ntsync của Linux, dùng để thử nghiệm. Khi đang bật, Proton dùng nó thay cho droiddeck-esync và droiddeck-fsync. Hoạt động với Proton Experimental ARM64, GE-Proton và proton-cachyos. Áp dụng từ phiên sau.</string> + <string name="perf_esync">droiddeck-esync</string> + <string name="perf_fsync">droiddeck-fsync trước</string> + <string name="perf_fsync_hint">Chạy fsync riêng của Proton trên các tệp Proton gốc, với futex_waitv được cung cấp ở không gian người dùng, nên không cần pack và vẫn hoạt động ngay khi Steam cập nhật Proton. Khi đang bật, game dùng nó trước droiddeck-esync, trừ một số ít Proton giữ fsync tắt. Áp dụng từ phiên sau.</string> + <string name="perf_esync_hint">Các luồng Windows của game chờ nhau qua eventfd và bộ nhớ dùng chung, thay vì đi vòng qua wineserver. Dùng pack droiddeck-esync được làm riêng cho đúng bản build Proton đó; pack được tự động tải về. Cho đến khi có pack cho một bản build Proton mới, droiddeck-fsync sẽ thay thế. Áp dụng từ phiên sau.</string> + <string name="perf_xalia_hint">Tắt trình hỗ trợ điều hướng bằng tay cầm của Proton, vốn tốn CPU trong mọi game.</string> + <string name="perf_seccomp">Chạy proot không có seccomp</string> + <string name="perf_seccomp_hint">Có thể sửa lỗi thiếu syscall trên một số kernel. Có thể giảm hiệu năng.</string> + <string name="perf_fastpath">Fast path của proot</string> + <string name="perf_fastpath_needs_seccomp">Cần bộ lọc seccomp của proot.</string> + <string name="perf_fastpath_hint">Tra cứu tệp nhanh hơn và ít giật hơn. Tắt nếu một chương trình không tìm thấy tệp của nó.</string> + <string name="perf_identity">Danh tính phiên</string> + <string name="perf_hostname">Tên máy chủ</string> + <string name="perf_hostname_hint">Tên mà Steam thấy cho thiết bị này. Để trống để khôi phục %1$s.</string> + <string name="perf_hostname_invalid">Chỉ chữ, số và gạch ngang ở giữa, tối đa 63 ký tự.</string> + <string name="perf_phantom_warning">Android được đặt để tắt phiên này</string> + <string name="driver_installed">Đã cài</string> + <string name="driver_available">Có thể tải</string> + <string name="driver_nothing_new">Không có gì mới từ lần kiểm tra trước.</string> + <string name="driver_restore_builtin">Khôi phục driver tích hợp</string> + <string name="driver_delete_builtin">Được tích hợp trong ứng dụng: việc này xóa các tệp đã giải nén của nó và ẩn nó đi. Khôi phục driver tích hợp sẽ đưa nó trở lại.</string> + <string name="driver_delete_bundle">Một nửa của gói Android + Linux: cả hai nửa đều bị xóa. Driver đang dùng sẽ chuyển về mặc định.</string> + <string name="driver_delete_plain">Các tệp của nó bị xóa khỏi ứng dụng.</string> + <string name="driver_delete_in_use">Các tệp của nó bị xóa. Nó đang được dùng, nên mặc định sẽ thay thế.</string> + <string name="drawer_page_display">Hiển thị</string> + <string name="drawer_page_effects">Hiệu ứng</string> + <string name="drawer_page_controls">Điều khiển</string> + <string name="drawer_page_components">Thành phần</string> + <string name="drawer_page_controller">Tay cầm</string> + <string name="drawer_page_settings">Thiết lập</string> + <string name="drawer_page_desc">Trang %1$s</string> + <string name="drawer_steam">Steam</string> + <string name="drawer_desktop">Desktop</string> + <string name="drawer_steam_menu">Menu Steam</string> + <string name="drawer_quick_access">Truy cập nhanh</string> + <string name="drawer_open_qam">Mở Quick Access Menu</string> + <string name="drawer_prev_page">Trang trước</string> + <string name="drawer_next_page">Trang sau</string> + <string name="drawer_hud">HUD hiệu năng</string> + <string name="drawer_scaling">Chế độ chia tỷ lệ</string> + <string name="drawer_scaling_sharpness">Độ sắc nét khi chia tỷ lệ</string> + <string name="drawer_effects">Hiệu ứng màn hình</string> + <string name="drawer_effects_hint">Thời gian thực. Tốn một ít thời gian GPU khi có hiệu ứng đang bật.</string> + <string name="drawer_look">Thiết lập dựng sẵn</string> + <string name="drawer_look_custom">Tùy chỉnh</string> + <string name="drawer_effects_advanced">Nâng cao</string> + <string name="widgets_expanded">Mở rộng</string> + <string name="widgets_collapsed">Thu gọn</string> + <string name="drawer_cas">Làm nét (CAS)</string> + <string name="drawer_cas_level">Mức làm nét</string> + <string name="drawer_fake_hdr">HDR giả</string> + <string name="drawer_deband">Khử dải màu</string> + <string name="drawer_deband_strength">Cường độ khử dải màu</string> + <string name="drawer_brightness">Độ sáng</string> + <string name="drawer_contrast">Độ tương phản</string> + <string name="drawer_gamma">Gamma</string> + <string name="drawer_saturation">Độ bão hòa</string> + <string name="drawer_fxaa">FXAA (làm mượt cạnh)</string> + <string name="drawer_toon">Viền hoạt hình</string> + <string name="drawer_crt">CRT</string> + <string name="drawer_ntsc">NTSC</string> + <string name="drawer_texture">Lọc texture</string> + <string name="drawer_texture_hint">Game DirectX 9–11, từ lần khởi chạy kế tiếp.</string> + <string name="drawer_anisotropy">Lọc dị hướng (anisotropic)</string> + <string name="drawer_texture_sharpness">Độ sắc nét texture</string> + <string name="drawer_texture_sharpness_note">Một độ lệch mip âm: texture sắc nét hơn, có thể nhấp nháy nhẹ. Tự động khớp với mức phiên được phóng to so với màn hình.</string> + <string name="drawer_touch_auto">Tự động (%1$s)</string> + <string name="drawer_keyboard">Bàn phím</string> + <string name="drawer_pc_keyboard">Bàn phím PC</string> + <string name="drawer_android_keyboard">Android</string> + <string name="drawer_second_screen">Màn hình thứ hai</string> + <string name="drawer_second_screen_mode">Chế độ</string> + <string name="drawer_second_screen_shows">Hiển thị</string> + <string name="drawer_android_apps">Ứng dụng Android</string> + <string name="drawer_launch_app">Khởi chạy một ứng dụng</string> + <string name="common_hide">Ẩn</string> + <string name="common_show">Hiện</string> + <string name="drawer_no_apps">Không có ứng dụng nào để khởi chạy</string> + <string name="drawer_session_behavior">Hành vi phiên</string> + <string name="drawer_suspend_hint">Áp dụng ngay và cho các phiên sau ở chế độ này.</string> + <string name="drawer_next_session">Phiên sau</string> + <string name="drawer_support">Hỗ trợ</string> + <string name="drawer_logs">Log phiên</string> + <string name="drawer_logs_hint">Đính kèm vào báo cáo lỗi</string> + <string name="drawer_share_logs">Chia sẻ log</string> + <string name="drawer_background">Nền</string> + <string name="drawer_stop">Dừng</string> + <string name="drawer_stop_title">Dừng phiên?</string> + <string name="drawer_running_now">Đang chạy</string> + <string name="drawer_no_game">Không có game nào đang chạy trên một Proton</string> + <string name="drawer_selected_profile">Hồ sơ game</string> + <string name="drawer_selected_profile_none">Chi tiết hồ sơ chưa khả dụng.</string> + <string name="drawer_profile_refresh_failed_initial">Không thể tải hồ sơ game. Đang tự động thử lại.</string> + <string name="drawer_profile_refresh_failed_stale">Không thể làm mới hồ sơ game. Đang hiện dữ liệu khả dụng gần nhất; đang tự động thử lại.</string> + <string name="drawer_profile_inspect_hint">Xem và sửa các thiết lập đã lưu được dùng ở lần khởi chạy kế tiếp.</string> + <string name="drawer_profile_default">Đang dùng Hồ sơ mặc định</string> + <string name="drawer_profile_custom">Hồ sơ tùy chỉnh</string> + <string name="drawer_component_inherited">Đang dùng %1$s từ Hồ sơ mặc định.</string> + <string name="drawer_game_components_note">Thay đổi được lưu vào hồ sơ của %1$s và áp dụng vào lần game khởi chạy kế tiếp.</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="other">%1$d ghi đè game</item> + </plurals> + <string name="drawer_no_proton">Chưa cài Proton nào.</string> + <string name="drawer_game_running">game đang chạy</string> + <string name="drawer_reapplied">đã áp dụng lại lúc khởi chạy %1$s</string> + <string name="drawer_original">Gốc</string> + <string name="drawer_components_note">Nhập và xóa ở trang Components.</string> + <string name="setup_check_done">Hoàn tất</string> + <string name="setup_check_warn">Cần chú ý</string> + <string name="setup_check_busy">Đang xử lý</string> + <string name="setup_limit_adb_failed">Kiểm tra địa chỉ IP và cổng của Gỡ lỗi qua mạng không dây, hoặc ghép nối lại nếu Android đã gỡ thiết bị này.</string> + <string name="setup_install">Cài</string> + <string name="setup_manage">Quản lý</string> + <string name="setup_tab_overview">Tổng quan</string> + <string name="setup_tab_controller">Tay cầm</string> + <string name="setup_tab_session">Phiên</string> + <string name="setup_tab_launcher">Launcher</string> + <string name="setup_title">Thiết lập</string> + <string name="setup_all_set">● Đã sẵn sàng</string> + <string name="setup_n_ready">%1$d trong %2$d đã sẵn sàng</string> + <string name="setup_system_check">Kiểm tra hệ thống</string> + <string name="setup_gpu_supported">Thiết bị được hỗ trợ</string> + <string name="setup_gpu_untested">GPU chưa được kiểm thử</string> + <string name="setup_gpu_unsupported">GPU không được hỗ trợ</string> + <string name="setup_gpu_detail">%1$s · %2$s</string> + <string name="setup_gpu_untested_detail">%1$s: %2$s</string> + <string name="setup_gpu_unsupported_detail">Steam cần driver Adreno; %1$s có thể hiện màn hình đen</string> + <string name="setup_runtime">Runtime Linux</string> + <string name="setup_runtime_progress">%1$s · %2$d%%</string> + <string name="setup_runtime_missing">Chưa cài · khoảng 3 GB, được cài vào lần Play đầu tiên</string> + <string name="setup_installed">Đã cài</string> + <string name="setup_runtime_update">%1$s · có cập nhật</string> + <string name="setup_runtime_current">%1$s · đã mới nhất</string> + <string name="setup_limit">Giới hạn tiến trình con</string> + <string name="setup_limit_on">Đang bật · Android có thể đóng Steam. Tắt nó mất khoảng một phút.</string> + <string name="setup_limit_unset">Chưa đặt · Android có thể đóng Steam. Tắt nó mất khoảng một phút.</string> + <string name="setup_limit_unknown">Không thể kiểm tra · tắt nó mất khoảng một phút.</string> + <string name="setup_fix_it">Khắc phục</string> + <string name="setup_details">Chi tiết</string> + <string name="setup_dev_options">Tùy chọn cho nhà phát triển</string> + <string name="setup_use_wireless">Dùng Gỡ lỗi qua mạng không dây</string> + <string name="setup_check_again">Kiểm tra lại</string> + <string name="setup_limit_turn_on">Bật giới hạn</string> + <string name="setup_account">Tài khoản Steam</string> + <string name="setup_signed_in_offline">Đã đăng nhập với %1$s · chế độ ngoại tuyến</string> + <string name="setup_signed_in">Đã đăng nhập với %1$s</string> + <string name="setup_sign_in">Nhấn Play và đăng nhập vào Steam</string> + <string name="setup_tools">Công cụ</string> + <string name="setup_controller">Tay cầm</string> + <string name="setup_controller_unavailable">Thiết lập tay cầm không khả dụng.</string> + <string name="setup_session">Phiên</string> + <string name="setup_logs">Log phiên</string> + <string name="setup_logs_hint">Được lưu sau mỗi phiên</string> + <string name="setup_agent_commands">Công cụ gỡ lỗi</string> + <string name="setup_latest_logs">Log phiên gần nhất</string> + <string name="setup_saved_logs">Log phiên đã lưu</string> + <string name="setup_saved_logs_hint">Giữ lại %1$d bản mới nhất</string> + <string name="setup_clear_logs">Xóa log</string> + <string name="setup_offline">Chế độ ngoại tuyến</string> + <string name="setup_sign_in_first">Đăng nhập Steam trước</string> + <string name="setup_launcher">Launcher</string> + <string name="setup_language">Ngôn ngữ</string> + <string name="setup_language_hint">Steam cũng dùng ngôn ngữ này, từ lần khởi động kế tiếp.</string> + <string name="setup_language_system">Theo hệ thống (%1$s)</string> + <string name="setup_language_system_short">Theo hệ thống</string> + <string name="setup_app_scale">Tỷ lệ ứng dụng</string> + <string name="setup_app_scale_hint">Văn bản và điều khiển của DroidDeck</string> + <string name="setup_app_scale_default">%1$d%% (Mặc định)</string> + <string name="setup_theme">Giao diện</string> + <string name="setup_theme_hint">Giao diện launcher</string> + <string name="setup_home">Dùng làm màn hình chính</string> + <string name="setup_home_on">DroidDeck có thể là ứng dụng Màn hình chính</string> + <string name="setup_home_off">DroidDeck không bao giờ được đề xuất làm ứng dụng Màn hình chính</string> + <string name="setup_orientation">Hướng màn hình</string> + <string name="setup_orientation_auto">Ngang tự động</string> + <string name="setup_orientation_landscape">Ngang</string> + <string name="setup_orientation_reverse">Ngang ngược</string> + <string name="setup_fullscreen">Toàn màn hình</string> + <string name="setup_fullscreen_on">Ẩn thanh trạng thái và thanh điều hướng của Android</string> + <string name="setup_fullscreen_off">Hiện thanh trạng thái và thanh điều hướng của Android</string> + <string name="setup_default_home">Ứng dụng Màn hình chính mặc định</string> + <string name="setup_choose_home">Chọn một ứng dụng Màn hình chính</string> + <string name="setup_choose">Chọn</string> + <string name="setup_linux_apps">Ứng dụng Linux (beta)</string> + <string name="setup_tool_files">Tệp</string> + <string name="setup_tool_files_hint">Duyệt và quản lý tệp</string> + <string name="setup_tool_protons">Phiên bản Proton</string> + <string name="setup_tool_protons_hint">Cài các bản build Proton ARM64</string> + <string name="setup_tool_performance">Hiệu năng</string> + <string name="setup_tool_performance_hint">Phân bổ lõi CPU</string> + <string name="setup_tool_roms">Thư mục ROM</string> + <string name="setup_tool_roms_hint">Nơi lưu game của trình giả lập</string> + <string name="setup_card_components">Thành phần</string> + <string name="setup_card_components_hint">FEX, DXVK, VKD3D</string> + <string name="setup_card_controls">Điều khiển</string> + <string name="setup_card_controls_hint">Gán nút</string> + <string name="store_tab_discover">Khám phá</string> + <string name="store_tab_search">Tìm kiếm</string> + <string name="store_tab_installed">Đã cài</string> + <string name="store_tab_count">%1$s · %2$d</string> + <string name="store_title">Store</string> + <string name="store_chip_flathub">Flathub · ARM64</string> + <string name="store_chip_ready">● Flatpak đã sẵn sàng</string> + <string name="store_needs_runtime">Cài runtime Linux từ Thiết lập trước.</string> + <string name="store_setup_title">Thiết lập Flatpak</string> + <string name="store_setup_text">Thêm Flatpak và kết nối nó với Flathub (khoảng 20 MB). Ứng dụng đầu tiên cũng tải runtime của nó.</string> + <string name="store_setting_up">Đang thiết lập…</string> + <string name="store_flatpak">Flatpak</string> + <string name="store_updates">Cập nhật</string> + <string name="store_busy">%1$s · %2$s</string> + <string name="store_busy_percent">%1$s · %2$s · %3$d%%</string> + <string name="store_starting">Đang khởi động…</string> + <string name="store_categories">Danh mục</string> + <string name="store_unreachable">Không thể kết nối Flathub. Kiểm tra kết nối của bạn và thử lại.</string> + <string name="store_try_again">Thử lại</string> + <string name="store_search_label">Tìm trên Flathub</string> + <string name="store_search_placeholder">Firefox, game, trình giả lập…</string> + <string name="store_search">Tìm kiếm</string> + <string name="store_all">Tất cả</string> + <string name="store_searching">Đang tìm…</string> + <string name="store_search_hint">Tìm theo tên hoặc chọn một danh mục. Chỉ các ứng dụng ARM64 được liệt kê.</string> + <string name="store_no_results">Không có ứng dụng ARM64 nào trên Flathub khớp với nội dung đó.</string> + <string name="store_results">Kết quả</string> + <string name="store_check_updates">Kiểm tra cập nhật</string> + <string name="store_update_all">Cập nhật tất cả (%1$d)</string> + <string name="store_all_current">● Mọi thứ đã mới nhất</string> + <string name="store_no_apps">Chưa có ứng dụng nào. Tìm vài ứng dụng ở Khám phá hoặc Tìm kiếm.</string> + <string name="store_setup_first_note">Thiết lập Flatpak để cài ứng dụng.</string> + <string name="store_update_available_prefix">Có cập nhật · %1$s</string> + <string name="store_open">Mở</string> + <string name="store_details">Chi tiết</string> + <string name="store_remove">Xóa</string> + <string name="store_press_again">Nhấn lại để xóa</string> + <string name="store_installing">Đang cài…</string> + <string name="store_install">Cài</string> + <string name="store_setup_first">Thiết lập Flatpak trước</string> + <string name="store_download_size">Tải %1$s</string> + <string name="store_installed_chip">● Đã cài</string> + <string name="store_details_failed">Không thể tải trang Flathub của ứng dụng này.</string> + <string name="store_loading">Đang tải từ Flathub…</string> + <string name="store_no_arm">Không có bản build ARM64 trên Flathub, nên không thể cài ở đây.</string> + <string name="store_screenshots">Ảnh chụp màn hình</string> + <string name="store_about">Giới thiệu</string> + <string name="store_app_id">ID ứng dụng</string> + <string name="store_license">Giấy phép</string> + <string name="store_runtime">Runtime</string> + <string name="store_installed_size">Kích thước đã cài</string> + <string name="store_installed_size_value">%1$s (cộng với runtime dùng chung của nó)</string> + <string name="store_website">Trang web</string> + <string name="store_verified">Đã xác minh</string> + <string name="upd_title">Cập nhật</string> + <string name="upd_build">DroidDeck %1$s · %2$s</string> + <string name="upd_checked">Đã kiểm tra %1$s</string> + <string name="upd_not_checked">Chưa kiểm tra</string> + <string name="upd_perm_title">Cho phép DroidDeck tự cập nhật</string> + <string name="upd_perm_text">Ở màn hình kế tiếp, bật Cho phép từ nguồn này, rồi quay lại. Việc cập nhật sẽ tự tiếp tục.</string> + <string name="upd_open_settings">Mở thiết lập</string> + <string name="upd_not_now">Không phải bây giờ</string> + <string name="upd_signed_status">Ký khác nhau</string> + <string name="upd_signed_headline">Không thể cập nhật tại chỗ</string> + <string name="upd_signed_detail">Bản này không được ký bằng khóa phát hành của DroidDeck, nên bản cập nhật không thể cài chồng lên nó. Gỡ cài đặt nó để chuyển.</string> + <string name="upd_available">Có cập nhật</string> + <string name="upd_blocked_status">Không thể cài</string> + <string name="upd_switch">Sẵn sàng chuyển</string> + <string name="upd_ahead_status">Trước Stable</string> + <string name="upd_ahead_headline">Bạn đang trước Stable</string> + <string name="upd_ahead_detail">Bản build này mới hơn bản Stable gần nhất. Bạn sẽ chuyển sang Stable ở bản phát hành kế tiếp của nó.</string> + <string name="upd_gone_status">Thử nghiệm đã kết thúc</string> + <string name="upd_gone_headline">Thử nghiệm này đã kết thúc</string> + <string name="upd_gone_detail">Bản sửa của nó đã được gộp hoặc bỏ. Theo dõi Preview để có các bản sửa mới nhất.</string> + <string name="upd_current_status">Đã mới nhất</string> + <string name="upd_current_headline">Bạn đã có bản %1$s mới nhất</string> + <string name="upd_dot_ago">· %1$s</string> + <string name="upd_stage_percent">%1$s… %2$d%%</string> + <string name="upd_stage">%1$s…</string> + <string name="upd_update_button">Cập nhật%1$s</string> + <string name="upd_install_button">Cài%1$s</string> + <string name="upd_install_anyway">Vẫn cài Stable %1$s</string> + <string name="upd_follow_preview">Theo dõi Preview</string> + <string name="upd_stop_session">Dừng phiên đang chạy để cập nhật.</string> + <string name="upd_less">Ít hơn</string> + <string name="upd_more">Thêm</string> + <string name="upd_new_stable">DroidDeck %1$s</string> + <string name="upd_new_preview">Bản Preview mới</string> + <string name="upd_new_test">Bản thử nghiệm PR #%1$d</string> + <string name="upd_mb">%1$d MB</string> + <string name="upd_channel">KÊNH CẬP NHẬT</string> + <string name="upd_stable">Stable</string> + <string name="upd_stable_hint">Các bản phát hành đã kiểm thử, dành cho hầu hết người dùng</string> + <string name="upd_version_ago">%1$s · %2$s</string> + <string name="upd_preview">Preview</string> + <string name="upd_preview_hint">Các bản sửa mới nhất, trước Stable</string> + <string name="upd_preview_current">Đã mới nhất</string> + <string name="upd_tests">Bản thử nghiệm</string> + <string name="upd_tests_hint">Thử một bản sửa trước khi nó được phát hành</string> + <string name="upd_tests_none">Hiện không có</string> + <string name="upd_pr">PR #%1$d</string> + <string name="upd_not_built">Không được build cho bản DroidDeck này</string> + <string name="upd_updated">Đã cập nhật %1$s</string> + <string name="upd_test_name">thử nghiệm PR #%1$d</string> + <string name="upd_while_ago">một lúc trước</string> + <string name="upd_just_now">vừa xong</string> + <plurals name="upd_tests_count"> + <item quantity="other">%1$d để thử</item> + </plurals> + <plurals name="upd_preview_behind"> + <item quantity="other">Thay đổi từ bản của bạn · chậm %1$d bản build</item> + </plurals> + <plurals name="upd_preview_recent"> + <item quantity="other">Thay đổi gần đây · %1$d bản build</item> + </plurals> + <string name="gate_title">Một thiết lập Android cần đổi</string> + <string name="gate_eyebrow">Steam</string> + <string name="gate_lede">Android dừng các ứng dụng khởi chạy nhiều tiến trình nền, và Steam khởi chạy hàng chục tiến trình. Cho đến khi thay đổi điều này, game sẽ đóng mà không có lỗi.</string> + <string name="gate_fix_for_me">Khắc phục giúp tôi</string> + <string name="gate_toggle_steps">Bật “Disable child process restrictions”, rồi quay lại. Giữ Tùy chọn cho nhà phát triển luôn mở, nếu không công tắc sẽ tắt lại.</string> + <string name="gate_open_dev">Mở Tùy chọn cho nhà phát triển</string> + <string name="gate_no_toggle">Phiên bản Android này không có công tắc cho việc đó, nên DroidDeck thay đổi nó qua Gỡ lỗi qua mạng không dây. Mất khoảng một phút, không cần máy tính.</string> + <string name="gate_needs_wifi">Gỡ lỗi qua mạng không dây cần Wi-Fi. Mạng nào cũng được; không cần Internet.</string> + <string name="gate_needs_notifications">Mã ghép nối nằm trong một thông báo. Hãy cho phép thông báo cho DroidDeck, hoặc dùng Cách khác.</string> + <string name="gate_allow_notifications">Cho phép thông báo</string> + <string name="gate_step1">Mở Settings → Developer options → Wireless debugging và bật nó lên</string> + <string name="gate_step2">Nhấn “Pair device with pairing code”</string> + <string name="gate_step3">Nhập mã vào thông báo của DroidDeck</string> + <string name="gate_waiting">Đang chờ cửa sổ bật lên ghép nối…</string> + <string name="gate_code_needed">Đã tìm thấy cửa sổ bật lên. Nhập mã của nó vào thông báo.</string> + <string name="gate_status">Trạng thái</string> + <string name="gate_polling">Đang kiểm tra mỗi 2 giây. Trang này sẽ đóng khi nó tắt.</string> + <string name="gate_other_ways">Cách khác</string> + <string name="gate_other_toggle">Gỡ lỗi qua mạng không dây, địa chỉ thủ công, ADB từ máy tính</string> + <string name="gate_other_no_toggle">Địa chỉ thủ công, ADB từ máy tính, Tùy chọn cho nhà phát triển</string> + <string name="gate_use_wireless_instead">Dùng Gỡ lỗi qua mạng không dây thay vào đó</string> + <string name="gate_some_roms">Một số ROM vẫn thêm công tắc này. Tìm “child process” trong Tùy chọn cho nhà phát triển.</string> + <string name="gate_manual_hint">Nếu thông báo không hoạt động, hãy tự nhập địa chỉ và mã của cửa sổ bật lên.</string> + <string name="gate_enter_manually">Nhập địa chỉ thủ công</string> + <string name="gate_from_computer">Từ một máy tính có ADB:</string> + <string name="gate_copy_command">Sao chép lệnh</string> + <string name="gate_choose_display">Chọn màn hình để mở Android Settings.</string> + <string name="gate_main_screen">Màn hình chính</string> + <string name="wadb_finding_port">Đã ghép nối. Đang tìm cổng Gỡ lỗi qua mạng không dây…</string> + <string name="wadb_enter_port">Đã ghép nối. Nhập cổng từ Settings.</string> + <string name="wadb_applying">Đã tìm thấy thiết bị. Đang áp dụng thiết lập…</string> + <string name="wadb_apply_failed">Không thể áp dụng tự động. Kiểm tra địa chỉ dưới đây và thử lại. %1$s</string> + <string name="wadb_title_wireless">Gỡ lỗi qua mạng không dây</string> + <string name="wadb_title_done">Đã cập nhật giới hạn tiến trình con</string> + <string name="wadb_title">Đổi giới hạn tiến trình con</string> + <string name="wadb_lede_compact0">Nếu thiếu tùy chọn này, hãy ghép nối qua Gỡ lỗi qua mạng không dây.</string> + <string name="wadb_lede_compact1">Đã ghép nối. Kiểm tra địa chỉ IP và cổng trong Gỡ lỗi qua mạng không dây.</string> + <string name="wadb_lede_done">Giới hạn tiến trình con đã được cập nhật.</string> + <string name="wadb_lede0">Dùng bước 2 nếu Tùy chọn cho nhà phát triển không có thiết lập tiến trình con. Mở Developer options → Wireless debugging → Pair device with pairing code.</string> + <string name="wadb_lede1">Đã ghép nối. Dùng địa chỉ dưới đây, hoặc cập nhật nó từ Wireless debugging → IP address & Port.</string> + <string name="wadb_lede_done_long">Giới hạn tiến trình con đã được cập nhật qua Gỡ lỗi qua mạng không dây.</string> + <string name="wadb_confirmed_on">Android xác nhận giới hạn đang bật.</string> + <string name="wadb_confirmed_off">Android xác nhận giới hạn đang tắt.</string> + <string name="wadb_pair_group">2 · Ghép nối Gỡ lỗi qua mạng không dây</string> + <string name="wadb_pair_address">Địa chỉ IP & cổng của cửa sổ bật lên ghép nối</string> + <string name="wadb_pin">Mã ghép nối (PIN)</string> + <string name="wadb_pin_placeholder">6 số</string> + <string name="wadb_pair">Ghép nối</string> + <string name="wadb_apply_group">2 · Áp dụng qua Gỡ lỗi qua mạng không dây</string> + <string name="wadb_connect_address">Địa chỉ IP & cổng kết nối</string> + <string name="wadb_pair_again">Ghép nối lại</string> + <string name="wadb_turn_off">Tắt giới hạn</string> + <string name="wadb_other_ways">Cách khác để đổi nó</string> + <string name="wadb_dev_try_first">1 · Tùy chọn cho nhà phát triển (thử trước)</string> + <string name="wadb_restrict_on">Đặt “Restrict child processes” thành bật.</string> + <string name="wadb_restrict_off">Đặt “Restrict child processes” thành tắt.</string> + <string name="wadb_computer_last">3 · ADB từ máy tính (phương án cuối)</string> + <string name="wadb_only_if">Chỉ khi Gỡ lỗi qua mạng không dây không khả dụng.</string> + <string name="wadb_computer_group">3 · ADB từ máy tính (phương án cuối)</string> + <string name="ctrl_osc_hint">Khi các nút điều khiển chạm xuất hiện trong một phiên</string> + <string name="ctrl_color">Màu</string> + <string name="ctrl_color_hint">Màu sắc của các nút trên màn hình</string> + <string name="ctrl_custom">Tùy chỉnh</string> + <string name="ctrl_opacity">Độ mờ</string> + <string name="ctrl_percent">%1$d%%</string> + <string name="ctrl_size">Kích thước nút</string> + <string name="ctrl_size_hint" formatted="false">100% là kích thước chuẩn</string> + <string name="ctrl_stick_click">Nhấn cần</string> + <string name="ctrl_stick_click_hint">Chạm hai lần và giữ một cần để dùng L3 hoặc R3</string> + <string name="ctrl_adaptive">Cần thích ứng</string> + <string name="ctrl_rumble">Rung khi chơi game</string> + <string name="ctrl_steam_button">Nút Steam</string> + <string name="ctrl_qam_button">Nút QAM</string> + <string name="ctrl_keyboard_button">Nút bàn phím</string> + <string name="ctrl_select_steam">Nhấn Select để mở menu Steam</string> + <string name="ctrl_select_steam_hint">Thay cho View/Select.</string> + <string name="setup_animations">Hiệu ứng chuyển động</string> + <string name="ctrl_adaptive_hint">Cần xuất hiện ở nơi bạn chạm và ẩn khi thả ra</string> + <string name="ctrl_layout">Bố cục</string> + <string name="ctrl_layout_custom">Vị trí tùy chỉnh đã lưu</string> + <string name="ctrl_layout_default">Vừa khớp với màn hình này</string> + <string name="ctrl_edit">Sửa</string> + <string name="ctrl_reset">Đặt lại</string> + <string name="ctrl_mapping">Gán nút</string> + <string name="ctrl_mapping_default">Mỗi nút gửi tín hiệu riêng của nó</string> + <string name="ctrl_mapping_count">%1$d trong %2$d nút đã được gán lại</string> + <string name="ctrl_configure">Cấu hình</string> + <string name="ctrl_reset_all">Đặt lại tay cầm</string> + <string name="ctrl_reset_all_hint">Khôi phục mọi thiết lập tay cầm và bố cục</string> + <string name="ctrl_mapping_lede">Mỗi nút trên màn hình gửi gì đến game. Ẩn sẽ xóa nút đó.</string> + <string name="ctrl_onscreen_buttons">Nút trên màn hình</string> + <string name="ctrl_defaults">Mặc định</string> + <string name="ctrl_reset_mapping">Đặt lại gán nút</string> + <string name="content_android_apps">Ứng dụng Android</string> + <string name="content_apps_count">%1$d ứng dụng</string> + <string name="content_no_android_apps">Không tìm thấy ứng dụng Android nào có thể khởi chạy.</string> + <string name="content_desktop_installed">● Đã cài desktop</string> + <string name="content_desktop_first_open">Cài ở lần mở đầu tiên</string> + <string name="content_available">Có thể cài</string> + <string name="content_not_installed">Chưa cài.</string> + <string name="content_not_installed_suffix">%1$s · chưa cài</string> + <string name="content_open_named">Mở %1$s</string> + <string name="content_games">Games</string> + <string name="content_choose_roms">Chọn một thư mục ROM.</string> + <string name="content_retroarch_roms">Duyệt đến /root/ROMs trong RetroArch.</string> + <string name="content_add_roms">Thêm game %1$s vào ROMs/%2$s.</string> + <string name="content_install_named">Cài %1$s</string> + <string name="content_stop_to_install">Dừng phiên để cài</string> + <string name="content_runtime_required">Cần runtime</string> + <string name="content_loading_details">Đang tải chi tiết cài đặt…</string> + <string name="content_details_unavailable">Chi tiết cài đặt không khả dụng. Thử lại sau.</string> + <string name="content_needs_runtime">Cài runtime Linux từ Thiết lập trước.</string> + <string name="content_stop_first">Dừng phiên đang chạy để cài ứng dụng desktop.</string> + <string name="content_install_into">Cài %1$s vào desktop Linux.</string> + <string name="store_busy_percent_short">%1$s · %2$d%%</string> + <string name="content_hide_note">Ẩn sẽ gỡ nó khỏi Desktop. Các tệp của nó vẫn ở trong runtime Linux.</string> + <string name="content_rom_gone">Game đó không còn trong thư mục ROM.</string> + <string name="content_desktop_system">Desktop · %1$s</string> + <string name="content_launch_in">Khởi chạy trong %1$s</string> + <string name="content_also_in">Cũng có trong %1$s</string> + <string name="content_page_gone">Trang đó không còn nữa. Chọn một mục ở bên trái.</string> + <string name="content_open_desktop">Mở desktop</string> + <string name="content_install_open_desktop">Cài & mở desktop</string> + <string name="desktop_remove_title">Gỡ cài đặt desktop</string> + <string name="desktop_remove_text">Gỡ desktop và các ứng dụng đi kèm. Giữ lại game, ứng dụng đã thêm, thiết lập và thư viện dùng chung.</string> + <string name="desktop_removing">Đang gỡ desktop</string> + <string name="desktop_removed">Đã gỡ desktop</string> + <string name="desktop_cleaning_legacy">Đang xóa tệp LXQt cũ</string> + <string name="desktop_removal_unknown">Cập nhật DroidDeck trước khi gỡ phiên bản desktop này.</string> + <string name="content_linux_desktop">Desktop Linux</string> + <string name="content_linux_desktop_hint">KDE Plasma, Firefox và các trình giả lập của bạn</string> + <string name="games_empty">Game bạn cài trong Steam sẽ xuất hiện ở đây sau phiên.</string> + <string name="games_play_steam">Chơi trên Steam</string> + <string name="games_launch_settings">Thiết lập khởi chạy</string> + <string name="games_launch">Khởi chạy</string> + <string name="games_files">Tệp game</string> + <string name="games_prefix">Prefix Proton</string> + <string name="games_saves_looking">Đang tìm các lần lưu…</string> + <string name="games_saves_none">Không tìm thấy thư mục lưu theo tên. Xuất sẽ lấy toàn bộ thư mục người dùng.</string> + <string name="games_saves_folders">%1$d thư mục lưu</string> + <string name="games_saves_summary">%1$s · %2$d tệp · %3$s</string> + <string name="games_kb">%1$d KB</string> + <string name="games_manage_saves">Quản lý tệp lưu</string> + <string name="games_saves">Tệp lưu game</string> + <string name="games_import_saves">Nhập tệp lưu…</string> + <string name="games_export_gamehub">Xuất dưới dạng zip GameHub…</string> + <string name="games_export_winlator">Xuất dưới dạng zip Winlator…</string> + <string name="games_open_save_folder">Mở thư mục lưu</string> + <string name="games_never_played">Chưa chơi bao giờ</string> + <string name="games_runtime_busy_percent">Runtime đang bận · %1$d%%</string> + <string name="games_runtime_busy">Runtime đang bận</string> + <string name="games_runtime_first_play">Runtime sẽ cài ở lần Play đầu tiên</string> + <string name="games_runtime_update">Có cập nhật runtime</string> + <string name="games_runtime_ready">● Runtime đã sẵn sàng</string> + <string name="games_last_played">Chơi lần cuối %1$s</string> + <string name="games_internal">Bộ nhớ trong</string> + <string name="games_added">Game đã thêm</string> + <string name="games_custom">Game tùy chỉnh</string> + <string name="games_store_storage">Lưu trữ game</string> + <string name="gpu_auto_hint">Cặp tốt nhất cho GPU này, luôn được cập nhật</string> + <string name="gpu_manual_hint">Tự chọn một cặp</string> + <string name="gpu_checking">Đang kiểm tra driver…</string> + <string name="gpu_none_listed">Chưa có driver nào được liệt kê. Làm mới để tìm.</string> + <string name="gpu_hide_other">Ẩn driver cho GPU khác</string> + <string name="gpu_show_other">Hiện driver cho GPU khác</string> + <string name="gpu_advanced">Nâng cao</string> + <string name="gpu_set_with_display">Được đặt cùng với driver hiển thị: %1$s mang cả hai.</string> + <string name="gpu_set_with_runtime">Được đặt cùng với driver runtime: %1$s mang cả hai.</string> + <string name="gpu_runtime_hint">Dùng bởi game và desktop.</string> + <string name="gpu_runtime_default">Mặc định runtime</string> + <string name="gpu_display_hint">Vẽ khung hình lên màn hình. Áp dụng sau khi DroidDeck khởi động lại.</string> + <string name="gpu_auto_by_gpu">Tự động - chọn theo GPU</string> + <string name="gpu_manage">Tải, nhập hoặc xóa…</string> + <string name="gpu_in_use_caps">ĐANG DÙNG</string> + <string name="gpu_builtin">Các driver tích hợp trong DroidDeck</string> + <string name="gpu_matched_pair">Driver runtime và hiển thị được đặt cùng nhau thành một cặp khớp.</string> + <string name="gpu_recommended_for">Được đề xuất cho GPU này: %1$s.</string> + <string name="gpu_refresh_hint">Làm mới để tìm driver cho GPU này.</string> + <string name="gpu_recommended_caps">ĐỀ XUẤT</string> + <string name="gpu_unavailable">Không khả dụng</string> + <string name="load_runtime">Runtime Linux</string> + <string name="load_runtime_detail">Đang chuẩn bị runtime Linux</string> + <string name="load_session">Phiên</string> + <string name="load_session_detail">Đang khởi động hiển thị và âm thanh</string> + <string name="load_steam">Client Steam</string> + <string name="load_steam_detail">Đang kiểm tra Steam và các công cụ tương thích của nó</string> + <string name="load_opening_steam">Đang mở Steam</string> + <string name="load_opening_steam_detail">Đang chờ khung hình đầu tiên của Steam</string> + <string name="load_desktop">Desktop</string> + <string name="load_desktop_detail">Đang chuẩn bị desktop</string> + <string name="load_opening">Đang mở</string> + <string name="load_opening_detail">Đang chờ khung hình đầu tiên</string> + <string name="load_starting_steam">Đang khởi động Steam</string> + <string name="load_ended">Phiên đã kết thúc</string> + <string name="load_step">Bước %1$d trong %2$d</string> + <string name="load_steam_paused">Steam đang tạm dừng</string> + <string name="deck_tab_grips">Grip</string> + <string name="deck_tab_left">Pad trái</string> + <string name="deck_tab_right">Pad phải</string> + <string name="deck_tab_both">Cả hai</string> + <string name="deck_tab_gamepad">Gamepad</string> + <string name="deck_surface_stick">Cần</string> + <string name="deck_surface_trackpad">Trackpad</string> + <string name="deck_surface_switch">Chuyển giữa cần và trackpad</string> + <string name="deck_more_controls">Thêm điều khiển Deck</string> + <string name="deck_panels">BẢNG</string> + <string name="deck_close">Đóng điều khiển màn hình phụ</string> + <string name="deck_open_steam_menu">Mở menu Steam</string> + <string name="deck_left_upper">Trái · trên</string> + <string name="deck_left_lower">Trái · dưới</string> + <string name="deck_right_upper">Phải · trên</string> + <string name="deck_right_lower">Phải · dưới</string> + <string name="deck_click">Nhấn</string> + <string name="deck_right_trackpad">Trackpad phải</string> + <string name="deck_left_trackpad">Trackpad trái</string> + <string name="second_trackpad">Trackpad</string> + <string name="second_show_keyboard">Hiện bàn phím</string> + <string name="second_hide_keyboard">Ẩn bàn phím</string> + <string name="second_terminal">Terminal Linux</string> + <string name="second_terminal_not_ready">Phiên Linux chưa sẵn sàng cho terminal.</string> + <string name="rail_steam">Steam</string> + <string name="rail_games">Games</string> + <string name="rail_desktop">Desktop</string> + <string name="rail_store">Store</string> + <string name="rail_components">Components</string> + <string name="rail_apps">Ứng dụng</string> + <string name="rail_setup">Thiết lập</string> + <string name="rail_updates">Cập nhật</string> + <string name="rail_percent">%1$d%%</string> + <string name="rail_resume_named">Tiếp tục %1$s</string> + <string name="roms_text">Hiển thị như /root/ROMs trong mỗi phiên. Bộ nhớ trong ở /root/Storage.</string> + <string name="roms_none">Chưa chọn thư mục</string> + <string name="roms_applies">Áp dụng từ phiên sau. Thẻ SD hoạt động được.</string> + <string name="roms_choose">Chọn thư mục</string> + <string name="roms_change">Đổi thư mục</string> + <string name="proton_lede">Cài một bản build Proton ARM64, sau đó chọn nó cho từng game trong Steam → Properties → Compatibility.</string> + <string name="proton_stop_first">Dừng phiên đang chạy để cài hoặc gỡ một công cụ tương thích.</string> + <string name="proton_available">Các bản build khả dụng</string> + <string name="proton_installed">Đã cài %1$s</string> + <string name="proton_queued">Đang chờ từ một yêu cầu trước</string> + <string name="proton_not_installed">Chưa cài</string> + <string name="proton_update_available">Có cập nhật → %1$s</string> + <string name="proton_remove_older">Xóa các phiên bản %1$s cũ</string> + <string name="proton_remove_older_body">Xóa %1$s. Game đang đặt dùng các phiên bản này phải chuyển sang một Proton khác.</string> + <string name="proton_remove_body">Game đang đặt dùng phiên bản này phải chuyển sang một Proton khác.</string> + <string name="proton_install_now">Cài ngay</string> + <string name="proton_cancel_queued">Hủy hàng chờ</string> + <string name="pckb_android">Bàn phím Android</string> + <string name="pckb_hide">Ẩn ✕</string> + <string name="main_launch_named">Khởi chạy %1$s</string> + <string name="main_choose_screen">Chọn một màn hình. Phụ: %1$s.</string> + <string name="main_secondary_gone">Màn hình phụ không còn khả dụng.</string> + <string name="main_primary">Màn hình chính</string> + <string name="main_secondary">Màn hình phụ</string> + <string name="widgets_prev_tab">Tab trước</string> + <string name="widgets_next_tab">Tab sau</string> + <string name="widgets_on">Bật</string> + <string name="widgets_all_n">Tất cả %1$d</string> + <string name="widgets_none">Không có</string> + <string name="widgets_n_of_m">%1$d trong %2$d</string> + <string name="widgets_off">Tắt</string> + <string name="screen_bottom">Màn hình dưới</string> + <string name="game_env_applies">Áp dụng vào lần game khởi động kế tiếp. Hồ sơ của một game sẽ ghi đè hồ sơ dùng chung.</string> + <string name="game_env_other">Một game khác theo app ID…</string> + <string name="game_env_done">Xong</string> + <string name="game_env_default">Mặc định</string> + <string name="game_env_off">Tắt</string> + <string name="game_env_change">Đổi…</string> + <string name="game_env_change_title">Đổi biến</string> + <string name="game_env_restore_default">Quay về mặc định</string> + <string name="game_env_eyebrow">Game Proton</string> + <string name="pip_title">Hình trong hình</string> + <string name="pip_auto">Tự động vào hình trong hình</string> + <string name="unfolded_controls">Điều khiển trên màn hình mở</string> + <string name="unfolded_controls_hint">Khi mở, game nằm phía trên trackpad và grip phía sau của Steam Deck. Khi gập, game chiếm toàn màn hình.</string> + <string name="pip_suspended">Đã tạm dừng</string> + <string name="pip_not_ready">Hình trong hình chỉ khả dụng khi phiên đã sẵn sàng.</string> + <string name="pip_permission">Cho phép hình trong hình cho DroidDeck trong thiết lập Android.</string> + <string name="pip_unavailable">Android không thể mở hình trong hình.</string> + <string name="game_shortcut">Shortcut</string> + <string name="game_shortcut_add">Thêm vào màn hình chính</string> + <string name="game_shortcut_choose">Chọn game</string> + <string name="game_shortcut_scanning">Đang quét game đã cài…</string> + <string name="game_link_copy">Sao chép liên kết khởi chạy</string> + <string name="game_link_copied">Đã sao chép liên kết khởi chạy</string> + <string name="game_shortcut_unsupported">Launcher này không thể thêm shortcut. Hãy dùng Sao chép liên kết khởi chạy.</string> + <string name="game_link_invalid">Liên kết khởi chạy game không hợp lệ.</string> + <string name="game_link_missing">Game này không còn trong thư viện DroidDeck của bạn.</string> + <string name="game_link_session_busy">Dừng phiên hiện tại trước khi khởi chạy một game Steam.</string> + <string name="game_link_failed">Không thể gửi game đến Steam. Hãy thử khởi chạy lại.</string> + <string name="setup_retry_removal">Thử gỡ lại</string> + <string name="runtime_removing">Đang gỡ</string> + <string name="runtime_removal_incomplete">Gỡ chưa hoàn tất</string> + <string name="game_file_export">Xuất tệp game</string> + <string name="game_file_sync">Đồng bộ tất cả tệp game vào thư mục</string> + <string name="game_file_stop_sync">Ngừng đồng bộ tệp game</string> + <string name="game_file_folder">Chọn một thư mục cho các tệp game frontend</string> + <string name="game_file_exported">Đã lưu tệp game vào %1$s</string> + <string name="game_frontend_files">Tệp frontend</string> + + <string name="main_wait_runtime">Chờ hoạt động runtime hoàn tất</string> + <string name="main_wait_proton_install">Chờ việc cài công cụ tương thích hoàn tất</string> + <string name="main_wait_desktop_install">Chờ việc cài ứng dụng desktop hoàn tất</string> + <string name="main_wait_install_game">Chờ việc cài hoàn tất trước khi khởi chạy một game</string> + <string name="main_saves_close_session">Hãy đóng phiên Steam trước, để game không ghi đè lên tệp vừa nhập</string> + <string name="main_saves_importing">Đang nhập vào %1$s</string> + <plurals name="main_saves_imported"> + <item quantity="other">Đã nhập %1$d tệp từ %2$s vào %3$s</item> + </plurals> + <plurals name="main_saves_imported_backup"> + <item quantity="other">Đã nhập %1$d tệp từ %2$s vào %3$s. Tệp lưu cũ đã được sao lưu vào Download/DroidDeck/Saves/backups</item> + </plurals> + <string name="main_saves_pick_zip">Chọn một tệp zip lưu cho %1$s</string> + <string name="main_saves_exporting">Đang xuất %1$s</string> + <plurals name="main_saves_exported"> + <item quantity="other">Đã xuất %1$d tệp dưới dạng %2$s: %3$s</item> + </plurals> + <string name="main_saves_pick_folder">Chọn nơi lưu %1$s (%2$s)</string> + <string name="main_action_failed">%1$s thất bại: %2$s</string> + <string name="main_no_logs_yet">Chưa có log phiên: hãy chạy một phiên trước.</string> + <string name="main_no_logs_to_clear">Không có log phiên để xóa.</string> + <plurals name="main_logs_cleared"> + <item quantity="other">Đã xóa %1$d thư mục log phiên.</item> + </plurals> + <string name="main_wadb_pair_failed">Ghép nối Gỡ lỗi qua mạng không dây thất bại</string> + <string name="main_wadb_command_failed">Lệnh Gỡ lỗi qua mạng không dây thất bại</string> + <string name="main_wadb_unavailable">Gỡ lỗi qua mạng không dây không khả dụng</string> + <string name="main_adb_copied">Đã sao chép lệnh ADB</string> + <string name="main_pick_roms">Chọn thư mục ROM</string> + <string name="main_non_adreno_title">Không phải GPU Adreno</string> + <string name="main_non_adreno_text">Turnip hỗ trợ GPU Adreno. Trên %1$s, Steam có thể hiện màn hình đen. Tải xuống: %2$.0f MB.</string> + <string name="main_install_anyway">Vẫn cài</string> + <string name="main_remove_runtime_title">Gỡ runtime Linux</string> + <string name="main_remove_runtime_text">Việc này xóa runtime, client Steam bên trong nó, và mọi game đã cài ở đó.</string> + <string name="main_applied">Đã áp dụng: %1$s</string> + <string name="android_app_open_failed_primary">Không thể mở %1$s trên màn hình chính</string> + <string name="android_app_open_failed_on">Không thể mở %1$s trên %2$s</string> + <string name="android_app_open_failed_display">Không thể mở %1$s trên màn hình %2$d</string> + <string name="comp_busy_importing">Đang nhập</string> + <string name="comp_busy_swapping">Đang thay</string> + <string name="comp_busy_restoring">Đang khôi phục</string> + <string name="comp_busy_cancelling">Đang hủy</string> + <string name="comp_busy_deleting">Đang xóa</string> + <string name="comp_cannot_read">không thể đọc tệp</string> + <string name="comp_imported">Đã nhập %1$s</string> + <string name="comp_swap_cancelled">Lượt thay đang chờ đã bị hủy.</string> + <string name="comp_pick_wcp">Chọn một gói thành phần (-linux .wcp)</string> + <string name="comp_nightlies_unreachable">Không thể kết nối Nightlies</string> + <string name="comp_stored">Đã lưu %1$s</string> + <string name="comp_download_failed">Tải xuống thất bại: %1$s</string> + <string name="drivers_pick_linux">Chọn một driver runtime Linux (zip -Linux hoặc gói Android + Linux)</string> + <string name="drivers_pick_android">Chọn một driver hiển thị (zip AdrenoTools hoặc gói Android + Linux)</string> + <string name="drivers_pick_any">Chọn một zip driver hoặc một gói Android + Linux</string> + <string name="main_pkg_problem">%1$s: %2$s</string> + <string name="main_removing_named">Đang gỡ %1$s…</string> + <string name="main_forgetting_named">Đang quên %1$s…</string> + <string name="main_pick_game_storage">Chọn thư mục lưu trữ game</string> + <string name="main_pick_decky_zip">Chọn một tệp ZIP plugin Decky</string> + <string name="main_storage_not_usable">Không thể dùng: %1$s</string> + <string name="main_running_program">Chương trình</string> + <string name="main_removing_runtime">Đang gỡ runtime Linux</string> + <string name="main_settings_main_screen">Không thể mở Settings trên màn hình dưới; đã mở nó trên màn hình chính.</string> + <string name="main_open_dev_options">Mở Developer options trong Settings</string> + + <string name="upd_unknown_apps_failed">Không thể mở thiết lập \"Install unknown apps\" của Android</string> + <string name="upd_couldnt_check">Không thể kiểm tra: %1$s</string> + <string name="upd_no_download">Bản build này không có tệp tải cho bản DroidDeck này</string> + <string name="upd_stop_first">Dừng phiên đang chạy trước, rồi cập nhật</string> + <string name="upd_stage_downloading">Đang tải</string> + <string name="upd_stage_installing">Đang cài</string> + <string name="upd_stage_confirm">Đang chờ bạn xác nhận</string> + <string name="upd_failed">Cập nhật thất bại</string> + <string name="upd_not_offered">Android không đề nghị cài bản cập nhật</string> + + <string name="decky_install_failed">Cài Decky thất bại: %1$s</string> + <string name="decky_plugin_starting">Đang bắt đầu nhập plugin…</string> + <string name="decky_plugin_failed">Nhập plugin thất bại: %1$s</string> + <string name="decky_plugin_installed">Đã cài plugin. Khởi động lại Steam để tải nó.</string> + + <string name="drivers_not_checked">Chưa kiểm tra - nhấn làm mới để tìm driver mới</string> + <string name="drivers_bundle_sets_display">Android + Linux: cùng đặt driver hiển thị</string> + <string name="drivers_bundle_sets_runtime">Android + Linux: cùng đặt driver runtime</string> + <string name="drivers_runtime_builtin">Turnip tích hợp trong runtime</string> + <string name="drivers_system_vulkan">Vulkan hệ thống: không có bản build đi kèm cho GPU này</string> + <string name="drivers_bundled">%1$s (đi kèm)</string> + <string name="drivers_manual">Thủ công</string> + <string name="drivers_summary">%1$s · %2$s</string> + <string name="drivers_half_published">Hiện chỉ một nửa được phát hành</string> + <string name="drivers_pair_size">%1$.0f MB cho cả hai</string> + <string name="drivers_none_to_set">Không có driver để đặt: %1$s</string> + <string name="drivers_checking_latest">Đang kiểm tra driver mới nhất…</string> + <string name="drivers_up_to_date_check_failed">Đã mới nhất (không thể kiểm tra: %1$s)</string> + <string name="drivers_recommended_unpublished">Các driver được đề xuất hiện không được phát hành</string> + <string name="drivers_downloading_pair">Đang tải %1$s %2$s…</string> + <string name="drivers_updated_to">Đã cập nhật lên %1$s %2$s.</string> + <string name="drivers_updated_to_restart">Đã cập nhật lên %1$s %2$s. Driver hiển thị áp dụng sau khi DroidDeck khởi động lại.</string> + <string name="drivers_switched_to">Đã chuyển sang %1$s %2$s.</string> + <string name="drivers_switched_to_restart">Đã chuyển sang %1$s %2$s. Driver hiển thị áp dụng sau khi DroidDeck khởi động lại.</string> + <string name="drivers_using">Đang dùng %1$s %2$s.</string> + <string name="drivers_using_restart">Đang dùng %1$s %2$s. Driver hiển thị áp dụng sau khi DroidDeck khởi động lại.</string> + <string name="drivers_not_a_driver">Không phải zip driver: không phải driver AdrenoTools, Turnip -Linux, cũng không phải gói Android + Linux</string> + <string name="drivers_import_failed">Nhập thất bại: %1$s</string> + <string name="drivers_imported_unnamed">Driver đã nhập</string> + <string name="drivers_using_bundle">Đang dùng %1$s làm cả driver runtime và hiển thị.</string> + <string name="drivers_using_bundle_restart">Đang dùng %1$s làm cả driver runtime và hiển thị. Driver hiển thị áp dụng sau khi DroidDeck khởi động lại.</string> + <string name="drivers_deleted_bundle">Đã xóa %1$s (cả hai driver)</string> + <string name="drivers_deleted">Đã xóa %1$s</string> + <string name="drivers_release_status">Mới nhất: %1$s · đã kiểm tra %2$s</string> + <string name="drivers_release_status_failed">Mới nhất: %1$s · %2$s không thể kết nối · đã kiểm tra %3$s</string> + <plurals name="drivers_ago_minutes"> + <item quantity="other">%1$d phút trước</item> + </plurals> + <plurals name="drivers_ago_hours"> + <item quantity="other">%1$d giờ trước</item> + </plurals> + <plurals name="drivers_ago_days"> + <item quantity="other">%1$d ngày trước</item> + </plurals> + <string name="drivers_checking_sources">Đang kiểm tra Banners-Turnip, WinNative và DroidDeck…</string> + <string name="drivers_check_failed">kiểm tra thất bại</string> + <string name="drivers_installed_pick_either">Đã cài %1$s - chọn nó ở một trong hai danh sách để đặt cả hai driver</string> + <string name="drivers_installed_pick">Đã cài %1$s - chọn nó trong menu</string> + + <string name="session_wait_removal">Chờ việc gỡ runtime hoàn tất</string> + <string name="session_title_game">Game</string> + <string name="session_touch_auto_touchpad">touchpad</string> + <string name="session_touch_auto_direct">trực tiếp</string> + <string name="session_swap_failed">Thay thất bại: %1$s</string> + <string name="session_profile_save_failed">Lưu hồ sơ thất bại: %1$s</string> + <string name="session_desktop_paused">Desktop đang tạm dừng</string> + <string name="session_named_paused">%1$s đang tạm dừng</string> + <string name="session_program_paused">Chương trình đang tạm dừng</string> + <string name="session_starting_desktop">Đang khởi động desktop</string> + <string name="session_starting_named">Đang khởi động %1$s</string> + <string name="session_starting_program">Đang khởi động chương trình</string> + <string name="session_no_logs">Không có log cho phiên này.</string> + <string name="session_logs_archive_failed">Không thể tạo tệp lưu trữ log phiên.</string> + <string name="session_logs_share_failed">Không thể chia sẻ log phiên.</string> + <string name="session_starting_session">Đang khởi động phiên…</string> + <string name="session_runtime_downloading">đang tải runtime Linux</string> + <string name="session_runtime_downloading_mb">đang tải runtime Linux · %1$d trong %2$d MB</string> + <string name="session_runtime_checking">đang kiểm tra runtime Linux</string> + <string name="session_runtime_unpacking">đang giải nén runtime Linux</string> + <string name="session_desktop_downloading">đang tải desktop</string> + <string name="session_desktop_downloading_mb">đang tải desktop · %1$d trong %2$d MB</string> + <string name="session_desktop_checking">đang kiểm tra desktop</string> + <string name="session_desktop_unpacking">đang giải nén desktop</string> + <string name="session_proton_downloading">đang tải Proton Experimental (ARM64)</string> + <string name="session_proton_downloading_mb">đang tải Proton Experimental (ARM64) · %1$d trong %2$d MB</string> + <string name="session_proton_checking">đang kiểm tra Proton Experimental (ARM64)</string> + <string name="session_proton_unpacking">đang giải nén Proton Experimental (ARM64)</string> + <string name="session_runtime_catalog_unreachable">Không thể kết nối danh mục runtime. Kiểm tra kết nối và nhấn Play lại.</string> + <string name="session_runtime_install_failed">Runtime Linux chưa cài được. Kiểm tra runtime trong Thiết lập và nhấn Play lại.</string> + <string name="session_desktop_install_failed">Desktop chưa cài được (%1$s). Kiểm tra kết nối và nhấn Desktop lại.</string> + <string name="session_ended_named">%1$s đã dừng ngoài ý muốn. Hãy chia sẻ log cùng báo cáo lỗi, hoặc thử lại.</string> + <string name="session_ended_program">Chương trình đã dừng ngoài ý muốn. Hãy chia sẻ log cùng báo cáo lỗi, hoặc thử lại.</string> + <string name="session_ended_desktop">Desktop đã dừng ngoài ý muốn. Hãy chia sẻ log cùng báo cáo lỗi, hoặc thử lại.</string> + <string name="session_ended_detail">Mã thoát %1$d · %2$s</string> + <string name="session_ended_no_log">không có log</string> + <plurals name="session_hint_enosys"> + <item quantity="other">Log cho thấy %1$d lỗi \"Function not implemented\": khả năng tăng tốc seccomp của proot đang làm hỏng một helper trên thiết bị này. Hãy thử %2$s → \"%3$s\" (hoặc \"%4$s\") rồi khởi động lại.</item> + </plurals> + + <string name="mode_decky_plugins">Plugin</string> + <string name="mode_decky_plugins_hint">Cài một tệp ZIP plugin cho phiên Steam kế tiếp.</string> + <string name="mode_decky_plugin_downloading">Đang tải…</string> + <string name="mode_decky_plugin_install_zip">Cài từ ZIP</string> + <string name="store_cat_games">Games</string> + <string name="store_cat_audio_video">Âm thanh & Video</string> + <string name="store_cat_graphics">Đồ họa</string> + <string name="store_cat_internet">Internet</string> + <string name="store_cat_office">Văn phòng</string> + <string name="store_cat_developer">Nhà phát triển</string> + <string name="store_cat_education">Giáo dục</string> + <string name="store_cat_science">Khoa học</string> + <string name="store_cat_system">Hệ thống</string> + <string name="store_cat_utilities">Công cụ</string> + <string name="store_section_popular">Phổ biến</string> + <string name="store_section_trending">Xu hướng</string> + <string name="store_section_new">Mới</string> + <string name="store_section_updated">Đã cập nhật</string> + <string name="store_section_loading">đang tải…</string> + <string name="store_flatpak_setup">Thiết lập Flatpak</string> + <string name="store_problem">%1$s: %2$s</string> + <string name="store_updates_check_failed">Không thể kiểm tra cập nhật trên Flathub</string> + <string name="content_rom_installed">đã cài</string> + <string name="content_rom_folder">thư mục</string> + <string name="theme_graphite">Than chì</string> + <string name="theme_graphite_detail">Các bề mặt than chì, với màu xanh quả cầu riêng của logo cho mọi thứ được chọn, lấy nét hoặc nhấn.</string> + <string name="theme_paper">Giấy trên đen</string> + <string name="theme_paper_detail">Trắng là màu chính, xanh của biểu tượng là tín hiệu cho các đường kẻ, vòng và điểm.</string> + <string name="theme_icon_blue">Xanh biểu tượng</string> + <string name="theme_icon_blue_detail">Nền đen, chữ trắng, xanh cho mọi thứ được chọn, lấy nét hoặc nhấn.</string> + <string name="theme_electric">Xanh điện navy</string> + <string name="theme_electric_detail">Màu xanh sáng hơn, trên các bề mặt nghiêng về navy thay vì xám.</string> + <string name="pckb_key_esc">Esc</string> + <string name="pckb_key_ins">Ins</string> + <string name="pckb_key_del">Del</string> + <string name="pckb_key_home">Home</string> + <string name="pckb_key_end">End</string> + <string name="pckb_key_pgup">PgUp</string> + <string name="pckb_key_pgdn">PgDn</string> + <string name="pckb_key_backspace">⌫ Backspace</string> + <string name="pckb_key_tab">Tab ⇥</string> + <string name="pckb_key_caps">Caps</string> + <string name="pckb_key_enter">Enter ⏎</string> + <string name="pckb_key_shift">⇧ Shift</string> + <string name="pckb_key_ctrl">Ctrl</string> + <string name="pckb_key_super">Super</string> + <string name="pckb_key_alt">Alt</string> + <string name="pckb_key_space">Space</string> + <string name="pckb_key_altgr">AltGr</string> + <string name="second_key_tab">Tab</string> + <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> + <string name="game_sync_err_runtime">Cài runtime trước</string> + <string name="game_sync_err_steam_library">Thư viện Steam không khả dụng</string> + <string name="game_sync_err_previous_sd">Thư viện SD trước đó không khả dụng</string> + <string name="game_sync_err_game_storage">Lưu trữ game không khả dụng</string> + <string name="game_sync_err_added_games">Thư mục game đã thêm không khả dụng</string> + <string name="game_sync_err_manifests">Manifest của Steam không khả dụng</string> + <string name="content_system_many">Nhiều hệ máy</string> + <string name="fm_drive_internal">Nội bộ</string> + <string name="fm_drive_linux">Hệ thống Linux</string> + <string name="fm_drive_sd">Thẻ SD</string> + <string name="fm_drive_storage">Lưu trữ</string> + <string name="fm_select">Chọn</string> + <string name="fm_properties">Thuộc tính</string> + <string name="fm_favorite_add">Thêm vào Yêu thích</string> + <string name="fm_favorite_remove">Xóa khỏi Yêu thích</string> + <string name="fm_rename">Đổi tên</string> + <string name="fm_copy">Sao chép</string> + <string name="fm_cut">Cắt</string> + <string name="fm_actions">Hành động</string> + <string name="fm_favorites">Yêu thích</string> + <string name="fm_pin_current">Ghim thư mục hiện tại</string> + <string name="fm_no_favorites">Chưa có mục yêu thích nào - ghim một thư mục bằng menu ⋮ của nó để quay lại đây nhanh.</string> + <string name="fm_unpin_favorite">Bỏ khỏi mục yêu thích</string> + <string name="fm_size_zero">0 byte</string> + <string name="fm_unit_bytes">byte</string> + <string name="fm_prop_location">Vị trí</string> + <string name="fm_prop_type">Loại</string> + <string name="fm_prop_size">Kích thước</string> + <string name="fm_prop_modified">Đã sửa đổi</string> + <string name="fm_type_folder">Thư mục</string> + <string name="fm_type_file">Tệp</string> + <string name="fm_type_ext_file">Tệp %1$s</string> + <string name="fm_read_only">Chỉ đọc</string> + <string name="fm_read_only_hint">Ngăn game (và Wine) ghi đè hoặc xóa tệp này.</string> + <string name="fm_read_only_failed">Không thể đổi Chỉ đọc</string> + <string name="fm_hidden">Ẩn</string> + <string name="fm_hidden_unsupported">Bộ lưu trữ này không thể lưu cờ ẩn.</string> + <string name="fm_hidden_hint">Đánh dấu tệp là ẩn trong Windows (thuộc tính hidden của Wine).</string> + <string name="fm_hidden_failed">Không thể đổi Ẩn trên bộ lưu trữ này</string> + <string name="fm_done">Xong</string> + <string name="fm_op_deleting">Đang xóa...</string> + <string name="fm_delete_failed">Xóa thất bại</string> + <string name="fm_op_moving">Đang di chuyển...</string> + <string name="fm_op_copying">Đang sao chép...</string> + <string name="fm_op_moving_item">Đang di chuyển - %1$s</string> + <string name="fm_op_copying_item">Đang sao chép - %1$s</string> + <string name="fm_op_moving_batch">Đang di chuyển %1$d/%2$d - %3$s</string> + <string name="fm_op_copying_batch">Đang sao chép %1$d/%2$d - %3$s</string> + <string name="fm_paste_failed">%1$d xong, %2$d thất bại</string> + <string name="fm_paste_skipped">%1$d xong, %2$d đã bỏ qua</string> + <plurals name="fm_paste_moved"> + <item quantity="other">%1$d mục đã di chuyển</item> + </plurals> + <plurals name="fm_paste_copied"> + <item quantity="other">%1$d mục đã sao chép</item> + </plurals> + <string name="fm_already_exists">\"%1$s\" đã tồn tại</string> + <string name="fm_op_renaming">Đang đổi tên...</string> + <string name="fm_rename_failed">Đổi tên thất bại</string> + <string name="fm_op_creating_folder">Đang tạo thư mục...</string> + <string name="fm_create_folder_failed">Không thể tạo thư mục</string> + <string name="fm_new_folder">Thư mục mới</string> + <string name="fm_folder_name">Tên thư mục</string> + <string name="fm_create">Tạo</string> + <string name="fm_new_name">Tên mới</string> + <string name="fm_delete_title">Xóa?</string> + <string name="fm_delete_text">Xóa \"%1$s\" vĩnh viễn?</string> + <plurals name="fm_delete_items_title"> + <item quantity="other">Xóa %1$d mục?</item> + </plurals> + <string name="fm_cannot_undo">Không thể hoàn tác việc này.</string> + <plurals name="fm_and_more"> + <item quantity="other">…và %1$d mục nữa</item> + </plurals> + <string name="fm_op_deleting_batch">Đang xóa %1$d/%2$d - %3$s</string> + <plurals name="fm_delete_some_failed"> + <item quantity="other">%1$d mục không thể xóa</item> + </plurals> + <string name="fm_conflict_merge_hint">Gộp sẽ thêm và thay thế các tệp trong thư mục hiện có.</string> + <string name="fm_conflict_overwrite_hint">Ghi đè sẽ thay thế tệp hiện có.</string> + <string name="fm_conflict_apply_all">Áp dụng cho mọi xung đột</string> + <string name="fm_conflict_merge">Gộp</string> + <string name="fm_conflict_overwrite">Ghi đè</string> + <string name="fm_conflict_keep_both">Giữ cả hai</string> + <string name="fm_conflict_skip">Bỏ qua</string> + <string name="fm_select_this_folder">Chọn thư mục này</string> + <string name="fm_back">Quay lại</string> + <string name="fm_drive_not_readable">%1$s đã được gắn nhưng hiện không đọc được</string> + <string name="fm_list_view">Xem dạng danh sách</string> + <string name="fm_grid_view">Xem dạng lưới</string> + <string name="fm_search">Tìm kiếm</string> + <string name="fm_sort">Sắp xếp</string> + <string name="fm_sort_name">Tên</string> + <string name="fm_sort_date">Ngày sửa đổi</string> + <string name="fm_rows_comfortable">Hàng thoải mái</string> + <string name="fm_rows_compact">Hàng gọn</string> + <string name="fm_hide_hidden">Ẩn tệp ẩn</string> + <string name="fm_show_hidden">Hiện tệp ẩn</string> + <string name="fm_hide_favorites">Ẩn mục yêu thích</string> + <string name="fm_show_favorites">Hiện mục yêu thích</string> + <string name="fm_filter_folder">Lọc thư mục này</string> + <string name="fm_free_space">%1$s trống</string> + <string name="fm_selected_count">%1$d đã chọn</string> + <string name="fm_select_none">Không chọn</string> + <string name="fm_select_all">Tất cả</string> + <string name="fm_paste">Dán</string> + <plurals name="fm_items"> + <item quantity="other">%1$d mục</item> + </plurals> + <string name="fm_paste_here">Dán %1$s vào đây</string> + <string name="fm_paste_move_here">Dán %1$s (di chuyển) vào đây</string> + <string name="fm_rail_title">Tệp</string> + <string name="fm_rail_storage">LƯU TRỮ</string> + <string name="fm_rail_quick">NHANH</string> + <string name="fm_rail_favorites">YÊU THÍCH</string> + <string name="fm_rail_downloads">Downloads</string> + <string name="fm_rail_downloads_short">Downlds</string> + <string name="fm_rail_roms">ROMs</string> + <string name="fm_rail_session_logs">Log phiên</string> + <string name="fm_rail_pictures">Pictures</string> + <string name="fm_favorite_added">Đã thêm \"%1$s\" vào Yêu thích</string> + <string name="fm_favorite_removed">Đã xóa \"%1$s\" khỏi Yêu thích</string> + <string name="fm_empty_directory">Thư mục trống</string> + <string name="fm_up_one_level">Lên một cấp</string> + <string name="fm_choose_folder">Chọn một thư mục</string> + <string name="fm_choose_file">Chọn một tệp</string> + <string name="ctrl_tint_steam_blue">Xanh Steam</string> + <string name="ctrl_tint_sky">Xanh da trời</string> + <string name="ctrl_tint_white">Trắng</string> + <string name="ctrl_tint_violet">Tím</string> + <string name="ctrl_tint_green">Xanh lá</string> + <string name="ctrl_tint_amber">Hổ phách</string> + <string name="ctrl_tint_red">Đỏ</string> + <string name="ctrl_tint_pink">Hồng</string> + <string name="ctrl_button_a">Nút A</string> + <string name="ctrl_button_b">Nút B</string> + <string name="ctrl_button_x">Nút X</string> + <string name="ctrl_button_y">Nút Y</string> + <string name="ctrl_button_lb">Bumper trái</string> + <string name="ctrl_button_rb">Bumper phải</string> + <string name="ctrl_button_lt">Trigger trái</string> + <string name="ctrl_button_rt">Trigger phải</string> + <string name="ctrl_button_view">Nút View</string> + <string name="ctrl_button_menu">Nút Menu</string> + <string name="ctrl_target_view">View</string> + <string name="ctrl_target_menu">Menu</string> + <string name="ctrl_target_up">D-pad lên</string> + <string name="ctrl_target_down">D-pad xuống</string> + <string name="ctrl_target_left">D-pad trái</string> + <string name="ctrl_target_right">D-pad phải</string> + <string name="ctrl_target_hidden">Ẩn</string> + <string name="ctrl_editor_drag">Kéo để di chuyển</string> + <string name="ctrl_editor_snap">Hít vào lưới (snap)</string> + <string name="ctrl_editor_save">Lưu</string> + <string name="second_mode_none">Không có</string> + <string name="second_mode_keyboard_trackpad">Bàn phím + trackpad</string> + <string name="second_mode_terminal">Terminal</string> + <string name="second_mode_deck_controls">Grip + trackpad Deck</string> + + <string name="rtinst_removing">Đang gỡ runtime Linux</string> + <plurals name="rtinst_removing_entries"> + <item quantity="other">Đang gỡ runtime Linux · %1$d mục</item> + </plurals> + <string name="rtinst_removed">Đã gỡ runtime Linux</string> + <string name="rtinst_removal_failed">Không thể hoàn tất việc gỡ runtime Linux. Thử gỡ lại trong Thiết lập.</string> + <string name="rtinst_removing_leftovers">Đang xóa phần còn lại của runtime</string> + <string name="rtinst_downloading">Đang tải</string> + <string name="rtinst_verifying">Đang xác minh</string> + <string name="rtinst_extracting">Đang giải nén</string> + <string name="rtinst_channel_description">Hiển thị khi runtime Linux đang được cài hoặc gỡ</string> + <string name="rtinst_title_installing">Đang cài runtime Linux</string> + <string name="rtinst_title_removing">Đang gỡ runtime Linux</string> + + <string name="deskpkg_runtime_missing">Runtime Linux chưa được cài</string> + <string name="deskpkg_no_checksum">Danh mục không có checksum cho %1$s</string> + <string name="deskpkg_checksum_mismatch">Checksum không khớp - không có gì bị thay đổi</string> + <string name="deskpkg_installing">Đang cài %1$s</string> + <string name="deskpkg_place_failed">Không thể đặt AppImage</string> + <string name="deskpkg_extract_failed">Giải nén thất bại</string> + <string name="deskpkg_install_failed">Cài thất bại</string> + + <string name="flatpakmgr_setup_failed">Thiết lập Flatpak thất bại (mã thoát %1$d)</string> + <string name="flatpakmgr_setup_unfinished">Thiết lập Flatpak chưa hoàn tất</string> + <string name="flatpakmgr_busy">Một tác vụ store khác đang chạy</string> + <string name="flatpakmgr_not_ready">Thiết lập Flatpak trước</string> + <string name="flatpakmgr_starting">Đang khởi động</string> + <string name="flatpakmgr_exit_status">Flatpak đã thoát với mã %1$d</string> + <string name="flatpakmgr_installing">Đang cài %1$s</string> + <string name="flatpakmgr_updating">Đang cập nhật %1$s</string> + <string name="flatpakmgr_removing">Đang gỡ %1$s</string> + <string name="flatpakmgr_installing_step">Đang cài %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_updating_step">Đang cập nhật %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_removing_step">Đang gỡ %1$s (%2$d/%3$d)</string> + <string name="flatpakmgr_ref_translations">bản dịch</string> + <string name="flatpakmgr_ref_graphics_drivers">driver đồ họa</string> + <string name="flatpakmgr_ref_media_codecs">codec media</string> + <string name="flatpakmgr_ref_runtime">runtime %1$s</string> + <string name="flatpakmgr_ref_runtime_branch">runtime %1$s %2$s</string> + + <string name="appimg_too_small">Tệp này quá nhỏ để là một AppImage</string> + <string name="appimg_unreadable">Không thể đọc tệp: %1$s</string> + <string name="appimg_not_linux">Đây không phải AppImage (không phải chương trình Linux)</string> + <string name="appimg_wrong_arch">AppImage này không được build cho ARM64 (aarch64) hay PC x86</string> + <string name="appimg_not_appimage">Đây là chương trình Linux nhưng không phải AppImage</string> + <string name="appimg_wrong_type">Chỉ có thể nhập AppImage loại 2 (tệp này là loại %1$d)</string> + <string name="appimg_no_space_import">Không đủ dung lượng trống: nhập cần khoảng %1$s</string> + <string name="appimg_no_space_update">Không đủ dung lượng trống: cập nhật cần khoảng %1$s</string> + <string name="appimg_import_failed">Nhập thất bại</string> + <string name="appimg_gone">AppImage đó không còn nữa</string> + <string name="appimg_update_failed">Cập nhật thất bại</string> + <string name="appimg_extracting">Đang giải nén %1$s</string> + <string name="appimg_extract_failed">Không thể giải nén AppImage</string> + <string name="appimg_extract_failed_detail">Không thể giải nén AppImage: %1$s</string> + + <string name="deckymgr_stop_session_loader">Dừng phiên đang hoạt động trước khi cài Decky Loader</string> + <string name="deckymgr_download_failed">Tải Decky Loader thất bại</string> + <string name="deckymgr_size_mismatch">Kích thước Decky Loader không khớp với metadata của bản phát hành</string> + <string name="deckymgr_checksum_download_failed">Không thể tải checksum của bản phát hành</string> + <string name="deckymgr_checksum_missing">Thiếu checksum của bản phát hành</string> + <string name="deckymgr_checksum_invalid">Checksum của bản phát hành không hợp lệ</string> + <string name="deckymgr_checksum_mismatch">Checksum Decky Loader không khớp</string> + <string name="deckymgr_not_elf">Tài sản phát hành không phải là file thực thi Linux 64-bit tương thích</string> + <string name="deckymgr_session_started_loader">Một phiên đã bắt đầu trong khi tải; hãy dừng nó trước khi cài Decky Loader</string> + <string name="deckymgr_loader_not_executable">Không thể đánh dấu PluginLoader có thể thực thi</string> + <string name="deckymgr_cef_failed">Không thể bật gỡ lỗi CEF từ xa của Steam</string> + <string name="deckymgr_stage_failed">Không thể dàn (stage) PluginLoader</string> + <string name="deckymgr_loader_preserve_failed">Không thể giữ lại PluginLoader hiện có</string> + <string name="deckymgr_loader_activate_failed">Không thể kích hoạt PluginLoader mới</string> + <string name="deckymgr_stop_session_plugin">Dừng phiên đang hoạt động trước khi cài một plugin</string> + <string name="deckymgr_loader_required">Cài Decky Loader trước khi nhập một plugin</string> + <string name="deckymgr_zip_unreadable">Không thể đọc tệp ZIP đã chọn</string> + <string name="deckymgr_plugins_dir_failed">Không thể tạo thư mục plugin Decky</string> + <string name="deckymgr_extracting_zip">Đang giải nén ZIP plugin</string> + <string name="deckymgr_zip_one_folder">ZIP plugin phải chứa một thư mục plugin cấp cao nhất</string> + <string name="deckymgr_zip_missing_files">ZIP plugin thiếu plugin.json, package.json, hoặc dist/index.js</string> + <string name="deckymgr_plugin_name_missing">Thiếu tên plugin</string> + <string name="deckymgr_session_started_plugin">Một phiên đã bắt đầu trong khi cài đặt; hãy dừng nó trước khi cài một plugin</string> + <string name="deckymgr_plugin_installed">Đã cài plugin · khởi động lại Steam để tải nó</string> + <string name="deckymgr_plugin_install_failed">Cài ZIP plugin thất bại</string> + <string name="deckymgr_zip_too_many">ZIP plugin chứa quá nhiều mục</string> + <string name="deckymgr_zip_invalid_path">ZIP plugin chứa một đường dẫn không hợp lệ</string> + <string name="deckymgr_zip_duplicate_paths">ZIP plugin chứa các đường dẫn trùng lặp</string> + <string name="deckymgr_zip_unsafe_path">ZIP plugin chứa một đường dẫn không an toàn</string> + <string name="deckymgr_folder_failed">Không thể tạo thư mục plugin</string> + <string name="deckymgr_zip_too_large">ZIP plugin giải nén vượt quá giới hạn 1 GiB</string> + <string name="deckymgr_bundled_not_executable">Không thể đánh dấu tệp nhị phân plugin đi kèm có thể thực thi</string> + <string name="deckymgr_remote_entry_invalid">Mục remote_binary không hợp lệ</string> + <string name="deckymgr_remote_name_invalid">Tên tệp nhị phân từ xa không hợp lệ</string> + <string name="deckymgr_remote_url_https">URL tệp nhị phân từ xa phải dùng HTTPS</string> + <string name="deckymgr_remote_hash_invalid">Tệp nhị phân từ xa %1$s có mã băm SHA-256 không hợp lệ</string> + <string name="deckymgr_bin_dir_failed">Không thể tạo thư mục bin của plugin</string> + <string name="deckymgr_downloading_binary">Đang tải tệp nhị phân plugin · %1$s</string> + <string name="deckymgr_binary_download_failed">Không thể tải tệp nhị phân plugin %1$s</string> + <string name="deckymgr_binary_checksum_mismatch">Checksum tệp nhị phân plugin không khớp · %1$s</string> + <string name="deckymgr_binary_not_executable">Không thể đánh dấu tệp nhị phân plugin có thể thực thi · %1$s</string> + <string name="deckymgr_binary_replace_failed">Không thể thay thế tệp nhị phân plugin · %1$s</string> + <string name="deckymgr_binary_install_failed">Không thể cài tệp nhị phân plugin · %1$s</string> + <string name="deckymgr_folder_taken">Thư mục plugin thuộc về một plugin khác</string> + <string name="deckymgr_preserve_failed">Không thể giữ lại plugin hiện có</string> + <string name="deckymgr_activate_failed">Không thể kích hoạt plugin đã nhập</string> + <string name="deckymgr_settings_failed">Không thể cập nhật thiết lập plugin Decky</string> + <string name="deckymgr_restore_failed">Không thể khôi phục plugin hiện có</string> + + <string name="adbfix_save_failed">Không thể lưu danh tính Gỡ lỗi qua mạng không dây</string> + <string name="adbfix_command_failed">Lệnh ADB thất bại (%1$d)</string> + <string name="adbfix_not_confirmed">Android không xác nhận giới hạn tiến trình con đã được đổi: %1$s</string> + <string name="adbfix_no_pairing">Chưa có ghép nối Gỡ lỗi qua mạng không dây nào được lưu</string> + <string name="adbfix_enter_address">Nhập địa chỉ IP & cổng Gỡ lỗi qua mạng không dây hiện tại</string> + + <string name="adbpair_timed_out">Hết thời gian chờ cửa sổ bật lên ghép nối. Hãy thử lại từ DroidDeck.</string> + <string name="adbpair_discovery_failed_code">Không thể tìm cửa sổ bật lên ghép nối (%1$d). Kiểm tra Wi-Fi đã bật.</string> + <string name="adbpair_discovery_failed">Không thể tìm cửa sổ bật lên ghép nối: %1$s</string> + <string name="adbpair_six_digits">Nhập đủ 6 số của mã</string> + <string name="adbpair_pairing">Đang ghép nối…</string> + <string name="adbpair_code_rejected">Mã đó không hoạt động. Mở lại cửa sổ bật lên ghép nối và nhập mã mới của nó.</string> + <string name="adbpair_connecting">Đã ghép nối. Đang kết nối…</string> + <string name="adbpair_port_not_found">Đã ghép nối, nhưng không tìm thấy cổng Gỡ lỗi qua mạng không dây. Giữ Gỡ lỗi qua mạng không dây luôn mở và thử lại.</string> + <string name="adbpair_applying">Đang áp dụng thiết lập…</string> + <string name="adbpair_command_failed">Lệnh Gỡ lỗi qua mạng không dây thất bại</string> + <string name="adbpair_channel_name">Thiết lập Gỡ lỗi qua mạng không dây</string> + <string name="adbpair_channel_description">Hỏi mã ghép nối Gỡ lỗi qua mạng không dây khi Settings đang mở</string> + <string name="adbpair_title_pair">Ghép nối Gỡ lỗi qua mạng không dây</string> + <string name="adbpair_waiting_text">Bật Gỡ lỗi qua mạng không dây, rồi nhấn “Pair device with pairing code”.</string> + <string name="adbpair_waiting_long">Bật Gỡ lỗi qua mạng không dây, rồi nhấn “Pair device with pairing code”. Ở lại trong Settings: mã sẽ hiện trong thông báo này.</string> + <string name="adbpair_code_label">Mã ghép nối 6 số</string> + <string name="adbpair_code_hint">Nhấn Enter code và nhập 6 số hiện trong Settings.</string> + <string name="adbpair_title_code">Nhập mã ghép nối Wi-Fi</string> + <string name="adbpair_title_retry">Thử lại mã</string> + <string name="adbpair_enter_code">Nhập mã</string> + <string name="adbpair_title_working">Đang thiết lập Steam</string> + <string name="adbpair_title_done">Steam đã sẵn sàng</string> + <string name="adbpair_done_text">Nhấn để quay lại DroidDeck. Bạn có thể tắt Gỡ lỗi qua mạng không dây ngay bây giờ.</string> + <string name="adbpair_title_failed">Thiết lập Gỡ lỗi qua mạng không dây đã dừng</string> + + <string name="selfinst_http">Lượt tải trả về HTTP %1$d</string> + <string name="selfinst_size_mismatch">Kích thước tải xuống không khớp với APK đã phát hành và đã bị loại bỏ - hãy thử lại</string> + <string name="selfinst_checksum_mismatch">Tệp tải xuống không khớp checksum và đã bị loại bỏ - hãy thử lại</string> + <string name="selfinst_wrong_package">Bản cập nhật này dành cho package Android %1$s, không phải %2$s</string> + <string name="selfinst_metadata_signer">Metadata cập nhật không ghi tên khóa ký phát hành của DroidDeck</string> + <string name="selfinst_unreadable">Android không thể đọc APK đã tải xuống</string> + <string name="selfinst_apk_wrong_package">APK đã tải xuống dành cho %1$s, không phải %2$s</string> + <string name="selfinst_version_mismatch">APK đã tải xuống ghi versionCode %1$d, không phải %2$d đã phát hành</string> + <string name="selfinst_downgrade">Android sẽ không cài versionCode %1$d chồng lên %2$d đã cài</string> + <string name="selfinst_no_signing">APK đã tải xuống không có thông tin ký</string> + <string name="selfinst_wrong_signer">APK đã tải xuống không được ký bằng khóa phát hành của DroidDeck</string> + <string name="appupd_http">Danh mục cập nhật trả về HTTP %1$d</string> + + <string name="loadstate_starting">Đang khởi động phiên…</string> + <string name="loadstate_elapsed">%1$d:%2$02d đã trôi qua · vẫn đang xử lý</string> + <string name="loadstate_steam_update">Đang tải bản cập nhật client Steam · %1$d%%</string> + <string name="loadstate_steam_client">Đang tải client Steam · %1$s</string> + + <string name="pextra_busy">Một lượt cài công cụ tương thích đang chạy</string> + <string name="pextra_stop_session">Dừng phiên đang hoạt động trước khi cài công cụ tương thích</string> + <string name="pextra_no_release">Không tìm thấy bản phát hành ARM64 cho %1$s</string> + <string name="pextra_no_space">Cần ít nhất 4 GB trống cộng với kích thước tải xuống để cài %1$s</string> + <string name="pextra_downloading">Đang tải %1$s %2$s</string> + <string name="pextra_retrying">Đang thử lại tải · lần %1$d trong %2$d</string> + <string name="pextra_interrupted">Tải xuống bị gián đoạn</string> + <string name="pextra_download_failed">Tải xuống thất bại; tệp dở được giữ lại để lần thử lại có thể tiếp tục</string> + <string name="pextra_too_big">Tải xuống vượt quá kích thước bản phát hành và đã bị xóa</string> + <string name="pextra_incomplete">Tải xuống chưa hoàn tất; tệp dở được giữ lại để thử lại tiếp tục</string> + <string name="pextra_verifying">Đang xác minh tải xuống</string> + <string name="pextra_checksum_unreadable">Không thể đọc checksum bản phát hành; hãy thử lại</string> + <string name="pextra_no_checksum">%1$s %2$s không công bố checksum; không có gì được cài</string> + <string name="pextra_checksum_mismatch">Checksum không khớp; tệp tải xuống đã bị xóa</string> + <string name="pextra_session_started">Một phiên đã bắt đầu trong khi tải; hãy dừng nó trước khi cài công cụ tương thích</string> + <string name="pextra_installing">Đang cài %1$s</string> + <string name="pextra_install_failed">Cài %1$s thất bại (mã thoát %2$d)</string> + <string name="pextra_could_not_install">Không thể cài %1$s</string> + <string name="esync_fetching">Đang lấy pack droiddeck-esync</string> + + <string name="cmgr_fex_linux_helpers">có trợ giúp Linux</string> + <string name="cmgr_fex_dlls_only">chỉ DLL</string> + <string name="cmgr_original_proton_updated">Gốc (Proton đã cập nhật từ lần thay gần nhất)</string> + <string name="cmgr_changed_outside">Đã thay đổi bên ngoài Components</string> + <string name="cmgr_not_present">không có</string> + <string name="cmgr_original_version">Gốc %1$s</string> + <string name="cmgr_original_from">Gốc từ %1$s</string> + <string name="cmgr_swap_queued">%1$s %2$s sẽ vào %3$s khi game đang chạy trên đó đóng lại.</string> + <string name="cmgr_swap_done">%1$s %2$s giờ đã ở trong %3$s. Áp dụng vào lần game khởi động kế tiếp.</string> + <string name="cmgr_original_missing">Gói gốc đó không được lưu</string> + <string name="cmgr_restore_queued">%1$s của %2$s sẽ trở về bản gốc khi game đang chạy trên đó đóng lại.</string> + <string name="cmgr_restore_done">%1$s của %2$s đã được khôi phục từ bản gốc %3$s.</string> + <string name="cmgr_package_in_use">%1$s đang được dùng trong %2$s. Hãy thay Proton đó sang thứ khác trước.</string> + <string name="cmgr_package_not_stored">%1$s không được lưu</string> + <string name="cmgr_deleted">Đã xóa %1$s.</string> + <string name="cmgr_original_kept">Bản gốc của build đã cài được giữ lại: đó là đường quay về.</string> + <string name="cmgr_not_stored">Chưa được lưu</string> + <string name="cmgr_deleted_original">Đã xóa bản gốc %1$s của %2$s.</string> + <string name="cmgr_nightlies_only">Lượt tải chỉ đến từ các bản phát hành Nightlies</string> + <string name="cmgr_list_predates_checksums">Danh sách gói này có trước checksum - hãy làm mới và thử lại</string> + <string name="cmgr_checksum_mismatch">Checksum không khớp</string> + + <string name="sprefs_upscaler_linear">Tuyến tính</string> + <string name="sprefs_upscaler_nearest">Gần nhất</string> + <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (chất lượng)</string> + <string name="sprefs_upscaler_sharpen">Chỉ làm nét</string> + <string name="sprefs_lod_bias_auto">Tự động (khớp tỷ lệ)</string> + + <string name="look_off_desc">Không áp dụng gì.</string> + <string name="look_game_clarity">Game Clarity</string> + <string name="look_game_clarity_desc">Làm nét với một chút tương phản - lựa chọn hàng ngày.</string> + <string name="look_vivid">Vivid</string> + <string name="look_vivid_desc">Màu sắc được đẩy mạnh. Khử dải màu được bật để bầu trời không bị sọc.</string> + <string name="look_cinematic">Cinematic</string> + <string name="look_cinematic_desc">Tương phản sâu hơn, màu lạnh hơn, cạnh được làm mượt.</string> + <string name="look_competitive">Competitive</string> + <string name="look_competitive_desc">Vùng tối được nâng lên và làm nét mạnh - phát hiện người trước tiên.</string> + <string name="look_adaptive_sharpen">Adaptive Sharpen</string> + <string name="look_adaptive_sharpen_desc">Chỉ làm nét, không đổi màu sắc.</string> + <string name="look_filmic">Filmic</string> + <string name="look_filmic_desc">Đường cong giống phim, hơi trầm.</string> + <string name="look_arcade">Arcade</string> + <string name="look_arcade_desc">Mạnh mẽ và rực rỡ.</string> + <string name="look_retro_crt">Retro CRT</string> + <string name="look_retro_crt_desc">Ống CRT và tín hiệu analog cùng áp dụng.</string> + <string name="look_upscale_sharp">Upscale Sharp</string> + <string name="look_upscale_sharp_desc">Dùng khi chạy dưới độ phân giải màn hình.</string> + <string name="look_pixel_clean">Pixel Clean</string> + <string name="look_pixel_clean_desc">Pixel sắc nét cho game 2D và các game cũ.</string> + <string name="look_anime_edge">Anime Edge</string> + <string name="look_anime_edge_desc">Viền hoạt hình cộng làm nét.</string> + + <string name="gpuinfo_not_adreno">Không phải GPU Adreno</string> + <string name="gpuinfo_supported">Được hỗ trợ</string> + <string name="gpuinfo_experimental">Thử nghiệm: driver của nó là bản build thử nghiệm</string> + <string name="gpuinfo_below_tested">Dưới mức phần cứng đã kiểm thử (Adreno 725 và mới hơn): có thể không chạy được</string> + <string name="gpuinfo_unsupported">Không được hỗ trợ: DroidDeck cần GPU Adreno (Snapdragon)</string> + + <string name="turnip_rate_limited">Đã đạt giới hạn tốc độ của GitHub - hãy thử lại sau</string> + <string name="turnip_github_http">GitHub trả về HTTP %1$d</string> + <string name="turnip_download_http">tải xuống trả về HTTP %1$d</string> + <string name="turnip_list_predates_checksums">Danh sách driver này có trước checksum - hãy làm mới và thử lại</string> + <string name="turnip_checksum_mismatch">Checksum không khớp - tệp tải xuống đã bị loại bỏ</string> + <string name="turnip_not_adrenotools">Không phải driver AdrenoTools: %1$s</string> + <string name="turnip_reject_no_meta">không có meta.json</string> + <string name="turnip_reject_meta_unreadable">meta.json không đọc được (%1$s)</string> + <string name="turnip_reject_linux_driver">đây là driver runtime Linux (hãy nhập nó dưới \"Linux runtime driver\")</string> + <string name="turnip_reject_wayland">đây là driver game Wayland, mà ứng dụng này không dùng đến</string> + <string name="turnip_reject_no_library_name">meta.json không ghi tên libraryName, nên không có gì để gửi cho AdrenoTools</string> + <string name="turnip_reject_library_missing">meta.json ghi tên %1$s, nhưng không có trong zip</string> + <string name="turnip_reject_not_aarch64">%1$s không phải là thư viện chia sẻ ELF AArch64 64-bit</string> + <string name="turnip_reject_glibc">%1$s liên kết glibc - một Turnip \"-Linux\", thuộc về mục \"Linux runtime driver\"</string> + <string name="lvd_no_library">Không có libvulkan_freedreno*.so trong zip này. Một zip Turnip Android (AdrenoTools) hoặc -Wayland không phải là driver runtime Linux.</string> + <string name="lvd_not_aarch64">%1$s không phải là thư viện chia sẻ ELF AArch64 64-bit.</string> + <string name="lvd_not_glibc">%1$s không phải là driver glibc - nó liên kết libc của Android. Runtime Linux cần một zip \"-Linux\"; một Turnip thường hoặc \"-Wayland\" hoàn toàn không thể được client Steam tải.</string> + <string name="bundle_no_manifest">Không phải gói driver: không có manifest.json ghi tên một driver android và một driver linux</string> + <string name="bundle_entry_outside">Không phải gói driver: %1$s trỏ ra ngoài nó</string> + <string name="bundle_too_large">Không phải gói driver: nó giải nén vượt quá %1$d MB</string> + + <string name="gstore_sd_card">Thẻ SD</string> + <string name="gstore_folder">Thư mục</string> + + <string name="logshare_chooser">Chia sẻ log phiên</string> + + <string name="hdr_needs_android_10">cần Android 10 hoặc mới hơn (lớp hiển thị có không gian màu)</string> + <string name="hdr_none">màn hình này báo không hỗ trợ HDR</string> + <string name="hdr_not_hdr10">màn hình này hỗ trợ %1$s, không phải HDR10</string> + + <string name="gpu_this_gpu">GPU này</string> + <string name="phantom_title_on">Giới hạn tiến trình con đang bật</string> + <string name="phantom_title_unset">Giới hạn tiến trình con chưa được đặt</string> + <string name="phantom_title_unreadable">Không thể kiểm tra giới hạn tiến trình con</string> + <string name="phantom_title_off">Giới hạn tiến trình con đang tắt</string> + <string name="phantom_title_not_required">Không cần giới hạn tiến trình con</string> + <string name="phantom_instructions_fix">%1$s Nếu không thể, hãy kết nối thiết bị này với một máy tính có ADB và chạy lệnh dưới đây. Quay lại đây và kiểm tra lại.</string> + <string name="phantom_instructions_off">Phiên Steam có thể khởi động.</string> + <string name="phantom_instructions_not_applicable">Phiên bản Android này không dùng giới hạn này.</string> + <string name="phantom_fix_toggle">Bật “Disable child process restrictions” trong Developer options, và giữ Developer options luôn mở sau đó.</string> + <string name="phantom_fix_no_toggle">Phiên bản Android này không có công tắc cho việc đó. Dùng Gỡ lỗi qua mạng không dây để đổi nó từ thiết bị này.</string> + <string name="upd_block_no_apk">Bản build này không có tệp tải cho bản DroidDeck này.</string> + <string name="upd_block_version_code">Android sẽ không cài nó chồng lên bản này vì versionCode %1$d của nó thấp hơn %2$d đã cài.</string> + <string name="upd_fail_downgrade">Android sẽ không cài một phiên bản cũ hơn chồng lên phiên bản mới hơn. Hãy giữ nguyên bản này cho đến khi kênh bắt kịp.</string> + <string name="upd_fail_signature">Bản DroidDeck này được ký khác (có thể là bản build cục bộ?), nên Android sẽ không cập nhật tại chỗ.</string> + <string name="upd_fail_storage">Không đủ dung lượng trống cho bản cập nhật.</string> + <string name="upd_fail_aborted">Bản cập nhật đã bị hủy.</string> + <string name="upd_fail_with_message">Bản cập nhật không cài được: %1$s</string> + <string name="upd_fail_generic">Bản cập nhật không cài được.</string> + <string name="saves_layout_zip">zip %1$s</string> + <string name="saves_zip">zip</string> + <string name="saves_not_launched">%1$s chưa được khởi chạy</string> + <string name="saves_none_found">Không tìm thấy tệp lưu nào cho %1$s</string> + <string name="saves_not_save_zip">%1$s không có thư mục drive_c, nên nó không phải tệp zip lưu GameHub hoặc Winlator</string> + <string name="gstore_cannot_create">không thể tạo thư mục trong %1$s</string> + <string name="gstore_cannot_write">không thể ghi trong %1$s</string> + <string name="gstore_cannot_write_detail">không thể ghi trong %1$s (%2$s)</string> + <string name="gfiles_folder_unavailable">Thư mục không khả dụng</string> + <string name="gfiles_name_taken">Một tệp khác đã dùng tên %1$s</string> + <string name="gfiles_art_unavailable">Thư mục hình ảnh không khả dụng</string> + <string name="gfiles_unrelated_inventory">Thư mục có một inventory đồng bộ không liên quan</string> + <string name="gfiles_remove_failed">Không thể xóa %1$s</string> + <string name="gfiles_outside_folder">Tệp nằm ngoài thư mục xuất</string> + <string name="cmgr_fex_unnamed">FEX (bản build không có tên phiên bản)</string> + <string name="cmgr_no_profile">không có profile.json trong %1$s</string> + <string name="cmgr_not_component_package">%1$s không phải là gói thành phần Linux (loại %2$s)</string> + <string name="gstore_volume_free">%1$s · %2$s trống</string> + <string name="mode_storage_auto_short">Tự động</string> + <string name="appupd_catalog_rejected">Danh mục cập nhật không qua được kiểm tra (%1$s)</string> + <string name="main_adb_clip_label">Thiết lập tiến trình con của DroidDeck</string> + <string name="setup_signed_in_offline_unnamed">Đã đăng nhập · chế độ ngoại tuyến</string> + <string name="setup_signed_in_unnamed">Đã đăng nhập</string> + <string name="driver_tag_bundled">ĐI KÈM</string> + <string name="driver_tag_downloaded">ĐÃ TẢI</string> + <string name="driver_tag_imported">ĐÃ NHẬP</string> + <string name="driver_tag_bundle">ANDROID + LINUX</string> + <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> + <string name="display_session">Hiển thị phiên</string> + <string name="display_resolution">Độ phân giải</string> + <string name="display_game_fit">Khớp game 16:9</string> + <string name="display_game_fit_hint">Cách game 16:9 khớp với màn hình này. Từ phiên sau.</string> + <string name="display_game_fit_unavailable">Chọn Khớp màn hình hoặc một kích thước tùy chỉnh hẹp hơn 16:9.</string> + <string name="display_preserve">Giữ tỷ lệ</string> + <string name="display_stretch">Kéo giãn để khớp màn hình</string> + <string name="display_image_scaling">Chia tỷ lệ hình ảnh</string> + <string name="display_filter">Bộ lọc chia tỷ lệ</string> + <string name="display_filter_hint">Dùng chung cho Steam và Desktop.</string> + <string name="display_filter_live">Áp dụng ngay. Dùng chung cho Steam và Desktop.</string> + <string name="display_filter_note">FSR, GSR và NIS áp dụng khi phóng to hình ảnh phiên. Chỉ làm nét cũng hoạt động mà không cần phóng to.</string> + <string name="display_sharpness">Độ sắc nét bộ lọc</string> + <string name="display_frame_rate">Tốc độ khung hình</string> + <string name="display_look_filter">%1$s Đặt chia tỷ lệ thành %2$s.</string> + <string name="display_match_screen">Khớp màn hình (%1$d×%2$d)</string> + <string name="display_follow_screen">Theo màn hình (đổi kích thước khi gập hoặc mở)</string> + <string name="mode_tab_steam">Steam</string> + <string name="device_info_show">Hiện chi tiết thiết bị</string> + <string name="device_info_hide">Ẩn chi tiết thiết bị</string> + <string name="device_info_loading">Đang đọc thiết bị…</string> + <string name="device_info_copy">Sao chép</string> + <string name="device_info_copy_hint">Sao chép các chi tiết này vào báo cáo lỗi.</string> + <string name="device_info_copied">Đã sao chép.</string> + <string name="force_ssbs_title">Buộc SSBS</string> + <string name="force_ssbs_hint">Giữ một thiết lập tốc độ CPU mà Wine liên tục tắt. Có thể giúp game Windows nhanh hơn trên Snapdragon 8 Elite; thay đổi ít thấy trên chip cũ hơn. Áp dụng vào lần game khởi động kế tiếp.</string> + + <string name="rail_stores">Stores</string> + <string name="source_steam">Steam</string> + <string name="source_added">Tùy chỉnh</string> + <string name="setup_stores">Stores</string> + <string name="setup_stores_open_on">Mở một store trên</string> + <string name="setup_stores_speed">Mức tốc độ tải</string> + <string name="setup_stores_engine">Engine stores</string> + <string name="setup_stores_engine_chip_ready">● Engine sẵn sàng</string> + <string name="setup_stores_engine_chip_missing">Chưa build</string> + <string name="stores_tier_balanced">Cân bằng</string> + <string name="stores_tier_fast">Nhanh</string> + <string name="stores_tier_max">Tối đa</string> + <string name="stores_title">Stores</string> + <string name="stores_settings">Thiết lập Stores</string> + <string name="stores_settings_eyebrow">Thiết lập</string> + <string name="stores_chip_signed_out">(chưa đăng nhập)</string> + <string name="stores_tab_store">Store</string> + <string name="stores_tab_installed">Đã cài</string> + <string name="stores_tab_library">Thư viện</string> + <string name="stores_tab_all">Tất cả</string> + <string name="stores_tab_downloads">Downloads</string> + <string name="stores_tab_count">%1$s (%2$d)</string> + <string name="stores_signin">Đăng nhập</string> + <string name="stores_signout_named">%1$s · đăng xuất</string> + <string name="stores_signout_title">Đăng xuất khỏi %1$s?</string> + <string name="stores_signout_note">Game đã cài vẫn giữ. Thư viện sẽ được tải lại khi bạn đăng nhập lại.</string> + <string name="stores_signout_stay">Vẫn đăng nhập</string> + <string name="stores_signout_go">Đăng xuất</string> + <string name="stores_search_catalog">Tìm trong danh mục %1$s…</string> + <string name="stores_search_installed">Tìm game đã cài…</string> + <string name="stores_search_library">Tìm trong thư viện của bạn…</string> + <string name="stores_search_open">Tìm kiếm</string> + <string name="stores_installed_empty">Chưa cài gì</string> + <string name="stores_library_loading">Đang tải</string> + <string name="stores_nothing_matches">Không có game</string> + <string name="stores_shelves_loading">Đang tải store…</string> + <string name="stores_amazon_no_catalog">Không có danh mục</string> + <string name="stores_shelf_new">Mới</string> + <string name="stores_shelf_deals">Ưu đãi</string> + <string name="stores_shelf_free">Game miễn phí</string> + <string name="stores_shelf_trending">Xu hướng</string> + <string name="stores_shelf_library">Thư viện của bạn</string> + <string name="stores_play">Chơi</string> + <string name="stores_install">Cài</string> + <string name="stores_resume_install">Tiếp tục cài</string> + <string name="stores_dl_interrupted">Bị gián đoạn</string> + <string name="stores_install_size">Cài · %1$s</string> + <string name="stores_get_free">Lấy miễn phí</string> + <string name="stores_view_on">Xem trên %1$s</string> + <string name="stores_buy_price">Mua · %1$s</string> + <string name="stores_installed_chip">Đã cài</string> + <string name="stores_in_library">Trong thư viện</string> + <string name="stores_free">Miễn phí</string> + <string name="stores_resume">Tiếp tục</string> + <string name="stores_refresh">Làm mới</string> + <string name="stores_show_mature">Hiện nội dung người lớn</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">Ngoại tuyến</string> + <string name="epic_card_none">Tắt</string> + <string name="epic_eos">Đăng nhập Epic (EOS)</string> + <string name="epic_offline">Khởi chạy ngoại tuyến</string> + <string name="epic_resolve">Giải quyết đăng nhập Epic</string> + <string name="store_cloud_saves">Tệp lưu trên cloud</string> + <string name="cloud_upload">Tải lên cloud</string> + <string name="cloud_download">Tải xuống từ cloud</string> + <string name="cloud_none">Không có tệp lưu trên cloud</string> + <string name="cloud_conflict">Xung đột cloud</string> + <string name="cloud_conflict_files">Xung đột cloud · %1$d tệp</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">Giữ bản cloud</string> + <string name="cloud_keep_local">Giữ bản cục bộ</string> + <string name="cloud_checking">Đang kiểm tra cloud…</string> + <string name="cloud_last_sync">Đồng bộ lần cuối %1$s</string> + <string name="cloud_never">Chưa đồng bộ</string> + <string name="cloud_done">%1$d tệp</string> + <string name="stores_eyebrow_installed">đã cài</string> + <string name="stores_eyebrow_library">trong thư viện của bạn</string> + <string name="stores_eyebrow_store">store</string> + <string name="achievements_title">Thành tích</string> + <string name="achievements_count">Đã mở khóa %1$d/%2$d</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">Đã mở khóa · %1$d</string> + <string name="achievements_group_locked">Đang khóa · %1$d</string> + <string name="achievements_group_hidden">Đang ẩn · %1$d</string> + <string name="achievements_hidden">Thành tích ẩn</string> + <string name="achievements_hidden_detail">Chi tiết sẽ hiện ra khi mở khóa.</string> + <string name="achievements_all">Đã mở khóa mọi thành tích</string> + <string name="achievements_reading">Đang tải…</string> + <string name="achievements_signed_out">Đăng nhập %1$s để xem thành tích của bạn.</string> + <string name="achievements_unreachable">Không thể kết nối tới %1$s. Hãy kiểm tra lại khi có mạng.</string> + <string name="achievements_none">%1$s không liệt kê thành tích nào cho game này.</string> + <string name="achievements_show_all">Hiện tất cả %1$d</string> + <string name="achievements_dialog_title">Thành tích · %1$s</string> + <string name="stores_in_steam">✓ Trong Steam</string> + <string name="stores_uninstall">Gỡ cài đặt</string> + <string name="stores_uninstalling">Đang gỡ cài đặt…</string> + <string name="stores_uninstall_title">Gỡ cài đặt %1$s?</string> + <string name="stores_uninstall_body_size">Giải phóng %1$s và gỡ nó khỏi Steam.</string> + <string name="stores_uninstall_body">Xóa các tệp của nó và shortcut Steam của nó.</string> + <string name="stores_launch_not_registered">Steam sẽ nhận ra game vào lần khởi động kế tiếp; hãy khởi chạy từ đó, hoặc mở Steam ngay.</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">Lựa chọn của Steam cho mỗi game</string> + <string name="stores_card_audio">Âm thanh</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">Components</string> + <string name="stores_card_components_value">FEX, DXVK, VKD3D</string> + <string name="stores_card_engine">Engine</string> + <string name="stores_card_engine_value">%1$s · gốc</string> + <string name="stores_card_engine_java">%1$s · tích hợp</string> + <string name="stores_how_it_runs">Cách nó chạy</string> + <string name="stores_kv_fetched">Lấy từ</string> + <string name="stores_kv_fetched_value">%1$s, bằng tài khoản của bạn</string> + <string name="stores_kv_installed_to">Cài vào</string> + <string name="stores_kv_installed_to_value">Lưu trữ game (Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">Khởi chạy bởi</string> + <string name="stores_kv_launched_by_value">Steam và Proton, cùng đường dẫn với game Steam</string> + <string name="stores_kv_launched_by_epic">Steam và Proton · đăng nhập Epic mỗi lần khởi chạy, lớp phủ EOS tắt</string> + <string name="stores_kv_launched_by_amazon">Steam và Proton · đã đặt biến Amazon Games Services</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">được thêm vào Steam khi cài (Deck UI + Steam Input)</string> + <string name="stores_downloads_empty">Không có lượt tải</string> + <string name="stores_dl_paused">Đã tạm dừng</string> + <string name="stores_dl_queued">Đang chờ</string> + <string name="stores_dl_queued_at">Đang chờ · #%1$d</string> + <string name="stores_dl_cancelled">Đã hủy</string> + <string name="stores_dl_failed">Thất bại</string> + <string name="stores_dl_failed_reason">Thất bại · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">Tạm dừng</string> + <string name="stores_dl_resume">Tiếp tục</string> + <string name="stores_dl_cancel">Hủy</string> + <string name="stores_dl_retry">Thử lại</string> + <string name="stores_dl_clear">Xóa</string> + <string name="stores_dl_clear_all">Xóa các mục đã xong</string> + <string name="stores_stage_manifest">Manifest</string> + <string name="stores_stage_download">Tải xuống</string> + <string name="stores_stage_verify">Xác minh</string> + <string name="stores_stage_install">Cài</string> + <string name="stores_stage_done">Xong</string> + <string name="stores_stage_checking">Đang kiểm tra</string> + <string name="stores_stage_downloading">Đang tải</string> + <string name="stores_dl_cancel_confirm">Xóa lượt tải?</string> + <string name="stores_dl_failed_kept">Thất bại · %1$s được giữ lại</string> + <string name="stores_stage_verifying">Đang xác minh</string> + <string name="stores_stage_installing">Đang cài</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d tệp</string> + <string name="stores_download_manager">Quản lý tải xuống</string> + <string name="stores_parallel">Số lượt tải cùng lúc</string> + <string name="stores_install_where">Cài vào</string> + <string name="stores_install_title">Cài %1$s:</string> + <string name="stores_target_internal">Nội bộ</string> + <string name="stores_target_free">%1$s trống</string> + <string name="stores_notification_channel">Tải xuống Store</string> + <string name="stores_notification_channel_hint">Hiển thị tiến trình tải game và giữ các lượt tải tiếp tục chạy ở nền</string> + <string name="stores_notification_one">Đang tải</string> + <string name="stores_notification_many">%1$d lượt tải</string> + <string name="stores_notification_waiting">Đang chuẩn bị…</string> + <string name="stores_notification_done_channel">Đã hoàn tất tải Store</string> + <string name="stores_notification_installed">Đã cài %1$s</string> + <string name="stores_notification_failed">Tải %1$s thất bại</string> + <string name="stores_notification_delete">Xóa</string> + <string name="stores_notification_keep">Giữ</string> + <string name="stores_login_title">Đăng nhập vào %1$s</string> + <string name="stores_login_provider_title">Đăng nhập bằng nhà cung cấp của bạn</string> + <string name="stores_login_reload">Tải lại</string> + <string name="stores_login_close">Đóng</string> + <string name="stores_login_retry">Thử lại</string> + <string name="stores_login_finishing">Đang hoàn tất đăng nhập…</string> + <string name="stores_login_error_network">Không thể tải trang đăng nhập: %1$s</string> + <string name="stores_login_error_idp_blocked">Nhà cung cấp này từ chối đăng nhập từ một trang nhúng. Hãy đăng nhập bằng mật khẩu store của bạn thay vào đó.</string> + <string name="stores_login_error_http">Trang đăng nhập trả về lỗi (HTTP %1$d).</string> + <string name="stores_login_error_ssl">Kết nối đến trang đăng nhập không an toàn.</string> + <string name="stores_login_error_verification">Không thể xác minh đăng nhập. Hãy thử lại.</string> + <string name="stores_login_error_generic">Đăng nhập thất bại. Hãy thử lại.</string> + <string name="common_close">Đóng</string> + <string name="common_back">Quay lại</string> + <string name="common_edit">Sửa</string> + <string name="games_add_all">Thêm tất cả game trong thư mục này</string> + <string name="games_added_none">Không có gì mới trong thư mục này</string> + <string name="edit_name">Tên</string> + <string name="edit_target">Đích</string> + <string name="edit_start_in">Khởi chạy trong</string> + <string name="edit_launch_options">Tùy chọn khởi chạy</string> + <string name="edit_artwork">Hình ảnh</string> + <string name="edit_other_exe">Exe khác…</string> + <string name="edit_remove">Xóa</string> + <string name="art_cover">Ảnh bìa</string> + <string name="art_background">Hình nền</string> + <string name="art_logo">Logo</string> + <string name="art_icon">Biểu tượng</string> + <string name="art_src_folder">Thư mục game</string> + <string name="art_src_steam">Steam</string> + <string name="art_src_sgdb">SteamGridDB</string> + <string name="art_src_file">Tệp của bạn</string> + <string name="art_src_exe">Từ exe</string> + <string name="art_src_auto">Tự động</string> + <string name="art_pick_image">Chọn hình ảnh…</string> + <string name="art_pick_title">Chọn một hình ảnh: %1$s</string> + <string name="art_reset">Đặt lại về tự động</string> + <plurals name="games_added_n"> + <item quantity="other">%1$d game đã được thêm</item> + </plurals> + <plurals name="games_already_n"> + <item quantity="other">%1$d đã có trong Games</item> + </plurals> + <string name="hop_back">B · Về lại %1$s</string> + <string name="hop_from_games">game đó</string> + <string name="hop_from_play">Thiết lập Play</string> + <string name="sheet_graphics_drivers">Driver đồ họa</string> + <string name="comp_this_proton">Proton này</string> + <string name="comp_every_proton">Mọi Proton</string> + <string name="comp_for_every_proton">Cho mọi Proton</string> + <string name="comp_available_count">Khả dụng · %1$d</string> + <string name="comp_checked">Đã kiểm tra %1$s</string> + <string name="comp_check_all">Kiểm tra thành phần và driver mới</string> + <string name="comp_get">Lấy</string> + <string name="comp_proton_bar">%1$s · DXVK, VKD3D-Proton và FEX dưới đây thuộc về Proton này</string> + <string name="comp_device_tested">%1$s · đã kiểm thử</string> + <string name="comp_device_untested">%1$s · chưa kiểm thử</string> + <string name="comp_device_unsupported">%1$s · không hỗ trợ</string> + <string name="gpu_auto_keeps">Giữ cặp được đề xuất luôn mới</string> + <string name="gpu_bar_auto">Tự động · %1$s</string> + <string name="gpu_separately">Driver runtime và hiển thị riêng biệt</string> + <string name="gpu_use_title">Dùng %1$s?</string> + <string name="proton_remove_title">Xóa %1$s?</string> + <string name="setup_tab_diagnostics">Chẩn đoán</string> + <string name="setup_ready">Sẵn sàng để chơi</string> + <string name="setup_not_ready">Chưa sẵn sàng để chơi</string> + <string name="setup_ready_runtime">runtime %1$s</string> + <string name="setup_ready_limit_off">giới hạn tiến trình đã tắt</string> + <string name="setup_ready_signed_in">đã đăng nhập với %1$s</string> + <string name="setup_needs_you">Khi có gì cần bạn</string> + <string name="setup_group_look">Giao diện</string> + <string name="setup_group_device">Thiết bị này</string> + <string name="setup_group_art">Hình ảnh thư viện</string> + <string name="flathub_eyebrow">ỨNG DỤNG LINUX</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Ứng dụng desktop cho desktop Linux, mở từ Desktop.</string> + <string name="flathub_setup">Thiết lập Flathub</string> + <string name="flathub_remove_title">Xóa %1$s?</string> + <string name="flathub_remove_note">Thiết lập và tệp của nó được giữ lại.</string> + <string name="flathub_keep">Giữ</string> + <string name="desktop_size">Dung lượng trên đĩa</string> + <string name="desktop_uninstall_title">Gỡ cài đặt desktop?</string> + <string name="desktop_all_systems">Tất cả</string> + <string name="gog_achievements">Thành tích GOG</string> + <string name="gog_achievements_hint">Đồng bộ thành tích cho các game GOG được hỗ trợ. Áp dụng từ lần khởi động kế tiếp.</string> + <string name="gog_achievements_off">Đã tắt thành tích</string> + <string name="gog_achievements_on">Đã bật thành tích</string> + <string name="gog_achievements_signin">Đăng nhập lại GOG</string> + <string name="gog_achievements_uploaded">Đã tải thành tích lên</string> + <string name="gog_achievements_pending">Đang chờ tải thành tích lên</string> + <string name="gog_achievements_enable_store">Bật thành tích GOG trong phần cài đặt Stores</string> + <string name="gog_achievements_unavailable">Không có thành tích; hãy kiểm tra nhật ký phiên</string> + <string name="achievement_unlocked">Đã mở khóa thành tích</string> + <string name="touch_paste">Dán</string> + <string name="touch_dpad">Phím chữ thập</string> + <string name="touch_left_stick">Cần analog trái</string> + <string name="touch_right_stick">Cần analog phải</string> + <string name="touch_menu">Menu</string> + <string name="touch_keyboard">Bàn phím</string> + <string name="touch_dpad_up">Phím chữ thập lên</string> + <string name="touch_dpad_down">Phím chữ thập xuống</string> + <string name="touch_dpad_left">Phím chữ thập trái</string> + <string name="touch_dpad_right">Phím chữ thập phải</string> + <string name="touch_title">Điều khiển chạm</string> + <string name="touch_controller">Bộ điều khiển chạm</string> + <string name="touch_mouse">Chuột</string> + <string name="touch_on">Bật</string> + <string name="touch_off">Tắt</string> + <string name="touch_speed_slow">Chậm</string> + <string name="touch_speed_normal">Bình thường</string> + <string name="touch_speed_fast">Nhanh</string> + <string name="touch_edit_layout">Chỉnh sửa bố cục</string> + <string name="touch_droiddeck_menu">Menu DroidDeck</string> + <string name="touch_drag_hint">Kéo các nút điều khiển vào màn hình</string> + <string name="touch_arrange">Sắp xếp</string> + <string name="touch_discard">Hủy bỏ</string> + <string name="touch_saving">Đang lưu…</string> + <string name="touch_icon">Biểu tượng</string> + <string name="touch_narrower">R−</string> + <string name="touch_wider">R+</string> + <string name="touch_shorter">C−</string> + <string name="touch_taller">C+</string> + <string name="touch_hide">Ẩn</string> + <string name="touch_color_opacity">Màu và độ mờ…</string> + <string name="touch_copy_layout">Sao chép bố cục này</string> + <string name="touch_layout_copied">Đã sao chép bố cục; hãy dán vào bố cục chạm của bất kỳ trò chơi nào</string> + <string name="touch_paste_layout">Dán bố cục đã sao chép</string> + <string name="touch_reset_layout">Đặt lại về mặc định của Steam</string> + <string name="touch_no_binding">Nút điều khiển này không có liên kết nào trong Steam để gắn biểu tượng</string> + <string name="touch_icon_for">Biểu tượng cho %1$s</string> + <string name="touch_layout_color">Màu bố cục chạm</string> + <string name="touch_done">Xong</string> + <string name="touch_save_failed">Không thể lưu bố cục chạm</string> + <string name="touch_saved">Đã lưu bố cục chạm vào Steam</string> + <string name="touch_app_id">Ứng dụng %1$d</string> + <string name="touch_label_hint">Nhãn (không bắt buộc)</string> + <string name="touch_icons_missing">Không tìm thấy biểu tượng của Steam</string> + <plurals name="touch_icons_found"> + <item quantity="other">%1$d biểu tượng từ Steam</item> + </plurals> + <string name="touch_icon_color">Màu biểu tượng</string> + <string name="touch_button_color">Màu nút</string> + <string name="touch_no_icon">Không có biểu tượng</string> + + <string name="stores_sync_sign_in">Đang kiểm tra đăng nhập…</string> + <string name="stores_sync_library">Đang lấy thư viện của bạn…</string> + <string name="stores_sync_game_list">Đang lấy danh sách trò chơi…</string> + <string name="stores_sync_details">Đang tải chi tiết trò chơi… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="other">Đang đồng bộ %1$d trò chơi…</item> + </plurals> + <string name="stores_sync_progress">Đang đồng bộ… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Đang lấy ảnh bìa…</string> + <string name="stores_sync_failed">Đồng bộ không thành công</string> + <string name="stores_sync_no_games">Không tìm thấy trò chơi nào trong thư viện %1$s của bạn</string> + <string name="stores_sync_amazon_no_answer">Amazon Games không phản hồi về thư viện của bạn: hãy làm mới, hoặc đăng nhập lại</string> + <string name="stores_sync_gog_unreachable">Không thể kết nối với GOG</string> + <string name="stores_sync_gog_unreadable">Không thể đọc thư viện GOG của bạn</string> + <string name="stores_registering">Đang đăng ký với Steam…</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Chế độ Cardboard</string> + <string name="cardboard_mode_hint">Hình cho mắt trái và mắt phải dành cho kính VR điện thoại.</string> + <string name="cardboard_source">Hình ảnh</string> + <string name="cardboard_source_screen">Màn hình ảo</string> + <string name="cardboard_source_sbs">3D cạnh nhau</string> + <string name="cardboard_viewer">Kính</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard (2014)</string> + <string name="cardboard_viewer_none">Không có thấu kính</string> + <string name="cardboard_ipd">Khoảng cách mắt</string> + <string name="cardboard_screen_width">Chiều rộng màn hình</string> + <string name="cardboard_screen_distance">Khoảng cách màn hình</string> + <string name="cardboard_head_tracking">Theo dõi đầu</string> + <string name="cardboard_no_sensor">Thiết bị này không có cảm biến xoay.</string> + <string name="cardboard_recenter">Căn giữa lại góc nhìn</string> + <string name="cardboard_recenter_button">Căn giữa lại</string> + <string name="cardboard_steamvr">SteamVR (thử nghiệm)</string> + <string name="cardboard_steamvr_install">Cài đặt</string> + <string name="cardboard_steamvr_install_hint">Tải SteamVR ARM64 của Valve (571 MB).</string> + <string name="cardboard_steamvr_installing">Đang tải và giải nén SteamVR…</string> + <string name="cardboard_steamvr_start">Bắt đầu</string> + <string name="cardboard_steamvr_ready_hint">Chạy SteamVR với đầu bạn làm kính; hình ảnh chuyển sang 3D cạnh nhau.</string> + <string name="cardboard_steamvr_stop">Dừng</string> + <string name="cardboard_steamvr_running_hint">SteamVR đang chạy.</string> + <string name="cardboard_steamvr_unavailable">Không khả dụng trong phiên này.</string> + <string name="cardboard_steamvr_failed">SteamVR không phản hồi; hãy xem nhật ký phiên.</string> + <string name="drawer_component_downloads">Tải thành phần</string> + <string name="drawer_component_downloads_hint">Bản tải xuống được dùng chung với trang Components. Chọn một phiên bản đã tải ở trên để dùng vào lần khởi chạy game kế tiếp.</string> + <string name="drawer_component_choose_download">Chọn phiên bản để tải</string> + <string name="stores_fact_developer">Nhà phát triển</string> + <string name="stores_fact_publisher">Nhà phát hành</string> + <string name="stores_fact_released">Phát hành</string> + <string name="stores_hltb_main">Cốt truyện chính</string> + <string name="stores_hltb_extras">+ Nội dung phụ</string> + <string name="stores_hltb_completionist">Hoàn thành 100%</string> + <string name="stores_hltb_hours">%1$s giờ</string> + <string name="stores_hltb_minutes">%1$d phút</string> +</resources> diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index a5539e8db..d3a8574a8 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -15,8 +15,10 @@ <string name="session_channel">会话</string> <string name="session_channel_description">在 Steam 或桌面会话打开时显示</string> <string name="session_notification">Steam 正在运行</string> + <string name="session_notification_downloads">Steam 正在下载</string> <string name="session_notification_paused">会话已暂停 — 准备好后点按恢复</string> <string name="resume_session">恢复</string> + <string name="suspend_now">立即暂停</string> <string name="stop_session">停止会话</string> <string name="frame_gen_title">帧生成</string> <string name="frame_gen_hint">选择帧生成模式</string> @@ -182,6 +184,8 @@ <string name="add_app_flatpak_id">应用 ID</string> <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> <string name="add_app_flatpak_invalid">不是 Flathub 应用 ID</string> + <string name="add_app_flatpak_bundle_hint">或选择 .flatpak 文件,用于不在 Flathub 上的应用。</string> + <string name="add_app_pick_flatpak">选择 .flatpak 文件</string> <string name="add_app_source">来源</string> <string name="add_app_appearance">在你的桌面上</string> <string name="add_app_preview_name">新应用</string> @@ -251,6 +255,10 @@ <string name="mode_osc">屏幕控制</string> <string name="mode_osc_qam">Steam + QAM</string> <string name="mode_osc_note">自动模式会在未连接手柄时显示屏幕控制。</string> + <string name="osc_steam_touch">Steam 触控(测试版)</string> + <string name="osc_style_next_session">Steam 的触控按键将从下次 Steam 会话开始生效</string> + <string name="osc_steam_layout">Steam 触控布局</string> + <string name="osc_steam_layout_hint">移动、调整大小或隐藏此游戏的触控按键;保存到 Steam</string> <string name="mode_controller">手柄</string> <string name="mode_controller_hint">决定 Steam 把你的手柄识别为哪种类型,将在下次会话生效。</string> <string name="mode_controller_deck">Steam Deck 手柄</string> @@ -268,6 +276,8 @@ <string name="mode_suspend_manual">手动</string> <string name="mode_suspend_native">原生</string> <string name="mode_suspend_steam_note">自动会暂停并在显示时恢复。原生会先让 Steam 做好睡眠准备。手动会等待点按恢复。“从不”则保持运行。</string> + <string name="mode_background_downloads">后台继续下载</string> + <string name="mode_background_downloads_hint">Steam 正在下载时推迟后台暂停。</string> <string name="mode_suspend_note">自动会在后台暂停并在显示时恢复。手动会等待点按恢复。“从不”则保持运行。</string> <string name="mode_startup">启动</string> <string name="mode_steam_startup">随 DroidDeck 启动 Steam</string> @@ -331,6 +341,7 @@ <string name="wincomp_found_shipped">游戏含有 %1$s(通常可直接使用)</string> <string name="wincomp_already_here">此游戏中已有</string> <string name="wincomp_no_recommendation">游戏文件夹中没有指向任何组件的内容。</string> + <string name="wincomp_active">已为此游戏安装(%1$d)</string> <string name="wincomp_all">全部组件(%1$d)</string> <string name="wincomp_installers">需要 Windows 安装程序(%1$d)</string> <string name="wincomp_installers_note">这些组件需要在前缀中运行真正的安装程序(.NET、PhysX、XNA、完整的 Visual C++ 安装程序等)。DroidDeck 暂时无法做到,接下来会支持。</string> @@ -341,6 +352,9 @@ <string name="wincomp_loading">正在加载组件…</string> <string name="wincomp_failed">无法安装:%1$s</string> <string name="wincomp_downloaded">已下载</string> + <string name="wincomp_downloading">正在下载 %1$s</string> + <string name="wincomp_installing">正在安装:%1$s</string> + <string name="wincomp_reverting">正在关闭…</string> <string name="wincomp_download_size">下载 %1$s</string> <string name="wincomp_next_launch">将在下次启动游戏时生效。</string> <string name="mode_stretch169">拉伸 16:9 比例的游戏</string> @@ -397,6 +411,44 @@ <string name="comp_check_nightlies">检查 Nightlies 上的新软件包</string> <string name="comp_proton">Proton</string> <string name="comp_proton_note">此处选择的 Proton 会运行所有未在 Steam 中单独选择 Proton 的游戏,在 Steam 中所做的更改也会显示在这里。Steam 更新时会替换 Valve 的 Proton;每个构建的原件都会保留。</string> + <string name="comp_scope">配置</string> + <string name="comp_profile_configure">设置配置档案</string> + <string name="comp_profile">配置档案</string> + <string name="comp_scope_general">默认配置档案</string> + <string name="comp_scope_general_detail">没有覆盖设置的游戏会自动使用。</string> + <string name="comp_profile_game_detail">%1$s 启动时自动使用的已保存设置。</string> + <string name="comp_game_hint">只选择此游戏需要的设置。其余设置仍使用默认配置档案。</string> + <string name="comp_game_fex">FEX 预设</string> + <string name="comp_game_fex_inherited">正在使用默认配置档案的预设。</string> + <string name="comp_game_fex_override">已为此游戏保存自定义预设。</string> + <string name="comp_game_fex_custom">自定义 FEX 变量</string> + <string name="comp_game_use_general">使用默认</string> + <string name="comp_game_compatibility">游戏兼容性</string> + <string name="comp_game_environment_hint">此游戏的 Proton、DXVK、VKD3D 及同步环境选项。</string> + <string name="comp_game_proton_inherited">正在使用默认配置档案的 Proton。</string> + <string name="comp_game_proton_override">已通过 Steam 兼容性映射为此游戏保存自定义 Proton。</string> + <string name="comp_no_proton_short">无 Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">使用默认配置档案中来自 %1$s 的组件包,除非此游戏另选了其他组件包。</string> + <string name="comp_game_component_general">使用默认配置档案的组件包。</string> + <string name="comp_game_component_override">仅此游戏使用 %1$s。</string> + <string name="comp_game_runtime_options">运行时选项</string> + <string name="comp_game_environment_other_hint">其他按游戏设置的 Proton、DXVK、VKD3D-Proton 及同步环境选项。</string> + <string name="comp_game_saved_next">%1$s 的配置档案已保存,将在下次启动游戏时生效。</string> + <string name="game_no_steam_title">不启动 Steam 直接开始</string> + <string name="game_no_steam_detail">适用于一开就立即关闭的游戏:不经过 Proton 的 Steam 设置来启动。叠加层等 Steam 功能对该游戏会关闭。</string> + <string name="game_no_steam_on">开</string> + <string name="comp_game_proton_live">%1$s 的 Proton 选择已保存,现已在 Steam 中可用。</string> + <string name="comp_game_proton_next">%1$s 的 Proton 选择已保存。启动游戏前请重启 Steam。</string> + <string name="comp_game_proton_not_runnable">%1$s 无法在此设备上使用所选的 Proton。</string> + <string name="comp_game_load_failed">无法加载 %1$s 的配置档案:%2$s</string> + <string name="comp_game_save_failed">无法保存游戏配置档案:%1$s</string> + <string name="comp_game_reset">重置游戏配置档案</string> + <string name="comp_game_reset_title">重置 %1$s 的配置档案?</string> + <string name="comp_game_reset_body">移除此游戏的 Proton、FEX、组件包、环境变量、纹理过滤和 Windows 组件覆盖设置?之后它将使用默认配置档案。</string> + <string name="comp_game_reset_done">%1$s 现在使用默认配置档案。</string> + <string name="comp_game_reset_failed">无法完全重置游戏配置档案:%1$s</string> <string name="comp_default_live">%1$s 现在将运行所有未在 Steam 中单独选择 Proton 的游戏。</string> <string name="comp_default_next">从下次 Steam 启动起,%1$s 将运行所有未单独选择 Proton 的游戏。</string> <string name="comp_default_not_runnable">%1$s 无法在此设备启动游戏,因此不能设为默认。</string> @@ -560,6 +612,18 @@ <string name="drawer_stop_title">停止会话?</string> <string name="drawer_running_now">当前正在运行</string> <string name="drawer_no_game">没有游戏在 Proton 上运行</string> + <string name="drawer_selected_profile">游戏配置档案</string> + <string name="drawer_selected_profile_none">配置档案详情暂不可用。</string> + <string name="drawer_profile_refresh_failed_initial">无法加载游戏配置档案。正在自动重试。</string> + <string name="drawer_profile_refresh_failed_stale">无法刷新游戏配置档案。正在显示上次可用的数据,并自动重试。</string> + <string name="drawer_profile_inspect_hint">查看和编辑下次启动时使用的已保存设置。</string> + <string name="drawer_profile_default">正在使用默认配置档案</string> + <string name="drawer_profile_custom">自定义配置档案</string> + <string name="drawer_component_inherited">正在使用默认配置档案中的 %1$s。</string> + <string name="drawer_game_components_note">更改会保存到 %1$s 的配置档案,并在下次启动游戏时生效。</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="other">%1$d 项游戏覆盖设置</item> + </plurals> <string name="drawer_no_proton">尚未安装 Proton。</string> <string name="drawer_game_running">游戏运行中</string> <string name="drawer_reapplied">于启动 %1$s 时重新应用</string> @@ -616,6 +680,14 @@ <string name="setup_saved_logs">已保存的会话日志</string> <string name="setup_saved_logs_hint">保留最新的 %1$d 个</string> <string name="setup_clear_logs">清除日志</string> + <string name="setup_reset_settings">重置所有设置</string> + <string name="setup_reset_settings_hint">将所有设置恢复为默认值</string> + <string name="setup_reset_settings_button">重置</string> + <string name="setup_reset_settings_title">重置所有设置?</string> + <string name="setup_reset_settings_body">显示、性能、手柄、主题、语言和启动器设置将恢复为默认值。你的游戏、Steam 登录、商店账户、已下载的驱动和存储文件夹会被保留。</string> + <string name="setup_reset_settings_profiles">同时重置游戏配置档案(每个游戏的 Proton、FEX、组件和环境,以及默认配置档案的环境)</string> + <string name="setup_reset_settings_session_running">请先停止会话再重置设置。</string> + <string name="setup_reset_settings_partial">设置已重置,但部分内容未能重置。详情请查看日志。</string> <string name="setup_offline">离线模式</string> <string name="setup_sign_in_first">请先登录 Steam</string> <string name="setup_launcher">启动器</string> @@ -631,6 +703,10 @@ <string name="setup_home">用作主屏幕</string> <string name="setup_home_on">DroidDeck 可以设为主屏幕应用</string> <string name="setup_home_off">系统不会把 DroidDeck 作为主屏幕应用提供</string> + <string name="setup_orientation">屏幕方向</string> + <string name="setup_orientation_auto">自动横屏</string> + <string name="setup_orientation_landscape">横屏</string> + <string name="setup_orientation_reverse">反向横屏</string> <string name="setup_fullscreen">全屏</string> <string name="setup_fullscreen_on">隐藏 Android 状态栏和导航栏</string> <string name="setup_fullscreen_off">显示 Android 状态栏和导航栏</string> @@ -888,6 +964,12 @@ <string name="content_page_gone">该页面已不存在。请在左侧选择一个分区。</string> <string name="content_open_desktop">打开桌面</string> <string name="content_install_open_desktop">安装并打开桌面</string> + <string name="desktop_remove_title">卸载桌面</string> + <string name="desktop_remove_text">移除桌面及其自带应用。游戏、已添加的应用、设置和共享库会保留。</string> + <string name="desktop_removing">正在移除桌面</string> + <string name="desktop_removed">桌面已移除</string> + <string name="desktop_cleaning_legacy">正在移除旧的 LXQt 文件</string> + <string name="desktop_removal_unknown">请先更新 DroidDeck,再移除此桌面版本。</string> <string name="content_linux_desktop">Linux 桌面</string> <string name="content_linux_desktop_hint">KDE Plasma、Firefox 和你的模拟器</string> <string name="games_empty">你在 Steam 安装的游戏会在会话结束后显示在这里。</string> @@ -917,6 +999,8 @@ <string name="games_last_played">上次游玩于 %1$s</string> <string name="games_internal">内部存储</string> <string name="games_added">已添加游戏</string> + <string name="games_custom">自定义游戏</string> + <string name="games_store_storage">游戏存储</string> <string name="gpu_auto_hint">为此 GPU 挑选的最佳组合,并保持更新</string> <string name="gpu_manual_hint">自行挑选驱动组合</string> <string name="gpu_checking">正在检查驱动…</string> @@ -958,6 +1042,12 @@ <string name="deck_tab_left">左触控板</string> <string name="deck_tab_right">右触控板</string> <string name="deck_tab_both">两侧</string> + <string name="deck_tab_gamepad">手柄</string> + <string name="deck_surface_stick">摇杆</string> + <string name="deck_surface_trackpad">触控板</string> + <string name="deck_surface_switch">在摇杆与触控板之间切换</string> + <string name="deck_more_controls">更多 Deck 控制</string> + <string name="deck_panels">面板</string> <string name="deck_close">关闭副屏控制</string> <string name="deck_open_steam_menu">打开 Steam 菜单</string> <string name="deck_left_upper">左 · 上</string> @@ -993,6 +1083,10 @@ <string name="proton_installed">已安装 %1$s</string> <string name="proton_queued">来自较早请求的待处理项</string> <string name="proton_not_installed">未安装</string> + <string name="proton_update_available">有可用更新 → %1$s</string> + <string name="proton_remove_older">移除旧版 %1$s</string> + <string name="proton_remove_older_body">将移除 %1$s。设为使用这些版本的游戏必须改用其他 Proton。</string> + <string name="proton_remove_body">设为使用此版本的游戏必须改用其他 Proton。</string> <string name="proton_install_now">立即安装</string> <string name="proton_cancel_queued">取消排队</string> <string name="pckb_android">Android 键盘</string> @@ -1021,6 +1115,8 @@ <string name="game_env_eyebrow">Proton 游戏</string> <string name="pip_title">画中画</string> <string name="pip_auto">自动进入画中画</string> + <string name="unfolded_controls">展开屏幕上的控制</string> + <string name="unfolded_controls_hint">展开时,游戏画面位于 Steam Deck 触控板和背键上方。折叠时,游戏全屏显示。</string> <string name="pip_suspended">已暂停</string> <string name="pip_not_ready">会话就绪后即可使用画中画。</string> <string name="pip_permission">请在 Android 设置中允许 DroidDeck 的画中画。</string> @@ -1046,19 +1142,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> @@ -1066,19 +1155,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> @@ -1091,78 +1172,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> @@ -1170,19 +1228,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> @@ -1197,11 +1249,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> @@ -1214,39 +1262,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> @@ -1261,33 +1295,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> @@ -1300,28 +1321,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> @@ -1330,8 +1337,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> @@ -1349,27 +1354,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> @@ -1387,8 +1382,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> @@ -1396,8 +1389,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> @@ -1411,19 +1402,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> @@ -1431,8 +1414,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> @@ -1443,20 +1424,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> @@ -1470,8 +1445,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> @@ -1486,51 +1459,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> @@ -1539,8 +1492,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> @@ -1551,34 +1502,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> @@ -1593,75 +1531,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> @@ -1697,27 +1607,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> @@ -1725,24 +1621,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> @@ -1756,31 +1642,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> @@ -1788,19 +1665,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> @@ -1817,55 +1686,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> @@ -1890,29 +1742,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> @@ -1923,115 +1769,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> @@ -2049,6 +1841,7 @@ <string name="display_frame_rate">帧率</string> <string name="display_look_filter">%1$s 将缩放设为 %2$s。</string> <string name="display_match_screen">匹配屏幕(%1$d×%2$d)</string> + <string name="display_follow_screen">跟随屏幕(折叠或展开时调整大小)</string> <string name="mode_tab_steam">Steam</string> <string name="device_info_show">显示设备详情</string> <string name="device_info_hide">隐藏设备详情</string> @@ -2058,6 +1851,186 @@ <string name="device_info_copied">已复制。</string> <string name="force_ssbs_title">强制 SSBS</string> <string name="force_ssbs_hint">保持开启一项 Wine 会不断关闭的 CPU 速度设置。在骁龙 8 至尊版上可能让 Windows 游戏更快;在较旧芯片上变化不大。下次启动游戏时生效。</string> + + <string name="rail_stores">游戏商店</string> + <string name="source_steam">Steam</string> + <string name="source_added">自定义</string> + <string name="setup_stores">游戏商店</string> + <string name="setup_stores_open_on">打开商店时显示</string> + <string name="setup_stores_speed">下载速度档位</string> + <string name="setup_stores_engine">商店引擎</string> + <string name="setup_stores_engine_chip_ready">● 引擎就绪</string> + <string name="setup_stores_engine_chip_missing">未构建</string> + <string name="stores_tier_balanced">均衡</string> + <string name="stores_tier_fast">快速</string> + <string name="stores_tier_max">最高</string> + <string name="stores_title">游戏商店</string> + <string name="stores_settings">游戏商店设置</string> + <string name="stores_settings_eyebrow">设置</string> + <string name="stores_chip_signed_out">(未登录)</string> + <string name="stores_tab_store">商店</string> + <string name="stores_tab_installed">已安装</string> + <string name="stores_tab_library">库</string> + <string name="stores_tab_all">全部</string> + <string name="stores_tab_downloads">下载</string> + <string name="stores_tab_count">%1$s(%2$d)</string> + <string name="stores_signin">登录</string> + <string name="stores_signout_named">%1$s · 退出登录</string> + <string name="stores_signout_title">要从 %1$s 退出登录吗?</string> + <string name="stores_signout_note">已安装的游戏会保留。重新登录后将再次获取游戏库。</string> + <string name="stores_signout_stay">保持登录</string> + <string name="stores_signout_go">退出登录</string> + <string name="stores_search_catalog">搜索 %1$s 目录…</string> + <string name="stores_search_installed">搜索已安装的游戏…</string> + <string name="stores_search_library">搜索你的库…</string> + <string name="stores_search_open">搜索</string> + <string name="stores_installed_empty">尚未安装任何内容</string> + <string name="stores_library_loading">加载中</string> + <string name="stores_nothing_matches">没有游戏</string> + <string name="stores_shelves_loading">正在加载商店…</string> + <string name="stores_amazon_no_catalog">无目录</string> + <string name="stores_shelf_new">新上架</string> + <string name="stores_shelf_deals">特惠</string> + <string name="stores_shelf_free">免费游戏</string> + <string name="stores_shelf_trending">趋势</string> + <string name="stores_shelf_library">你的库</string> + <string name="stores_play">游玩</string> + <string name="stores_install">安装</string> + <string name="stores_resume_install">继续安装</string> + <string name="stores_dl_interrupted">已中断</string> + <string name="stores_install_size">安装 · %1$s</string> + <string name="stores_get_free">免费领取</string> + <string name="stores_view_on">在 %1$s 上查看</string> + <string name="stores_buy_price">购买 · %1$s</string> + <string name="stores_installed_chip">已安装</string> + <string name="stores_in_library">已在库中</string> + <string name="stores_free">免费</string> + <string name="stores_resume">继续</string> + <string name="stores_refresh">刷新</string> + <string name="stores_show_mature">显示成人内容</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">离线</string> + <string name="epic_card_none">关闭</string> + <string name="epic_eos">Epic 登录(EOS)</string> + <string name="epic_offline">离线启动</string> + <string name="epic_resolve">解决 Epic 登录问题</string> + <string name="store_cloud_saves">云存档</string> + <string name="cloud_upload">上传到云端</string> + <string name="cloud_download">从云端下载</string> + <string name="cloud_none">无云存档</string> + <string name="cloud_conflict">云存档冲突</string> + <string name="cloud_conflict_files">云存档冲突 · %1$d 个文件</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">保留云端版本</string> + <string name="cloud_keep_local">保留本地版本</string> + <string name="cloud_checking">正在检查云存档…</string> + <string name="cloud_last_sync">上次同步:%1$s</string> + <string name="cloud_never">尚未同步</string> + <string name="cloud_done">%1$d 个文件</string> + <string name="stores_eyebrow_installed">已安装</string> + <string name="stores_eyebrow_library">在你的库中</string> + <string name="stores_eyebrow_store">商店</string> + <string name="achievements_title">成就</string> + <string name="achievements_count">已解锁 %1$d / %2$d</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">已解锁 · %1$d</string> + <string name="achievements_group_locked">未解锁 · %1$d</string> + <string name="achievements_group_hidden">隐藏 · %1$d</string> + <string name="achievements_hidden">隐藏成就</string> + <string name="achievements_hidden_detail">解锁后显示详情。</string> + <string name="achievements_all">已解锁全部成就</string> + <string name="achievements_reading">读取中…</string> + <string name="achievements_signed_out">登录 %1$s 以查看你的成就。</string> + <string name="achievements_unreachable">无法连接到 %1$s。请在联网后再查看。</string> + <string name="achievements_none">%1$s 未列出此游戏的成就。</string> + <string name="achievements_show_all">显示全部 %1$d 项</string> + <string name="achievements_dialog_title">成就 · %1$s</string> + <string name="stores_in_steam">✓ 已在 Steam 中</string> + <string name="stores_uninstall">卸载</string> + <string name="stores_uninstalling">正在卸载…</string> + <string name="stores_uninstall_title">卸载 %1$s?</string> + <string name="stores_uninstall_body_size">将释放 %1$s,并从 Steam 中移除。</string> + <string name="stores_uninstall_body">将移除其文件和 Steam 快捷方式。</string> + <string name="stores_launch_not_registered">Steam 下次启动时会识别该游戏;请从 Steam 中启动它,或立即启动 Steam。</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">由 Steam 按游戏选择</string> + <string name="stores_card_audio">音频</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">组件</string> + <string name="stores_card_components_value">FEX、DXVK、VKD3D</string> + <string name="stores_card_engine">引擎</string> + <string name="stores_card_engine_value">%1$s · 原生</string> + <string name="stores_card_engine_java">%1$s · 内置</string> + <string name="stores_how_it_runs">运行方式</string> + <string name="stores_kv_fetched">获取来源</string> + <string name="stores_kv_fetched_value">%1$s(使用你的账户)</string> + <string name="stores_kv_installed_to">安装位置</string> + <string name="stores_kv_installed_to_value">游戏存储(Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">启动方式</string> + <string name="stores_kv_launched_by_value">Steam 和 Proton,与 Steam 游戏方式相同</string> + <string name="stores_kv_launched_by_epic">Steam 和 Proton · 每次启动时登录 Epic,关闭 EOS 覆盖层</string> + <string name="stores_kv_launched_by_amazon">Steam 和 Proton · 已设置 Amazon Games Services 变量</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">安装时添加到 Steam(Deck 界面 + Steam 输入)</string> + <string name="stores_downloads_empty">没有下载</string> + <string name="stores_dl_paused">已暂停</string> + <string name="stores_dl_queued">排队中</string> + <string name="stores_dl_queued_at">排队中 · 第 %1$d 位</string> + <string name="stores_dl_cancelled">已取消</string> + <string name="stores_dl_failed">失败</string> + <string name="stores_dl_failed_reason">失败 · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">暂停</string> + <string name="stores_dl_resume">继续</string> + <string name="stores_dl_cancel">取消</string> + <string name="stores_dl_retry">重试</string> + <string name="stores_dl_clear">清除</string> + <string name="stores_dl_clear_all">清除已完成</string> + <string name="stores_stage_manifest">清单</string> + <string name="stores_stage_download">下载</string> + <string name="stores_stage_verify">校验</string> + <string name="stores_stage_install">安装</string> + <string name="stores_stage_done">完成</string> + <string name="stores_stage_checking">检查中</string> + <string name="stores_stage_downloading">下载中</string> + <string name="stores_dl_cancel_confirm">删除下载?</string> + <string name="stores_dl_failed_kept">失败 · 已保留 %1$s</string> + <string name="stores_stage_verifying">校验中</string> + <string name="stores_stage_installing">安装中</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d 个文件</string> + <string name="stores_download_manager">下载管理</string> + <string name="stores_parallel">同时下载数</string> + <string name="stores_install_where">安装到</string> + <string name="stores_install_title">安装 %1$s:</string> + <string name="stores_target_internal">内部存储</string> + <string name="stores_target_free">可用 %1$s</string> + <string name="stores_notification_channel">商店下载</string> + <string name="stores_notification_channel_hint">显示游戏下载进度,并让下载在后台继续进行</string> + <string name="stores_notification_one">正在下载</string> + <string name="stores_notification_many">%1$d 个下载</string> + <string name="stores_notification_waiting">正在准备…</string> + <string name="stores_notification_done_channel">已完成的商店下载</string> + <string name="stores_notification_installed">%1$s 已安装</string> + <string name="stores_notification_failed">%1$s 下载失败</string> + <string name="stores_notification_delete">删除</string> + <string name="stores_notification_keep">保留</string> + <string name="stores_login_title">登录 %1$s</string> + <string name="stores_login_provider_title">使用第三方账号登录</string> + <string name="stores_login_reload">重新加载</string> + <string name="stores_login_close">关闭</string> + <string name="stores_login_retry">重试</string> + <string name="stores_login_finishing">正在完成登录…</string> + <string name="stores_login_error_network">无法加载登录页面:%1$s</string> + <string name="stores_login_error_idp_blocked">该第三方登录方式不允许在内嵌页面中登录。请改用商店密码登录。</string> + <string name="stores_login_error_http">登录页面返回了错误(HTTP %1$d)。</string> + <string name="stores_login_error_ssl">与登录页面的连接不安全。</string> + <string name="stores_login_error_verification">无法验证登录。请重试。</string> + <string name="stores_login_error_generic">登录失败。请重试。</string> <string name="common_close">关闭</string> <string name="common_back">返回</string> <string name="common_edit">编辑</string> @@ -2089,21 +2062,122 @@ <plurals name="games_already_n"> <item quantity="other">%1$d 个已在游戏中</item> </plurals> - <string name="achievements_title">成就</string> - <string name="achievements_count">已解锁 %1$d / %2$d</string> - <string name="achievements_count_short">%1$d / %2$d</string> - <string name="achievements_group_unlocked">已解锁 · %1$d</string> - <string name="achievements_group_locked">未解锁 · %1$d</string> - <string name="achievements_group_hidden">隐藏 · %1$d</string> - <string name="achievements_hidden">隐藏成就</string> - <string name="achievements_hidden_detail">解锁后显示详情。</string> - <string name="achievements_all">已解锁全部成就</string> - <string name="achievements_reading">读取中…</string> - <string name="achievements_signed_out">登录 %1$s 以查看你的成就。</string> - <string name="achievements_unreachable">无法连接到 %1$s。请在联网后再查看。</string> - <string name="achievements_none">%1$s 未列出此游戏的成就。</string> - <string name="achievements_show_all">显示全部 %1$d 项</string> - <string name="achievements_dialog_title">成就 · %1$s</string> + <string name="hop_back">B · 返回%1$s</string> + <string name="hop_from_games">游戏</string> + <string name="hop_from_play">游玩设置</string> + <string name="sheet_graphics_drivers">图形驱动</string> + <string name="comp_this_proton">此 Proton</string> + <string name="comp_every_proton">所有 Proton</string> + <string name="comp_for_every_proton">适用于所有 Proton</string> + <string name="comp_available_count">可用 · %1$d</string> + <string name="comp_checked">上次检查:%1$s</string> + <string name="comp_check_all">检查新组件和驱动</string> + <string name="comp_get">获取</string> + <string name="comp_proton_bar">%1$s · 下方的 DXVK、VKD3D-Proton 和 FEX 属于此 Proton</string> + <string name="comp_device_tested">%1$s · 已测试</string> + <string name="comp_device_untested">%1$s · 未测试</string> + <string name="comp_device_unsupported">%1$s · 不受支持</string> + <string name="gpu_auto_keeps">让推荐的驱动组合保持最新</string> + <string name="gpu_bar_auto">自动 · %1$s</string> + <string name="gpu_separately">分别设置运行时驱动和显示驱动</string> + <string name="gpu_use_title">使用 %1$s?</string> + <string name="proton_remove_title">移除 %1$s?</string> + <string name="setup_tab_diagnostics">诊断</string> + <string name="setup_ready">可以游玩了</string> + <string name="setup_not_ready">暂时还不能游玩</string> + <string name="setup_ready_runtime">运行时 %1$s</string> + <string name="setup_ready_limit_off">进程限制已关闭</string> + <string name="setup_ready_signed_in">已登录为 %1$s</string> + <string name="setup_needs_you">需要你处理</string> + <string name="setup_group_look">外观</string> + <string name="setup_group_device">此设备</string> + <string name="setup_group_art">游戏库美术图</string> + <string name="flathub_eyebrow">LINUX 应用</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">适用于 Linux 桌面的桌面应用,从“桌面”打开。</string> + <string name="flathub_setup">配置 Flathub</string> + <string name="flathub_remove_title">移除 %1$s?</string> + <string name="flathub_remove_note">其设置和文件会保留。</string> + <string name="flathub_keep">保留</string> + <string name="desktop_size">占用空间</string> + <string name="desktop_uninstall_title">卸载桌面?</string> + <string name="desktop_all_systems">全部</string> + <string name="gog_achievements">GOG 成就</string> + <string name="gog_achievements_hint">为受支持的 GOG 游戏同步成就。下次启动时生效。</string> + <string name="gog_achievements_off">成就已关闭</string> + <string name="gog_achievements_on">成就已开启</string> + <string name="gog_achievements_signin">重新登录 GOG</string> + <string name="gog_achievements_uploaded">成就已上传</string> + <string name="gog_achievements_pending">成就上传待处理</string> + <string name="gog_achievements_enable_store">在「商店」设置中开启 GOG 成就</string> + <string name="gog_achievements_unavailable">成就不可用;请查看会话日志</string> + <string name="achievement_unlocked">成就已解锁</string> + <string name="touch_paste">粘贴</string> + <string name="touch_dpad">方向键</string> + <string name="touch_left_stick">左摇杆</string> + <string name="touch_right_stick">右摇杆</string> + <string name="touch_menu">菜单</string> + <string name="touch_keyboard">键盘</string> + <string name="touch_dpad_up">方向键上</string> + <string name="touch_dpad_down">方向键下</string> + <string name="touch_dpad_left">方向键左</string> + <string name="touch_dpad_right">方向键右</string> + <string name="touch_title">触控</string> + <string name="touch_controller">触控控制器</string> + <string name="touch_mouse">鼠标</string> + <string name="touch_on">开</string> + <string name="touch_off">关</string> + <string name="touch_speed_slow">慢</string> + <string name="touch_speed_normal">正常</string> + <string name="touch_speed_fast">快</string> + <string name="touch_edit_layout">编辑布局</string> + <string name="touch_droiddeck_menu">DroidDeck 菜单</string> + <string name="touch_drag_hint">将控件拖到屏幕上</string> + <string name="touch_arrange">排列</string> + <string name="touch_discard">放弃</string> + <string name="touch_saving">正在保存…</string> + <string name="touch_icon">图标</string> + <string name="touch_narrower">宽−</string> + <string name="touch_wider">宽+</string> + <string name="touch_shorter">高−</string> + <string name="touch_taller">高+</string> + <string name="touch_hide">隐藏</string> + <string name="touch_color_opacity">颜色和不透明度…</string> + <string name="touch_copy_layout">复制此布局</string> + <string name="touch_layout_copied">布局已复制,可粘贴到任意游戏的触控布局中</string> + <string name="touch_paste_layout">粘贴已复制的布局</string> + <string name="touch_reset_layout">恢复为 Steam 的默认布局</string> + <string name="touch_no_binding">此控件在 Steam 中没有绑定,无法设置图标</string> + <string name="touch_icon_for">%1$s 的图标</string> + <string name="touch_layout_color">触控布局颜色</string> + <string name="touch_done">完成</string> + <string name="touch_save_failed">无法保存触控布局</string> + <string name="touch_saved">触控布局已保存到 Steam</string> + <string name="touch_app_id">应用 %1$d</string> + <string name="touch_label_hint">标签(可选)</string> + <string name="touch_icons_missing">未找到 Steam 的图标</string> + <plurals name="touch_icons_found"> + <item quantity="other">来自 Steam 的 %1$d 个图标</item> + </plurals> + <string name="touch_icon_color">图标颜色</string> + <string name="touch_button_color">按钮颜色</string> + <string name="touch_no_icon">无图标</string> + + <string name="stores_sync_sign_in">正在检查登录状态…</string> + <string name="stores_sync_library">正在获取你的游戏库…</string> + <string name="stores_sync_game_list">正在获取游戏列表…</string> + <string name="stores_sync_details">正在加载游戏详情…(%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="other">正在同步 %1$d 个游戏…</item> + </plurals> + <string name="stores_sync_progress">正在同步… %1$d/%2$d</string> + <string name="stores_sync_cover_art">正在获取封面图片…</string> + <string name="stores_sync_failed">同步失败</string> + <string name="stores_sync_no_games">在你的 %1$s 游戏库中未找到游戏</string> + <string name="stores_sync_amazon_no_answer">Amazon Games 未返回你的游戏库信息:请刷新,或重新登录</string> + <string name="stores_sync_gog_unreachable">无法连接到 GOG</string> + <string name="stores_sync_gog_unreadable">无法读取你的 GOG 游戏库</string> + <string name="stores_registering">正在向 Steam 注册…</string> <string name="drawer_page_glasses">眼镜</string> <string name="glasses_open">VITURE 眼镜</string> <string name="glasses_control">眼镜控制</string> @@ -2152,4 +2226,34 @@ <string name="stores_hltb_completionist">全收集</string> <string name="stores_hltb_hours">%1$s 小时</string> <string name="stores_hltb_minutes">%1$d 分钟</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Cardboard 模式</string> + <string name="cardboard_mode_hint">为手机 VR 眼镜提供左右眼画面。</string> + <string name="cardboard_source">画面</string> + <string name="cardboard_source_screen">虚拟屏幕</string> + <string name="cardboard_source_sbs">左右并排 3D</string> + <string name="cardboard_viewer">眼镜</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard(2014)</string> + <string name="cardboard_viewer_none">无镜片</string> + <string name="cardboard_ipd">瞳距</string> + <string name="cardboard_screen_width">屏幕宽度</string> + <string name="cardboard_screen_distance">屏幕距离</string> + <string name="cardboard_head_tracking">头部追踪</string> + <string name="cardboard_no_sensor">此设备没有旋转传感器。</string> + <string name="cardboard_recenter">重置视角</string> + <string name="cardboard_recenter_button">重置</string> + <string name="cardboard_steamvr">SteamVR(实验性)</string> + <string name="cardboard_steamvr_install">安装</string> + <string name="cardboard_steamvr_install_hint">下载 Valve 的 ARM64 版 SteamVR(571 MB)。</string> + <string name="cardboard_steamvr_installing">正在下载并解压 SteamVR…</string> + <string name="cardboard_steamvr_start">启动</string> + <string name="cardboard_steamvr_ready_hint">以你的头部作为头显运行 SteamVR;画面会切换为左右并排 3D。</string> + <string name="cardboard_steamvr_stop">停止</string> + <string name="cardboard_steamvr_running_hint">SteamVR 正在运行。</string> + <string name="cardboard_steamvr_unavailable">此会话中不可用。</string> + <string name="cardboard_steamvr_failed">SteamVR 未响应;请查看会话日志。</string> + <string name="drawer_component_downloads">下载组件</string> + <string name="drawer_component_downloads_hint">下载内容与“组件”页面共享。在上方选择已下载的版本,即可在下次启动游戏时使用。</string> + <string name="drawer_component_choose_download">选择要下载的版本</string> </resources> diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index ee0544fc1..63a65bf39 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -15,8 +15,10 @@ <string name="session_channel">會話</string> <string name="session_channel_description">Steam 或桌面會話開啟時顯示</string> <string name="session_notification">Steam 正在執行</string> + <string name="session_notification_downloads">Steam 正在下載</string> <string name="session_notification_paused">會話已暫停 - 準備好後請輕按「繼續」</string> <string name="resume_session">繼續</string> + <string name="suspend_now">立即暫停</string> <string name="stop_session">結束會話</string> <string name="frame_gen_title">幀生成</string> <string name="frame_gen_hint">選擇幀生成模式</string> @@ -182,6 +184,8 @@ <string name="add_app_flatpak_id">應用程式 ID</string> <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> <string name="add_app_flatpak_invalid">不是 Flathub 應用程式 ID</string> + <string name="add_app_flatpak_bundle_hint">亦可使用 .flatpak 檔案,適用於 Flathub 上沒有的應用程式。</string> + <string name="add_app_pick_flatpak">選擇 .flatpak 檔案</string> <string name="add_app_source">來源</string> <string name="add_app_appearance">顯示在桌面</string> <string name="add_app_preview_name">新應用程式</string> @@ -251,6 +255,10 @@ <string name="mode_osc">螢幕控制</string> <string name="mode_osc_qam">Steam + QAM</string> <string name="mode_osc_note">「自動」會在未連接控制器時顯示螢幕控制。</string> + <string name="osc_steam_touch">Steam 觸控(測試版)</string> + <string name="osc_style_next_session">Steam 的觸控控制會在下次 Steam 會話啟用</string> + <string name="osc_steam_layout">Steam 觸控佈局</string> + <string name="osc_steam_layout_hint">移動、調整大小或隱藏此遊戲的觸控控制;會儲存至 Steam</string> <string name="mode_controller">控制器</string> <string name="mode_controller_hint">Steam 如何識別你的控制器。下次會話生效。</string> <string name="mode_controller_deck">Steam Deck 控制器</string> @@ -268,6 +276,8 @@ <string name="mode_suspend_manual">手動</string> <string name="mode_suspend_native">原生</string> <string name="mode_suspend_steam_note">「自動」會暫停,並在重新顯示時繼續;「原生」會先讓 Steam 準備好休眠;「手動」會等你按「繼續」;「從不」會保持執行。</string> + <string name="mode_background_downloads">保持下載進行</string> + <string name="mode_background_downloads_hint">Steam 正在下載時,延遲在背景暫停。</string> <string name="mode_suspend_note">「自動」會在背景時暫停,並在重新顯示時繼續;「手動」會等你按「繼續」;「從不」會保持執行。</string> <string name="mode_startup">啟動</string> <string name="mode_steam_startup">隨 DroidDeck 啟動 Steam</string> @@ -331,6 +341,7 @@ <string name="wincomp_found_shipped">遊戲含有 %1$s(通常可直接使用)</string> <string name="wincomp_already_here">這款遊戲中已有</string> <string name="wincomp_no_recommendation">遊戲資料夾內沒有指向任何組件的內容。</string> + <string name="wincomp_active">已為這款遊戲安裝(%1$d)</string> <string name="wincomp_all">所有組件(%1$d)</string> <string name="wincomp_installers">需要 Windows 安裝程式(%1$d)</string> <string name="wincomp_installers_note">這些組件需要在前綴內執行真正的安裝程式(.NET、PhysX、XNA、完整的 Visual C++ 安裝程式等)。DroidDeck 暫時未能做到,下一步就會支援。</string> @@ -341,6 +352,9 @@ <string name="wincomp_loading">正在載入組件…</string> <string name="wincomp_failed">無法安裝:%1$s</string> <string name="wincomp_downloaded">已下載</string> + <string name="wincomp_downloading">正在下載 %1$s</string> + <string name="wincomp_installing">正在安裝:%1$s</string> + <string name="wincomp_reverting">正在關閉…</string> <string name="wincomp_download_size">下載 %1$s</string> <string name="wincomp_next_launch">會在下次啟動遊戲時生效。</string> <string name="mode_stretch169">拉伸 16:9 遊戲</string> @@ -397,6 +411,44 @@ <string name="comp_check_nightlies">在 Nightlies 檢查新軟件包</string> <string name="comp_proton">Proton</string> <string name="comp_proton_note">在此選擇的 Proton 會執行所有未在 Steam 中自選 Proton 的遊戲;在 Steam 中所作的變更也會在此顯示。Steam 更新時會取代 Valve 的 Proton;每個版本的原始檔案都會保留。</string> + <string name="comp_scope">配置</string> + <string name="comp_profile_configure">配置設定檔</string> + <string name="comp_profile">設定檔</string> + <string name="comp_scope_general">預設設定檔</string> + <string name="comp_scope_general_detail">沒有覆寫設定的遊戲會自動使用。</string> + <string name="comp_profile_game_detail">%1$s 啟動時會自動使用的已儲存設定。</string> + <string name="comp_game_hint">只需選擇這款遊戲需要的設定,其餘設定會繼續使用預設設定檔。</string> + <string name="comp_game_fex">FEX 預設</string> + <string name="comp_game_fex_inherited">正在沿用預設設定檔的 FEX 預設。</string> + <string name="comp_game_fex_override">已為這款遊戲儲存自訂 FEX 預設。</string> + <string name="comp_game_fex_custom">自訂 FEX 變數</string> + <string name="comp_game_use_general">使用預設</string> + <string name="comp_game_compatibility">遊戲兼容性</string> + <string name="comp_game_environment_hint">這款遊戲的 Proton、DXVK、VKD3D 及同步環境選項。</string> + <string name="comp_game_proton_inherited">正在使用預設設定檔的 Proton。</string> + <string name="comp_game_proton_override">已透過 Steam 兼容性對應為這款遊戲儲存自訂 Proton。</string> + <string name="comp_no_proton_short">無 Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">使用預設設定檔中來自 %1$s 的套件,除非這款遊戲選擇了其他套件。</string> + <string name="comp_game_component_general">使用預設設定檔的元件套件。</string> + <string name="comp_game_component_override">只在這款遊戲使用 %1$s。</string> + <string name="comp_game_runtime_options">運行環境選項</string> + <string name="comp_game_environment_other_hint">其他個別遊戲的 Proton、DXVK、VKD3D-Proton 及同步環境選項。</string> + <string name="comp_game_saved_next">已儲存 %1$s 的設定檔,下次啟動遊戲時生效。</string> + <string name="game_no_steam_title">不啟動 Steam 直接開始</string> + <string name="game_no_steam_detail">適用於一開就立即關閉的遊戲:不經過 Proton 的 Steam 設定來啟動。疊加層等 Steam 功能對該遊戲會關閉。</string> + <string name="game_no_steam_on">開</string> + <string name="comp_game_proton_live">已儲存 %1$s 的 Proton 選擇,現已可在 Steam 中使用。</string> + <string name="comp_game_proton_next">已儲存 %1$s 的 Proton 選擇。請先重新啟動 Steam,再啟動遊戲。</string> + <string name="comp_game_proton_not_runnable">%1$s 無法在此裝置上使用所選的 Proton。</string> + <string name="comp_game_load_failed">無法載入 %1$s 的設定檔:%2$s</string> + <string name="comp_game_save_failed">無法儲存遊戲設定檔:%1$s</string> + <string name="comp_game_reset">重設遊戲設定檔</string> + <string name="comp_game_reset_title">重設 %1$s 的設定檔?</string> + <string name="comp_game_reset_body">移除這款遊戲的 Proton、FEX、元件套件、環境、紋理過濾及 Windows 組件覆寫設定?遊戲會改用預設設定檔。</string> + <string name="comp_game_reset_done">%1$s 現已使用預設設定檔。</string> + <string name="comp_game_reset_failed">無法完全重設遊戲設定檔:%1$s</string> <string name="comp_default_live">%1$s 現在會執行所有未在 Steam 中自選 Proton 的遊戲。</string> <string name="comp_default_next">從下次啟動 Steam 起,%1$s 會執行所有未自選 Proton 的遊戲。</string> <string name="comp_default_not_runnable">%1$s 無法在此啟動遊戲,因此不能設為預設。</string> @@ -560,6 +612,18 @@ <string name="drawer_stop_title">停止會話?</string> <string name="drawer_running_now">正在執行</string> <string name="drawer_no_game">沒有遊戲正在 Proton 上執行</string> + <string name="drawer_selected_profile">遊戲設定檔</string> + <string name="drawer_selected_profile_none">暫時未有設定檔詳情。</string> + <string name="drawer_profile_refresh_failed_initial">無法載入遊戲設定檔,正在自動重試。</string> + <string name="drawer_profile_refresh_failed_stale">無法重新整理遊戲設定檔。正在顯示最後可用的資料,並會自動重試。</string> + <string name="drawer_profile_inspect_hint">查看及編輯下次啟動時使用的已儲存設定。</string> + <string name="drawer_profile_default">正在使用預設設定檔</string> + <string name="drawer_profile_custom">自訂設定檔</string> + <string name="drawer_component_inherited">正在使用預設設定檔中的 %1$s。</string> + <string name="drawer_game_components_note">變更會儲存至 %1$s 的設定檔,並在下次啟動遊戲時生效。</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="other">%1$d 項遊戲覆寫設定</item> + </plurals> <string name="drawer_no_proton">尚未安裝 Proton。</string> <string name="drawer_game_running">遊戲執行中</string> <string name="drawer_reapplied">於 %1$s 啟動時重新套用</string> @@ -616,6 +680,14 @@ <string name="setup_saved_logs">已儲存的會話記錄</string> <string name="setup_saved_logs_hint">保留最新的 %1$d 個</string> <string name="setup_clear_logs">清除記錄</string> + <string name="setup_reset_settings">重設所有設定</string> + <string name="setup_reset_settings_hint">將所有設定還原為預設值</string> + <string name="setup_reset_settings_button">重設</string> + <string name="setup_reset_settings_title">要重設所有設定嗎?</string> + <string name="setup_reset_settings_body">顯示、效能、控制器、主題、語言和啟動器設定將還原為預設值。你的遊戲、Steam 登入、商店帳戶、已下載的驅動程式和儲存資料夾都會保留。</string> + <string name="setup_reset_settings_profiles">同時重設遊戲設定檔(每個遊戲的 Proton、FEX、元件和環境,以及預設設定檔的環境)</string> + <string name="setup_reset_settings_session_running">請先停止會話再重設設定。</string> + <string name="setup_reset_settings_partial">設定已重設,但部分項目未能重設。詳情請查看記錄。</string> <string name="setup_offline">離線模式</string> <string name="setup_sign_in_first">請先登入 Steam</string> <string name="setup_launcher">啟動器</string> @@ -631,6 +703,10 @@ <string name="setup_home">用作主畫面</string> <string name="setup_home_on">DroidDeck 可用作主畫面應用程式</string> <string name="setup_home_off">DroidDeck 不會作為主畫面應用程式的選項</string> + <string name="setup_orientation">螢幕方向</string> + <string name="setup_orientation_auto">自動橫向</string> + <string name="setup_orientation_landscape">橫向</string> + <string name="setup_orientation_reverse">反向橫向</string> <string name="setup_fullscreen">全螢幕</string> <string name="setup_fullscreen_on">隱藏 Android 狀態列和導覽列</string> <string name="setup_fullscreen_off">顯示 Android 狀態列和導覽列</string> @@ -888,6 +964,12 @@ <string name="content_page_gone">該頁面已不存在,請在左側選擇一個部分。</string> <string name="content_open_desktop">開啟桌面</string> <string name="content_install_open_desktop">安裝並開啟桌面</string> + <string name="desktop_remove_title">解除安裝桌面</string> + <string name="desktop_remove_text">移除桌面及其內置應用程式。遊戲、已新增的應用程式、設定及共用程式庫會保留。</string> + <string name="desktop_removing">正在移除桌面</string> + <string name="desktop_removed">已移除桌面</string> + <string name="desktop_cleaning_legacy">正在移除舊的 LXQt 檔案</string> + <string name="desktop_removal_unknown">請先更新 DroidDeck,再移除此桌面版本。</string> <string name="content_linux_desktop">Linux 桌面</string> <string name="content_linux_desktop_hint">KDE Plasma、Firefox 和你的模擬器</string> <string name="games_empty">你在 Steam 安裝的遊戲會在會話結束後顯示在這裡。</string> @@ -917,6 +999,8 @@ <string name="games_last_played">上次遊玩:%1$s</string> <string name="games_internal">內部儲存空間</string> <string name="games_added">已新增的遊戲</string> + <string name="games_custom">自訂遊戲</string> + <string name="games_store_storage">遊戲儲存位置</string> <string name="gpu_auto_hint">此 GPU 的最佳組合,並會保持最新</string> <string name="gpu_manual_hint">自行選擇組合</string> <string name="gpu_checking">正在檢查驅動程式…</string> @@ -958,6 +1042,12 @@ <string name="deck_tab_left">左側觸控板</string> <string name="deck_tab_right">右側觸控板</string> <string name="deck_tab_both">兩側</string> + <string name="deck_tab_gamepad">控制器</string> + <string name="deck_surface_stick">搖桿</string> + <string name="deck_surface_trackpad">觸控板</string> + <string name="deck_surface_switch">在搖桿和觸控板之間切換</string> + <string name="deck_more_controls">更多 Deck 控制</string> + <string name="deck_panels">面板</string> <string name="deck_close">關閉第二螢幕控制</string> <string name="deck_open_steam_menu">開啟 Steam 選單</string> <string name="deck_left_upper">左側 · 上方</string> @@ -993,6 +1083,10 @@ <string name="proton_installed">已安裝 %1$s</string> <string name="proton_queued">先前的要求尚待處理</string> <string name="proton_not_installed">未安裝</string> + <string name="proton_update_available">有可用更新 → %1$s</string> + <string name="proton_remove_older">移除舊版 %1$s</string> + <string name="proton_remove_older_body">將移除 %1$s。設為使用這些版本的遊戲須改用其他 Proton。</string> + <string name="proton_remove_body">設為使用此版本的遊戲須改用其他 Proton。</string> <string name="proton_install_now">立即安裝</string> <string name="proton_cancel_queued">取消排隊</string> <string name="pckb_android">Android 鍵盤</string> @@ -1021,6 +1115,8 @@ <string name="game_env_eyebrow">Proton 遊戲</string> <string name="pip_title">畫中畫</string> <string name="pip_auto">自動進入畫中畫</string> + <string name="unfolded_controls">展開螢幕時的控制</string> + <string name="unfolded_controls_hint">展開時,遊戲會顯示在 Steam Deck 觸控板和背面握把上方;摺疊時,遊戲會全螢幕顯示。</string> <string name="pip_suspended">已暫停</string> <string name="pip_not_ready">會話準備就緒後即可使用畫中畫。</string> <string name="pip_permission">請在 Android 設定中允許 DroidDeck 使用畫中畫。</string> @@ -1046,19 +1142,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> @@ -1066,19 +1155,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> @@ -1091,78 +1172,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> @@ -1170,19 +1228,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> @@ -1197,11 +1249,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> @@ -1214,39 +1262,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> @@ -1261,33 +1295,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> @@ -1300,28 +1321,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> @@ -1330,8 +1337,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> @@ -1349,27 +1354,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> @@ -1387,8 +1382,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> @@ -1396,8 +1389,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> @@ -1411,19 +1402,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> @@ -1431,8 +1414,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> @@ -1443,20 +1424,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> @@ -1470,8 +1445,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> @@ -1486,51 +1459,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> @@ -1539,8 +1492,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> @@ -1551,34 +1502,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> @@ -1593,75 +1531,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> @@ -1697,27 +1607,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> @@ -1725,24 +1621,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> @@ -1756,31 +1642,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> @@ -1788,19 +1665,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> @@ -1817,55 +1686,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> @@ -1890,29 +1742,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> @@ -1923,115 +1769,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> @@ -2049,6 +1841,7 @@ <string name="display_frame_rate">幀率</string> <string name="display_look_filter">%1$s 將縮放設為 %2$s。</string> <string name="display_match_screen">配合螢幕(%1$d×%2$d)</string> + <string name="display_follow_screen">跟隨螢幕(摺疊或展開時調整大小)</string> <string name="mode_tab_steam">Steam</string> <string name="device_info_show">顯示裝置詳情</string> <string name="device_info_hide">隱藏裝置詳情</string> @@ -2058,6 +1851,186 @@ <string name="device_info_copied">已複製。</string> <string name="force_ssbs_title">強制 SSBS</string> <string name="force_ssbs_hint">讓一項 Wine 會不斷關閉的 CPU 速度設定保持開啟。在 Snapdragon 8 Elite 上可能令 Windows 遊戲更快;在較舊的晶片上變化不大。下次啟動遊戲時生效。</string> + + <string name="rail_stores">商店</string> + <string name="source_steam">Steam</string> + <string name="source_added">自訂</string> + <string name="setup_stores">商店</string> + <string name="setup_stores_open_on">開啟商店時顯示</string> + <string name="setup_stores_speed">下載速度等級</string> + <string name="setup_stores_engine">商店引擎</string> + <string name="setup_stores_engine_chip_ready">● 引擎已就緒</string> + <string name="setup_stores_engine_chip_missing">未建置</string> + <string name="stores_tier_balanced">平衡</string> + <string name="stores_tier_fast">快速</string> + <string name="stores_tier_max">最高</string> + <string name="stores_title">商店</string> + <string name="stores_settings">商店設定</string> + <string name="stores_settings_eyebrow">設定</string> + <string name="stores_chip_signed_out">(未登入)</string> + <string name="stores_tab_store">商店</string> + <string name="stores_tab_installed">已安裝</string> + <string name="stores_tab_library">收藏庫</string> + <string name="stores_tab_all">全部</string> + <string name="stores_tab_downloads">下載</string> + <string name="stores_tab_count">%1$s(%2$d)</string> + <string name="stores_signin">登入</string> + <string name="stores_signout_named">%1$s · 登出</string> + <string name="stores_signout_title">登出 %1$s?</string> + <string name="stores_signout_note">已安裝的遊戲會保留。重新登入時會再次取得收藏庫。</string> + <string name="stores_signout_stay">保持登入</string> + <string name="stores_signout_go">登出</string> + <string name="stores_search_catalog">搜尋 %1$s 目錄…</string> + <string name="stores_search_installed">搜尋已安裝的遊戲…</string> + <string name="stores_search_library">搜尋你的收藏庫…</string> + <string name="stores_search_open">搜尋</string> + <string name="stores_installed_empty">未安裝任何遊戲</string> + <string name="stores_library_loading">正在載入</string> + <string name="stores_nothing_matches">沒有遊戲</string> + <string name="stores_shelves_loading">正在載入商店…</string> + <string name="stores_amazon_no_catalog">沒有目錄</string> + <string name="stores_shelf_new">最新推出</string> + <string name="stores_shelf_deals">優惠</string> + <string name="stores_shelf_free">免費遊戲</string> + <string name="stores_shelf_trending">趨勢</string> + <string name="stores_shelf_library">你的收藏庫</string> + <string name="stores_play">開始遊戲</string> + <string name="stores_install">安裝</string> + <string name="stores_resume_install">繼續安裝</string> + <string name="stores_dl_interrupted">已中斷</string> + <string name="stores_install_size">安裝 · %1$s</string> + <string name="stores_get_free">免費領取</string> + <string name="stores_view_on">在 %1$s 查看</string> + <string name="stores_buy_price">購買 · %1$s</string> + <string name="stores_installed_chip">已安裝</string> + <string name="stores_in_library">已在收藏庫</string> + <string name="stores_free">免費</string> + <string name="stores_resume">繼續</string> + <string name="stores_refresh">重新整理</string> + <string name="stores_show_mature">顯示成人內容</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">離線</string> + <string name="epic_card_none">關閉</string> + <string name="epic_eos">Epic 登入(EOS)</string> + <string name="epic_offline">離線啟動</string> + <string name="epic_resolve">解決 Epic 登入問題</string> + <string name="store_cloud_saves">雲端存檔</string> + <string name="cloud_upload">上載至雲端</string> + <string name="cloud_download">從雲端下載</string> + <string name="cloud_none">沒有雲端存檔</string> + <string name="cloud_conflict">雲端衝突</string> + <string name="cloud_conflict_files">雲端衝突 · %1$d 個檔案</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">保留雲端版本</string> + <string name="cloud_keep_local">保留本機版本</string> + <string name="cloud_checking">正在檢查雲端…</string> + <string name="cloud_last_sync">上次同步:%1$s</string> + <string name="cloud_never">尚未同步</string> + <string name="cloud_done">%1$d 個檔案</string> + <string name="stores_eyebrow_installed">已安裝</string> + <string name="stores_eyebrow_library">在你的收藏庫中</string> + <string name="stores_eyebrow_store">商店</string> + <string name="achievements_title">成就</string> + <string name="achievements_count">已解鎖 %1$d / %2$d</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">已解鎖 · %1$d</string> + <string name="achievements_group_locked">未解鎖 · %1$d</string> + <string name="achievements_group_hidden">隱藏 · %1$d</string> + <string name="achievements_hidden">隱藏成就</string> + <string name="achievements_hidden_detail">解鎖後會顯示詳情。</string> + <string name="achievements_all">已解鎖全部成就</string> + <string name="achievements_reading">正在讀取…</string> + <string name="achievements_signed_out">登入 %1$s 以查看你的成就。</string> + <string name="achievements_unreachable">無法連接 %1$s。請在連線後再查看。</string> + <string name="achievements_none">%1$s 未列出此遊戲的成就。</string> + <string name="achievements_show_all">顯示全部 %1$d 項</string> + <string name="achievements_dialog_title">成就 · %1$s</string> + <string name="stores_in_steam">✓ 已在 Steam</string> + <string name="stores_uninstall">解除安裝</string> + <string name="stores_uninstalling">正在解除安裝…</string> + <string name="stores_uninstall_title">解除安裝 %1$s?</string> + <string name="stores_uninstall_body_size">將釋放 %1$s,並從 Steam 移除。</string> + <string name="stores_uninstall_body">將移除其檔案及 Steam 捷徑。</string> + <string name="stores_launch_not_registered">Steam 下次啟動時會加入此遊戲;請從 Steam 啟動,或立即啟動 Steam。</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">由 Steam 按遊戲選擇</string> + <string name="stores_card_audio">音訊</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">元件</string> + <string name="stores_card_components_value">FEX、DXVK、VKD3D</string> + <string name="stores_card_engine">引擎</string> + <string name="stores_card_engine_value">%1$s · 原生</string> + <string name="stores_card_engine_java">%1$s · 內置</string> + <string name="stores_how_it_runs">運行方式</string> + <string name="stores_kv_fetched">取自</string> + <string name="stores_kv_fetched_value">%1$s(使用你的帳戶)</string> + <string name="stores_kv_installed_to">安裝位置</string> + <string name="stores_kv_installed_to_value">遊戲儲存位置(Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">啟動方式</string> + <string name="stores_kv_launched_by_value">Steam 及 Proton,與 Steam 遊戲相同</string> + <string name="stores_kv_launched_by_epic">Steam 及 Proton · 每次啟動時登入 Epic,EOS 疊加層關閉</string> + <string name="stores_kv_launched_by_amazon">Steam 及 Proton · 已設定 Amazon Games Services 變數</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">安裝時加入 Steam(Deck 介面 + Steam Input)</string> + <string name="stores_downloads_empty">沒有下載項目</string> + <string name="stores_dl_paused">已暫停</string> + <string name="stores_dl_queued">排隊中</string> + <string name="stores_dl_queued_at">排隊中 · 第 %1$d 位</string> + <string name="stores_dl_cancelled">已取消</string> + <string name="stores_dl_failed">失敗</string> + <string name="stores_dl_failed_reason">失敗 · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">暫停</string> + <string name="stores_dl_resume">繼續</string> + <string name="stores_dl_cancel">取消</string> + <string name="stores_dl_retry">重試</string> + <string name="stores_dl_clear">清除</string> + <string name="stores_dl_clear_all">清除已完成</string> + <string name="stores_stage_manifest">清單</string> + <string name="stores_stage_download">下載</string> + <string name="stores_stage_verify">驗證</string> + <string name="stores_stage_install">安裝</string> + <string name="stores_stage_done">完成</string> + <string name="stores_stage_checking">正在檢查</string> + <string name="stores_stage_downloading">正在下載</string> + <string name="stores_dl_cancel_confirm">刪除下載?</string> + <string name="stores_dl_failed_kept">失敗 · 已保留 %1$s</string> + <string name="stores_stage_verifying">正在驗證</string> + <string name="stores_stage_installing">正在安裝</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d 個檔案</string> + <string name="stores_download_manager">下載管理員</string> + <string name="stores_parallel">同時下載數目</string> + <string name="stores_install_where">安裝至</string> + <string name="stores_install_title">安裝 %1$s:</string> + <string name="stores_target_internal">內部儲存空間</string> + <string name="stores_target_free">可用 %1$s</string> + <string name="stores_notification_channel">商店下載</string> + <string name="stores_notification_channel_hint">顯示遊戲下載進度,並讓下載在背景繼續進行</string> + <string name="stores_notification_one">正在下載</string> + <string name="stores_notification_many">%1$d 項下載</string> + <string name="stores_notification_waiting">正在準備…</string> + <string name="stores_notification_done_channel">已完成的商店下載</string> + <string name="stores_notification_installed">已安裝 %1$s</string> + <string name="stores_notification_failed">%1$s 下載失敗</string> + <string name="stores_notification_delete">刪除</string> + <string name="stores_notification_keep">保留</string> + <string name="stores_login_title">登入 %1$s</string> + <string name="stores_login_provider_title">透過你的帳戶提供者登入</string> + <string name="stores_login_reload">重新載入</string> + <string name="stores_login_close">關閉</string> + <string name="stores_login_retry">再試一次</string> + <string name="stores_login_finishing">正在完成登入…</string> + <string name="stores_login_error_network">無法載入登入頁面:%1$s</string> + <string name="stores_login_error_idp_blocked">此提供者不允許從內嵌頁面登入。請改用商店密碼登入。</string> + <string name="stores_login_error_http">登入頁面回應錯誤(HTTP %1$d)。</string> + <string name="stores_login_error_ssl">與登入頁面的連線不安全。</string> + <string name="stores_login_error_verification">無法驗證登入,請再試一次。</string> + <string name="stores_login_error_generic">登入失敗,請再試一次。</string> <string name="common_close">關閉</string> <string name="common_back">返回</string> <string name="common_edit">編輯</string> @@ -2089,21 +2062,122 @@ <plurals name="games_already_n"> <item quantity="other">%1$d 個已在遊戲中</item> </plurals> - <string name="achievements_title">成就</string> - <string name="achievements_count">已解鎖 %1$d / %2$d</string> - <string name="achievements_count_short">%1$d / %2$d</string> - <string name="achievements_group_unlocked">已解鎖 · %1$d</string> - <string name="achievements_group_locked">未解鎖 · %1$d</string> - <string name="achievements_group_hidden">隱藏 · %1$d</string> - <string name="achievements_hidden">隱藏成就</string> - <string name="achievements_hidden_detail">解鎖後會顯示詳情。</string> - <string name="achievements_all">已解鎖全部成就</string> - <string name="achievements_reading">正在讀取…</string> - <string name="achievements_signed_out">登入 %1$s 以查看你的成就。</string> - <string name="achievements_unreachable">無法連接 %1$s。請在連線後再查看。</string> - <string name="achievements_none">%1$s 未列出此遊戲的成就。</string> - <string name="achievements_show_all">顯示全部 %1$d 項</string> - <string name="achievements_dialog_title">成就 · %1$s</string> + <string name="hop_back">B · 返回%1$s</string> + <string name="hop_from_games">遊戲</string> + <string name="hop_from_play">遊玩設定</string> + <string name="sheet_graphics_drivers">圖形驅動程式</string> + <string name="comp_this_proton">此 Proton</string> + <string name="comp_every_proton">所有 Proton</string> + <string name="comp_for_every_proton">適用於所有 Proton</string> + <string name="comp_available_count">可用 · %1$d</string> + <string name="comp_checked">上次檢查:%1$s</string> + <string name="comp_check_all">檢查新元件及驅動程式</string> + <string name="comp_get">取得</string> + <string name="comp_proton_bar">%1$s · 以下的 DXVK、VKD3D-Proton 和 FEX 屬於此 Proton</string> + <string name="comp_device_tested">%1$s · 已測試</string> + <string name="comp_device_untested">%1$s · 未經測試</string> + <string name="comp_device_unsupported">%1$s · 不支援</string> + <string name="gpu_auto_keeps">讓建議的組合保持最新</string> + <string name="gpu_bar_auto">自動 · %1$s</string> + <string name="gpu_separately">分別設定運行環境及顯示驅動程式</string> + <string name="gpu_use_title">使用 %1$s?</string> + <string name="proton_remove_title">移除 %1$s?</string> + <string name="setup_tab_diagnostics">診斷</string> + <string name="setup_ready">可以開始遊戲</string> + <string name="setup_not_ready">尚未可以開始遊戲</string> + <string name="setup_ready_runtime">運行環境 %1$s</string> + <string name="setup_ready_limit_off">子進程限制已關閉</string> + <string name="setup_ready_signed_in">已登入為 %1$s</string> + <string name="setup_needs_you">需要你處理時</string> + <string name="setup_group_look">外觀</string> + <string name="setup_group_device">此裝置</string> + <string name="setup_group_art">收藏庫圖片</string> + <string name="flathub_eyebrow">Linux 應用程式</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">Linux 桌面的桌面應用程式,可從「桌面」開啟。</string> + <string name="flathub_setup">設定 Flathub</string> + <string name="flathub_remove_title">移除 %1$s?</string> + <string name="flathub_remove_note">其設定和檔案會保留。</string> + <string name="flathub_keep">保留</string> + <string name="desktop_size">佔用空間</string> + <string name="desktop_uninstall_title">解除安裝桌面?</string> + <string name="desktop_all_systems">全部</string> + <string name="gog_achievements">GOG 成就</string> + <string name="gog_achievements_hint">為受支援的 GOG 遊戲同步成就。下次啟動時生效。</string> + <string name="gog_achievements_off">成就已關閉</string> + <string name="gog_achievements_on">成就已開啟</string> + <string name="gog_achievements_signin">重新登入 GOG</string> + <string name="gog_achievements_uploaded">成就已上載</string> + <string name="gog_achievements_pending">成就上載待處理</string> + <string name="gog_achievements_enable_store">在「商店」設定中開啟 GOG 成就</string> + <string name="gog_achievements_unavailable">成就不可用;請查看工作階段日誌</string> + <string name="achievement_unlocked">成就已解鎖</string> + <string name="touch_paste">貼上</string> + <string name="touch_dpad">方向鍵</string> + <string name="touch_left_stick">左搖桿</string> + <string name="touch_right_stick">右搖桿</string> + <string name="touch_menu">選單</string> + <string name="touch_keyboard">鍵盤</string> + <string name="touch_dpad_up">方向鍵上</string> + <string name="touch_dpad_down">方向鍵下</string> + <string name="touch_dpad_left">方向鍵左</string> + <string name="touch_dpad_right">方向鍵右</string> + <string name="touch_title">觸控操作</string> + <string name="touch_controller">觸控控制器</string> + <string name="touch_mouse">滑鼠</string> + <string name="touch_on">開</string> + <string name="touch_off">關</string> + <string name="touch_speed_slow">慢</string> + <string name="touch_speed_normal">正常</string> + <string name="touch_speed_fast">快</string> + <string name="touch_edit_layout">編輯佈局</string> + <string name="touch_droiddeck_menu">DroidDeck 選單</string> + <string name="touch_drag_hint">將控制項拖曳到畫面上</string> + <string name="touch_arrange">排列</string> + <string name="touch_discard">放棄</string> + <string name="touch_saving">儲存中…</string> + <string name="touch_icon">圖示</string> + <string name="touch_narrower">闊−</string> + <string name="touch_wider">闊+</string> + <string name="touch_shorter">高−</string> + <string name="touch_taller">高+</string> + <string name="touch_hide">隱藏</string> + <string name="touch_color_opacity">顏色及不透明度…</string> + <string name="touch_copy_layout">複製此佈局</string> + <string name="touch_layout_copied">佈局已複製,可貼到任何遊戲的觸控佈局中</string> + <string name="touch_paste_layout">貼上已複製的佈局</string> + <string name="touch_reset_layout">還原為 Steam 的預設佈局</string> + <string name="touch_no_binding">此控制項在 Steam 中未綁定操作,無法設定圖示</string> + <string name="touch_icon_for">%1$s 的圖示</string> + <string name="touch_layout_color">觸控佈局顏色</string> + <string name="touch_done">完成</string> + <string name="touch_save_failed">無法儲存觸控佈局</string> + <string name="touch_saved">觸控佈局已儲存至 Steam</string> + <string name="touch_app_id">應用程式 %1$d</string> + <string name="touch_label_hint">標籤(選用)</string> + <string name="touch_icons_missing">找不到 Steam 的圖示</string> + <plurals name="touch_icons_found"> + <item quantity="other">來自 Steam 的 %1$d 個圖示</item> + </plurals> + <string name="touch_icon_color">圖示顏色</string> + <string name="touch_button_color">按鈕顏色</string> + <string name="touch_no_icon">沒有圖示</string> + + <string name="stores_sync_sign_in">正在檢查登入狀態…</string> + <string name="stores_sync_library">正在取得你的遊戲庫…</string> + <string name="stores_sync_game_list">正在取得遊戲清單…</string> + <string name="stores_sync_details">正在載入遊戲詳情…(%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="other">正在同步 %1$d 個遊戲…</item> + </plurals> + <string name="stores_sync_progress">正在同步… %1$d/%2$d</string> + <string name="stores_sync_cover_art">正在取得封面圖片…</string> + <string name="stores_sync_failed">同步失敗</string> + <string name="stores_sync_no_games">在你的 %1$s 遊戲庫中找不到遊戲</string> + <string name="stores_sync_amazon_no_answer">Amazon Games 未回應你的遊戲庫要求:請重新整理,或重新登入</string> + <string name="stores_sync_gog_unreachable">無法連線至 GOG</string> + <string name="stores_sync_gog_unreadable">無法讀取你的 GOG 遊戲庫</string> + <string name="stores_registering">正在向 Steam 註冊…</string> <string name="drawer_page_glasses">眼鏡</string> <string name="glasses_open">VITURE 眼鏡</string> <string name="glasses_control">眼鏡控制</string> @@ -2152,4 +2226,34 @@ <string name="stores_hltb_completionist">全收集</string> <string name="stores_hltb_hours">%1$s 小時</string> <string name="stores_hltb_minutes">%1$d 分鐘</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Cardboard 模式</string> + <string name="cardboard_mode_hint">為手機 VR 眼鏡提供左右眼畫面。</string> + <string name="cardboard_source">畫面</string> + <string name="cardboard_source_screen">虛擬螢幕</string> + <string name="cardboard_source_sbs">左右並排 3D</string> + <string name="cardboard_viewer">眼鏡</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard(2014)</string> + <string name="cardboard_viewer_none">無鏡片</string> + <string name="cardboard_ipd">瞳距</string> + <string name="cardboard_screen_width">螢幕闊度</string> + <string name="cardboard_screen_distance">螢幕距離</string> + <string name="cardboard_head_tracking">頭部追蹤</string> + <string name="cardboard_no_sensor">此裝置沒有旋轉感應器。</string> + <string name="cardboard_recenter">重設視角</string> + <string name="cardboard_recenter_button">重設</string> + <string name="cardboard_steamvr">SteamVR(實驗性)</string> + <string name="cardboard_steamvr_install">安裝</string> + <string name="cardboard_steamvr_install_hint">下載 Valve 的 ARM64 版 SteamVR(571 MB)。</string> + <string name="cardboard_steamvr_installing">正在下載並解壓 SteamVR…</string> + <string name="cardboard_steamvr_start">啟動</string> + <string name="cardboard_steamvr_ready_hint">以你的頭部作為頭戴裝置執行 SteamVR;畫面會切換為左右並排 3D。</string> + <string name="cardboard_steamvr_stop">停止</string> + <string name="cardboard_steamvr_running_hint">SteamVR 正在執行。</string> + <string name="cardboard_steamvr_unavailable">此工作階段中無法使用。</string> + <string name="cardboard_steamvr_failed">SteamVR 沒有回應;請查看工作階段日誌。</string> + <string name="drawer_component_downloads">下載元件</string> + <string name="drawer_component_downloads_hint">下載內容與「元件」頁面共用。在上方選擇已下載的版本,即可在下次啟動遊戲時使用。</string> + <string name="drawer_component_choose_download">選擇要下載的版本</string> </resources> diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 97d899754..f4ecbaabf 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -15,8 +15,10 @@ <string name="session_channel">工作階段</string> <string name="session_channel_description">在 Steam 或桌面工作階段開啟時顯示</string> <string name="session_notification">Steam 正在執行</string> + <string name="session_notification_downloads">Steam 正在下載</string> <string name="session_notification_paused">工作階段已暫停 - 準備好後點擊繼續</string> <string name="resume_session">繼續</string> + <string name="suspend_now">立即暫停</string> <string name="stop_session">結束此工作階段</string> <string name="frame_gen_title">影格生成</string> <string name="frame_gen_hint">請選擇你要使用的影格生成模式</string> @@ -182,6 +184,8 @@ <string name="add_app_flatpak_id">應用程式 ID</string> <string name="add_app_flatpak_placeholder">org.ppsspp.PPSSPP</string> <string name="add_app_flatpak_invalid">不是有效的 Flathub 應用程式 ID</string> + <string name="add_app_flatpak_bundle_hint">或是 .flatpak 檔案,適用於 Flathub 上沒有的應用程式。</string> + <string name="add_app_pick_flatpak">選擇 .flatpak 檔案</string> <string name="add_app_source">來源</string> <string name="add_app_appearance">顯示在桌面</string> <string name="add_app_preview_name">新應用程式</string> @@ -251,6 +255,10 @@ <string name="mode_osc">螢幕控制</string> <string name="mode_osc_qam">Steam + QAM</string> <string name="mode_osc_note">自動模式會在未連接控制器時顯示螢幕控制。</string> + <string name="osc_steam_touch">Steam 觸控(測試版)</string> + <string name="osc_style_next_session">Steam 觸控控制會在下次 Steam 工作階段啟用</string> + <string name="osc_steam_layout">Steam 觸控版面配置</string> + <string name="osc_steam_layout_hint">移動、調整大小或隱藏這款遊戲的觸控控制;會儲存到 Steam</string> <string name="mode_controller">控制器</string> <string name="mode_controller_hint">Steam 辨識你的控制器的方式。下次工作階段生效。</string> <string name="mode_controller_deck">Steam Deck 控制器</string> @@ -268,6 +276,8 @@ <string name="mode_suspend_manual">手動</string> <string name="mode_suspend_native">原生</string> <string name="mode_suspend_steam_note">「自動」會暫停,並在重新顯示時繼續。「原生」會先讓 Steam 做好睡眠準備。「手動」會等你點擊「繼續」。「從不」則會持續執行。</string> + <string name="mode_background_downloads">讓下載持續進行</string> + <string name="mode_background_downloads_hint">Steam 正在下載時,延後在背景暫停。</string> <string name="mode_suspend_note">「自動」會在背景時暫停,並在重新顯示時繼續。「手動」會等你點擊「繼續」。「從不」則會持續執行。</string> <string name="mode_startup">啟動</string> <string name="mode_steam_startup">隨 DroidDeck 啟動 Steam</string> @@ -331,6 +341,7 @@ <string name="wincomp_found_shipped">遊戲含有 %1$s(通常可直接使用)</string> <string name="wincomp_already_here">這款遊戲中已有</string> <string name="wincomp_no_recommendation">遊戲資料夾中沒有指向任何元件的內容。</string> + <string name="wincomp_active">已為這款遊戲安裝(%1$d)</string> <string name="wincomp_all">所有元件(%1$d)</string> <string name="wincomp_installers">需要 Windows 安裝程式(%1$d)</string> <string name="wincomp_installers_note">這些元件需要在前綴中執行真正的安裝程式(.NET、PhysX、XNA、完整的 Visual C++ 安裝程式等)。DroidDeck 目前還做不到,接下來就會支援。</string> @@ -341,6 +352,9 @@ <string name="wincomp_loading">正在載入元件…</string> <string name="wincomp_failed">無法安裝:%1$s</string> <string name="wincomp_downloaded">已下載</string> + <string name="wincomp_downloading">正在下載 %1$s</string> + <string name="wincomp_installing">正在安裝:%1$s</string> + <string name="wincomp_reverting">正在關閉…</string> <string name="wincomp_download_size">下載 %1$s</string> <string name="wincomp_next_launch">將在下次啟動遊戲時生效。</string> <string name="mode_stretch169">拉伸 16:9 遊戲</string> @@ -397,6 +411,44 @@ <string name="comp_check_nightlies">檢查 Nightlies 中的新套件</string> <string name="comp_proton">Proton</string> <string name="comp_proton_note">這裡選擇的 Proton 會執行所有未在 Steam 中自選 Proton 的遊戲,在 Steam 中所做的變更也會顯示在這裡。Steam 更新時會取代 Valve 的 Proton;每個組建版本的原始檔案都會保留。</string> + <string name="comp_scope">設定</string> + <string name="comp_profile_configure">調整設定檔</string> + <string name="comp_profile">設定檔</string> + <string name="comp_scope_general">預設設定檔</string> + <string name="comp_scope_general_detail">沒有覆寫設定的遊戲會自動使用。</string> + <string name="comp_profile_game_detail">%1$s 啟動時自動使用的已儲存設定。</string> + <string name="comp_game_hint">只選擇這款遊戲需要的設定,其餘設定會繼續使用預設設定檔。</string> + <string name="comp_game_fex">FEX 預設集</string> + <string name="comp_game_fex_inherited">使用預設設定檔的預設集。</string> + <string name="comp_game_fex_override">已為這款遊戲儲存自訂預設集。</string> + <string name="comp_game_fex_custom">自訂 FEX 變數</string> + <string name="comp_game_use_general">使用預設</string> + <string name="comp_game_compatibility">遊戲相容性</string> + <string name="comp_game_environment_hint">這款遊戲的 Proton、DXVK、VKD3D 與同步環境選項。</string> + <string name="comp_game_proton_inherited">使用預設設定檔的 Proton。</string> + <string name="comp_game_proton_override">已透過 Steam 相容性對應為這款遊戲儲存自訂 Proton。</string> + <string name="comp_no_proton_short">無 Proton</string> + <string name="comp_game_dxvk">DXVK</string> + <string name="comp_game_vkd3d">VKD3D-Proton</string> + <string name="comp_game_component_runtime">使用預設設定檔中來自 %1$s 的套件,除非這款遊戲另選其他套件。</string> + <string name="comp_game_component_general">使用預設設定檔的元件套件。</string> + <string name="comp_game_component_override">僅這款遊戲使用 %1$s。</string> + <string name="comp_game_runtime_options">執行環境選項</string> + <string name="comp_game_environment_other_hint">其他個別遊戲的 Proton、DXVK、VKD3D-Proton 與同步環境選項。</string> + <string name="comp_game_saved_next">已儲存 %1$s 的設定檔,下次啟動遊戲時生效。</string> + <string name="game_no_steam_title">不啟動 Steam 直接開始</string> + <string name="game_no_steam_detail">適用於一開就立即關閉的遊戲:不經過 Proton 的 Steam 設定來啟動。疊加層等 Steam 功能對這款遊戲會關閉。</string> + <string name="game_no_steam_on">開</string> + <string name="comp_game_proton_live">已儲存 %1$s 的 Proton 選擇,現在即可在 Steam 中使用。</string> + <string name="comp_game_proton_next">已儲存 %1$s 的 Proton 選擇。啟動遊戲前請重新啟動 Steam。</string> + <string name="comp_game_proton_not_runnable">%1$s 無法在這部裝置上使用所選的 Proton。</string> + <string name="comp_game_load_failed">無法載入 %1$s 的設定檔:%2$s</string> + <string name="comp_game_save_failed">無法儲存遊戲設定檔:%1$s</string> + <string name="comp_game_reset">重設遊戲設定檔</string> + <string name="comp_game_reset_title">要重設 %1$s 的設定檔嗎?</string> + <string name="comp_game_reset_body">要移除這款遊戲的 Proton、FEX、元件套件、環境、材質過濾和 Windows 元件覆寫嗎?遊戲將改用預設設定檔。</string> + <string name="comp_game_reset_done">%1$s 現在使用預設設定檔。</string> + <string name="comp_game_reset_failed">無法完全重設遊戲設定檔:%1$s</string> <string name="comp_default_live">%1$s 現在會執行所有未在 Steam 中自選 Proton 的遊戲。</string> <string name="comp_default_next">從下次啟動 Steam 起,%1$s 會執行所有未自選 Proton 的遊戲。</string> <string name="comp_default_not_runnable">%1$s 無法在此啟動遊戲,因此不能設為預設。</string> @@ -560,6 +612,18 @@ <string name="drawer_stop_title">停止工作階段?</string> <string name="drawer_running_now">目前正在執行</string> <string name="drawer_no_game">目前沒有使用 Proton 執行的遊戲</string> + <string name="drawer_selected_profile">遊戲設定檔</string> + <string name="drawer_selected_profile_none">尚無法取得設定檔詳細資料。</string> + <string name="drawer_profile_refresh_failed_initial">無法載入遊戲設定檔,正在自動重試。</string> + <string name="drawer_profile_refresh_failed_stale">無法重新整理遊戲設定檔。目前顯示最後取得的資料,正在自動重試。</string> + <string name="drawer_profile_inspect_hint">檢視並編輯下次啟動時使用的已儲存設定。</string> + <string name="drawer_profile_default">使用預設設定檔</string> + <string name="drawer_profile_custom">自訂設定檔</string> + <string name="drawer_component_inherited">使用預設設定檔中的 %1$s。</string> + <string name="drawer_game_components_note">變更會儲存到 %1$s 的設定檔,並在下次啟動遊戲時生效。</string> + <plurals name="drawer_profile_environment_overrides"> + <item quantity="other">%1$d 項遊戲覆寫</item> + </plurals> <string name="drawer_no_proton">尚未安裝 Proton。</string> <string name="drawer_game_running">遊戲執行中</string> <string name="drawer_reapplied">已在啟動 %1$s 時重新套用</string> @@ -616,6 +680,14 @@ <string name="setup_saved_logs">已儲存的工作階段日誌</string> <string name="setup_saved_logs_hint">保留最新的 %1$d 個</string> <string name="setup_clear_logs">清除日誌</string> + <string name="setup_reset_settings">重設所有設定</string> + <string name="setup_reset_settings_hint">將所有設定還原為預設值</string> + <string name="setup_reset_settings_button">重設</string> + <string name="setup_reset_settings_title">要重設所有設定嗎?</string> + <string name="setup_reset_settings_body">顯示、效能、控制器、佈景主題、語言和啟動器設定將還原為預設值。你的遊戲、Steam 登入、商店帳戶、已下載的驅動程式和儲存資料夾都會保留。</string> + <string name="setup_reset_settings_profiles">同時重設遊戲設定檔(每個遊戲的 Proton、FEX、元件和環境,以及預設設定檔的環境)</string> + <string name="setup_reset_settings_session_running">請先停止工作階段再重設設定。</string> + <string name="setup_reset_settings_partial">設定已重設,但部分項目無法重設。詳細資訊請查看日誌。</string> <string name="setup_offline">離線模式</string> <string name="setup_sign_in_first">請先登入 Steam</string> <string name="setup_launcher">啟動器</string> @@ -631,6 +703,10 @@ <string name="setup_home">用作主螢幕</string> <string name="setup_home_on">DroidDeck 可以作為主螢幕應用程式</string> <string name="setup_home_off">DroidDeck 不會作為主螢幕應用程式提供</string> + <string name="setup_orientation">螢幕方向</string> + <string name="setup_orientation_auto">自動橫向</string> + <string name="setup_orientation_landscape">橫向</string> + <string name="setup_orientation_reverse">反向橫向</string> <string name="setup_fullscreen">全螢幕</string> <string name="setup_fullscreen_on">隱藏 Android 狀態列和導覽列</string> <string name="setup_fullscreen_off">顯示 Android 狀態列和導覽列</string> @@ -888,6 +964,12 @@ <string name="content_page_gone">該頁面已不存在。請從左側選擇一個區段。</string> <string name="content_open_desktop">開啟桌面</string> <string name="content_install_open_desktop">安裝並開啟桌面</string> + <string name="desktop_remove_title">解除安裝桌面</string> + <string name="desktop_remove_text">移除桌面及其內建的應用程式。遊戲、已新增的應用程式、設定和共用程式庫都會保留。</string> + <string name="desktop_removing">正在移除桌面</string> + <string name="desktop_removed">已移除桌面</string> + <string name="desktop_cleaning_legacy">正在移除舊的 LXQt 檔案</string> + <string name="desktop_removal_unknown">請先更新 DroidDeck,再移除此桌面版本。</string> <string name="content_linux_desktop">Linux 桌面</string> <string name="content_linux_desktop_hint">KDE Plasma、Firefox 和你的模擬器</string> <string name="games_empty">在 Steam 中安裝的遊戲會在工作階段結束後顯示在這裡。</string> @@ -917,6 +999,8 @@ <string name="games_last_played">上次執行 %1$s</string> <string name="games_internal">內部儲存空間</string> <string name="games_added">已新增的遊戲</string> + <string name="games_custom">自訂遊戲</string> + <string name="games_store_storage">遊戲儲存空間</string> <string name="gpu_auto_hint">為此 GPU 選擇最佳驅動程式組合,並保持最新</string> <string name="gpu_manual_hint">自行選擇驅動程式組合</string> <string name="gpu_checking">正在檢查驅動程式…</string> @@ -958,6 +1042,12 @@ <string name="deck_tab_left">左側觸控板</string> <string name="deck_tab_right">右側觸控板</string> <string name="deck_tab_both">兩側</string> + <string name="deck_tab_gamepad">手把</string> + <string name="deck_surface_stick">搖桿</string> + <string name="deck_surface_trackpad">觸控板</string> + <string name="deck_surface_switch">在搖桿與觸控板之間切換</string> + <string name="deck_more_controls">更多 Deck 控制</string> + <string name="deck_panels">面板</string> <string name="deck_close">關閉第二螢幕控制</string> <string name="deck_open_steam_menu">開啟 Steam 選單</string> <string name="deck_left_upper">左側 · 上方</string> @@ -993,6 +1083,10 @@ <string name="proton_installed">已安裝 %1$s</string> <string name="proton_queued">上一次要求留下的待處理項</string> <string name="proton_not_installed">未安裝</string> + <string name="proton_update_available">有可用更新 → %1$s</string> + <string name="proton_remove_older">移除舊版 %1$s</string> + <string name="proton_remove_older_body">將移除 %1$s。設定為使用這些版本的遊戲必須改用其他 Proton。</string> + <string name="proton_remove_body">設定為使用此版本的遊戲必須改用其他 Proton。</string> <string name="proton_install_now">立即安裝</string> <string name="proton_cancel_queued">取消排隊</string> <string name="pckb_android">Android 鍵盤</string> @@ -1021,6 +1115,8 @@ <string name="game_env_eyebrow">Proton 遊戲</string> <string name="pip_title">子母畫面</string> <string name="pip_auto">自動進入子母畫面</string> + <string name="unfolded_controls">展開螢幕時的控制</string> + <string name="unfolded_controls_hint">展開時,遊戲會顯示在 Steam Deck 觸控板和背面握把上方。摺疊時,遊戲以全螢幕顯示。</string> <string name="pip_suspended">已暫停</string> <string name="pip_not_ready">工作階段準備就緒後即可使用子母畫面。</string> <string name="pip_permission">請在 Android 設定中允許 DroidDeck 使用子母畫面。</string> @@ -1046,19 +1142,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> @@ -1066,19 +1155,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> @@ -1091,78 +1172,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> @@ -1170,19 +1228,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> @@ -1197,11 +1249,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> @@ -1214,39 +1262,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> @@ -1261,33 +1295,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> @@ -1300,28 +1321,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> @@ -1330,8 +1337,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> @@ -1349,27 +1354,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> @@ -1387,8 +1382,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> @@ -1396,8 +1389,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> @@ -1411,19 +1402,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> @@ -1431,8 +1414,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> @@ -1443,20 +1424,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> @@ -1470,8 +1445,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> @@ -1486,51 +1459,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> @@ -1539,8 +1492,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> @@ -1551,34 +1502,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> @@ -1593,75 +1531,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> @@ -1697,27 +1607,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> @@ -1725,24 +1621,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> @@ -1756,31 +1642,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> @@ -1788,19 +1665,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> @@ -1817,55 +1686,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> @@ -1890,29 +1742,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> @@ -1923,115 +1769,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> @@ -2049,6 +1841,7 @@ <string name="display_frame_rate">影格率</string> <string name="display_look_filter">%1$s 將縮放設為 %2$s。</string> <string name="display_match_screen">符合螢幕(%1$d×%2$d)</string> + <string name="display_follow_screen">跟隨螢幕(摺疊或展開時調整大小)</string> <string name="mode_tab_steam">Steam</string> <string name="device_info_show">顯示裝置詳細資料</string> <string name="device_info_hide">隱藏裝置詳細資料</string> @@ -2058,6 +1851,186 @@ <string name="device_info_copied">已複製。</string> <string name="force_ssbs_title">強制 SSBS</string> <string name="force_ssbs_hint">讓一項 Wine 會不斷關閉的 CPU 速度設定保持開啟。在 Snapdragon 8 Elite 上可能讓 Windows 遊戲更快;在較舊的晶片上變化不大。下次啟動遊戲時生效。</string> + + <string name="rail_stores">遊戲商店</string> + <string name="source_steam">Steam</string> + <string name="source_added">自訂</string> + <string name="setup_stores">遊戲商店</string> + <string name="setup_stores_open_on">開啟商店時顯示</string> + <string name="setup_stores_speed">下載速度等級</string> + <string name="setup_stores_engine">商店引擎</string> + <string name="setup_stores_engine_chip_ready">● 引擎已就緒</string> + <string name="setup_stores_engine_chip_missing">尚未組建</string> + <string name="stores_tier_balanced">平衡</string> + <string name="stores_tier_fast">快速</string> + <string name="stores_tier_max">最高</string> + <string name="stores_title">遊戲商店</string> + <string name="stores_settings">遊戲商店設定</string> + <string name="stores_settings_eyebrow">設定</string> + <string name="stores_chip_signed_out">(未登入)</string> + <string name="stores_tab_store">商店</string> + <string name="stores_tab_installed">已安裝</string> + <string name="stores_tab_library">收藏庫</string> + <string name="stores_tab_all">全部</string> + <string name="stores_tab_downloads">下載</string> + <string name="stores_tab_count">%1$s(%2$d)</string> + <string name="stores_signin">登入</string> + <string name="stores_signout_named">%1$s · 登出</string> + <string name="stores_signout_title">要登出 %1$s 嗎?</string> + <string name="stores_signout_note">已安裝的遊戲會保留。重新登入時會再次取得收藏庫。</string> + <string name="stores_signout_stay">保持登入</string> + <string name="stores_signout_go">登出</string> + <string name="stores_search_catalog">搜尋 %1$s 目錄…</string> + <string name="stores_search_installed">搜尋已安裝的遊戲…</string> + <string name="stores_search_library">搜尋你的收藏庫…</string> + <string name="stores_search_open">搜尋</string> + <string name="stores_installed_empty">尚未安裝任何內容</string> + <string name="stores_library_loading">正在載入</string> + <string name="stores_nothing_matches">沒有遊戲</string> + <string name="stores_shelves_loading">正在載入商店…</string> + <string name="stores_amazon_no_catalog">沒有目錄</string> + <string name="stores_shelf_new">最新上架</string> + <string name="stores_shelf_deals">特惠</string> + <string name="stores_shelf_free">免費遊戲</string> + <string name="stores_shelf_trending">熱門趨勢</string> + <string name="stores_shelf_library">你的收藏庫</string> + <string name="stores_play">開始遊戲</string> + <string name="stores_install">安裝</string> + <string name="stores_resume_install">繼續安裝</string> + <string name="stores_dl_interrupted">已中斷</string> + <string name="stores_install_size">安裝 · %1$s</string> + <string name="stores_get_free">免費取得</string> + <string name="stores_view_on">在 %1$s 檢視</string> + <string name="stores_buy_price">購買 · %1$s</string> + <string name="stores_installed_chip">已安裝</string> + <string name="stores_in_library">已在收藏庫中</string> + <string name="stores_free">免費</string> + <string name="stores_resume">繼續</string> + <string name="stores_refresh">重新整理</string> + <string name="stores_show_mature">顯示成人內容</string> + <string name="epic_card_title">Epic</string> + <string name="epic_card_eos">EOS</string> + <string name="epic_card_offline">離線</string> + <string name="epic_card_none">關閉</string> + <string name="epic_eos">Epic 登入(EOS)</string> + <string name="epic_offline">離線啟動</string> + <string name="epic_resolve">解決 Epic 登入問題</string> + <string name="store_cloud_saves">雲端存檔</string> + <string name="cloud_upload">上傳到雲端</string> + <string name="cloud_download">從雲端下載</string> + <string name="cloud_none">沒有雲端存檔</string> + <string name="cloud_conflict">雲端衝突</string> + <string name="cloud_conflict_files">雲端衝突 · %1$d 個檔案</string> + <string name="cloud_pending">…</string> + <string name="cloud_keep_cloud">保留雲端版本</string> + <string name="cloud_keep_local">保留本機版本</string> + <string name="cloud_checking">正在檢查雲端…</string> + <string name="cloud_last_sync">上次同步:%1$s</string> + <string name="cloud_never">尚未同步</string> + <string name="cloud_done">%1$d 個檔案</string> + <string name="stores_eyebrow_installed">已安裝</string> + <string name="stores_eyebrow_library">在你的收藏庫中</string> + <string name="stores_eyebrow_store">商店</string> + <string name="achievements_title">成就</string> + <string name="achievements_count">已解鎖 %1$d / %2$d</string> + <string name="achievements_count_short">%1$d / %2$d</string> + <string name="achievements_group_unlocked">已解鎖 · %1$d</string> + <string name="achievements_group_locked">未解鎖 · %1$d</string> + <string name="achievements_group_hidden">隱藏 · %1$d</string> + <string name="achievements_hidden">隱藏成就</string> + <string name="achievements_hidden_detail">解鎖後會顯示詳細資訊。</string> + <string name="achievements_all">已解鎖所有成就</string> + <string name="achievements_reading">正在讀取…</string> + <string name="achievements_signed_out">登入 %1$s 以查看你的成就。</string> + <string name="achievements_unreachable">無法連線到 %1$s。請在連上網路後再查看。</string> + <string name="achievements_none">%1$s 沒有列出這款遊戲的成就。</string> + <string name="achievements_show_all">顯示全部 %1$d 項</string> + <string name="achievements_dialog_title">成就 · %1$s</string> + <string name="stores_in_steam">✓ 已在 Steam 中</string> + <string name="stores_uninstall">解除安裝</string> + <string name="stores_uninstalling">正在解除安裝…</string> + <string name="stores_uninstall_title">要解除安裝 %1$s 嗎?</string> + <string name="stores_uninstall_body_size">將釋放 %1$s,並從 Steam 中移除。</string> + <string name="stores_uninstall_body">將移除其檔案和 Steam 捷徑。</string> + <string name="stores_launch_not_registered">Steam 下次啟動時會加入這款遊戲;請從 Steam 啟動,或立即啟動 Steam。</string> + <string name="stores_card_proton">Proton</string> + <string name="stores_card_proton_value">由 Steam 依遊戲選擇</string> + <string name="stores_card_audio">音訊</string> + <string name="stores_card_audio_value">DirectAudio</string> + <string name="stores_card_components">元件</string> + <string name="stores_card_components_value">FEX、DXVK、VKD3D</string> + <string name="stores_card_engine">引擎</string> + <string name="stores_card_engine_value">%1$s · 原生</string> + <string name="stores_card_engine_java">%1$s · 內建</string> + <string name="stores_how_it_runs">執行方式</string> + <string name="stores_kv_fetched">取得來源</string> + <string name="stores_kv_fetched_value">%1$s,使用你的帳號</string> + <string name="stores_kv_installed_to">安裝位置</string> + <string name="stores_kv_installed_to_value">遊戲儲存空間(Games/%1$s/%2$s)</string> + <string name="stores_kv_launched_by">啟動方式</string> + <string name="stores_kv_launched_by_value">透過 Steam 和 Proton,與 Steam 遊戲的方式相同</string> + <string name="stores_kv_launched_by_epic">Steam 和 Proton · 每次啟動時登入 Epic,EOS 重疊層關閉</string> + <string name="stores_kv_launched_by_amazon">Steam 和 Proton · 已設定 Amazon Games Services 變數</string> + <string name="stores_kv_steam">Steam</string> + <string name="stores_kv_steam_on">安裝時加入 Steam(Deck 介面 + Steam Input)</string> + <string name="stores_downloads_empty">沒有下載項目</string> + <string name="stores_dl_paused">已暫停</string> + <string name="stores_dl_queued">排隊中</string> + <string name="stores_dl_queued_at">排隊中 · #%1$d</string> + <string name="stores_dl_cancelled">已取消</string> + <string name="stores_dl_failed">失敗</string> + <string name="stores_dl_failed_reason">失敗 · %1$s</string> + <string name="stores_dl_stage_percent">%1$s · %2$d%%</string> + <string name="stores_dl_bytes">%1$s / %2$s · %3$d%%</string> + <string name="stores_dl_pause">暫停</string> + <string name="stores_dl_resume">繼續</string> + <string name="stores_dl_cancel">取消</string> + <string name="stores_dl_retry">重試</string> + <string name="stores_dl_clear">清除</string> + <string name="stores_dl_clear_all">清除已完成項目</string> + <string name="stores_stage_manifest">資訊清單</string> + <string name="stores_stage_download">下載</string> + <string name="stores_stage_verify">驗證</string> + <string name="stores_stage_install">安裝</string> + <string name="stores_stage_done">完成</string> + <string name="stores_stage_checking">正在檢查</string> + <string name="stores_stage_downloading">正在下載</string> + <string name="stores_dl_cancel_confirm">要刪除下載內容嗎?</string> + <string name="stores_dl_failed_kept">失敗 · 已保留 %1$s</string> + <string name="stores_stage_verifying">正在驗證</string> + <string name="stores_stage_installing">正在安裝</string> + <string name="stores_dl_active_percent">%1$s %2$d%%</string> + <string name="stores_dl_with">%1$s · %2$s</string> + <string name="stores_dl_amount">%1$s / %2$s</string> + <string name="stores_dl_items">%1$d/%2$d 個檔案</string> + <string name="stores_download_manager">下載管理員</string> + <string name="stores_parallel">同時下載數</string> + <string name="stores_install_where">安裝到</string> + <string name="stores_install_title">安裝 %1$s:</string> + <string name="stores_target_internal">內部</string> + <string name="stores_target_free">可用 %1$s</string> + <string name="stores_notification_channel">商店下載</string> + <string name="stores_notification_channel_hint">顯示遊戲下載進度,並讓下載在背景持續進行</string> + <string name="stores_notification_one">正在下載</string> + <string name="stores_notification_many">%1$d 個下載項目</string> + <string name="stores_notification_waiting">正在準備…</string> + <string name="stores_notification_done_channel">已完成的商店下載</string> + <string name="stores_notification_installed">已安裝 %1$s</string> + <string name="stores_notification_failed">%1$s 下載失敗</string> + <string name="stores_notification_delete">刪除</string> + <string name="stores_notification_keep">保留</string> + <string name="stores_login_title">登入 %1$s</string> + <string name="stores_login_provider_title">透過你的帳號提供者登入</string> + <string name="stores_login_reload">重新載入</string> + <string name="stores_login_close">關閉</string> + <string name="stores_login_retry">再試一次</string> + <string name="stores_login_finishing">正在完成登入…</string> + <string name="stores_login_error_network">無法載入登入頁面:%1$s</string> + <string name="stores_login_error_idp_blocked">此提供者拒絕從內嵌頁面登入。請改用商店密碼登入。</string> + <string name="stores_login_error_http">登入頁面回應錯誤(HTTP %1$d)。</string> + <string name="stores_login_error_ssl">與登入頁面的連線不安全。</string> + <string name="stores_login_error_verification">無法驗證登入。請再試一次。</string> + <string name="stores_login_error_generic">登入失敗。請再試一次。</string> <string name="common_close">關閉</string> <string name="common_back">返回</string> <string name="common_edit">編輯</string> @@ -2089,21 +2062,122 @@ <plurals name="games_already_n"> <item quantity="other">%1$d 個已在遊戲中</item> </plurals> - <string name="achievements_title">成就</string> - <string name="achievements_count">已解鎖 %1$d / %2$d</string> - <string name="achievements_count_short">%1$d / %2$d</string> - <string name="achievements_group_unlocked">已解鎖 · %1$d</string> - <string name="achievements_group_locked">未解鎖 · %1$d</string> - <string name="achievements_group_hidden">隱藏 · %1$d</string> - <string name="achievements_hidden">隱藏成就</string> - <string name="achievements_hidden_detail">解鎖後會顯示詳細資訊。</string> - <string name="achievements_all">已解鎖所有成就</string> - <string name="achievements_reading">正在讀取…</string> - <string name="achievements_signed_out">登入 %1$s 以查看你的成就。</string> - <string name="achievements_unreachable">無法連線到 %1$s。請在連上網路後再查看。</string> - <string name="achievements_none">%1$s 沒有列出這款遊戲的成就。</string> - <string name="achievements_show_all">顯示全部 %1$d 項</string> - <string name="achievements_dialog_title">成就 · %1$s</string> + <string name="hop_back">B · 返回%1$s</string> + <string name="hop_from_games">遊戲</string> + <string name="hop_from_play">執行設定</string> + <string name="sheet_graphics_drivers">圖形驅動程式</string> + <string name="comp_this_proton">此 Proton</string> + <string name="comp_every_proton">所有 Proton</string> + <string name="comp_for_every_proton">適用於所有 Proton</string> + <string name="comp_available_count">可用 · %1$d</string> + <string name="comp_checked">已檢查 %1$s</string> + <string name="comp_check_all">檢查新元件和驅動程式</string> + <string name="comp_get">取得</string> + <string name="comp_proton_bar">%1$s · 下方的 DXVK、VKD3D-Proton 和 FEX 屬於此 Proton</string> + <string name="comp_device_tested">%1$s · 已測試</string> + <string name="comp_device_untested">%1$s · 未測試</string> + <string name="comp_device_unsupported">%1$s · 不支援</string> + <string name="gpu_auto_keeps">讓建議的驅動程式組合保持最新</string> + <string name="gpu_bar_auto">自動 · %1$s</string> + <string name="gpu_separately">分別設定執行環境驅動程式和顯示驅動程式</string> + <string name="gpu_use_title">要使用 %1$s 嗎?</string> + <string name="proton_remove_title">要移除 %1$s 嗎?</string> + <string name="setup_tab_diagnostics">診斷</string> + <string name="setup_ready">已可開始遊戲</string> + <string name="setup_not_ready">尚未準備好開始遊戲</string> + <string name="setup_ready_runtime">執行環境 %1$s</string> + <string name="setup_ready_limit_off">處理程序限制已關閉</string> + <string name="setup_ready_signed_in">已登入為 %1$s</string> + <string name="setup_needs_you">需要你處理的項目</string> + <string name="setup_group_look">外觀</string> + <string name="setup_group_device">這部裝置</string> + <string name="setup_group_art">收藏庫圖片</string> + <string name="flathub_eyebrow">LINUX 應用程式</string> + <string name="flathub_name">Flathub</string> + <string name="flathub_line">適用於 Linux 桌面的應用程式,從「桌面」開啟。</string> + <string name="flathub_setup">設定 Flathub</string> + <string name="flathub_remove_title">要移除 %1$s 嗎?</string> + <string name="flathub_remove_note">其設定和檔案會保留。</string> + <string name="flathub_keep">保留</string> + <string name="desktop_size">佔用空間</string> + <string name="desktop_uninstall_title">要解除安裝桌面嗎?</string> + <string name="desktop_all_systems">全部</string> + <string name="gog_achievements">GOG 成就</string> + <string name="gog_achievements_hint">為支援的 GOG 遊戲同步成就。下次啟動時生效。</string> + <string name="gog_achievements_off">成就已關閉</string> + <string name="gog_achievements_on">成就已開啟</string> + <string name="gog_achievements_signin">重新登入 GOG</string> + <string name="gog_achievements_uploaded">成就已上傳</string> + <string name="gog_achievements_pending">成就上傳待處理</string> + <string name="gog_achievements_enable_store">在「商店」設定中開啟 GOG 成就</string> + <string name="gog_achievements_unavailable">成就無法使用;請查看工作階段記錄</string> + <string name="achievement_unlocked">成就已解鎖</string> + <string name="touch_paste">貼上</string> + <string name="touch_dpad">方向鍵</string> + <string name="touch_left_stick">左搖桿</string> + <string name="touch_right_stick">右搖桿</string> + <string name="touch_menu">選單</string> + <string name="touch_keyboard">鍵盤</string> + <string name="touch_dpad_up">方向鍵上</string> + <string name="touch_dpad_down">方向鍵下</string> + <string name="touch_dpad_left">方向鍵左</string> + <string name="touch_dpad_right">方向鍵右</string> + <string name="touch_title">觸控操作</string> + <string name="touch_controller">觸控控制器</string> + <string name="touch_mouse">滑鼠</string> + <string name="touch_on">開</string> + <string name="touch_off">關</string> + <string name="touch_speed_slow">慢</string> + <string name="touch_speed_normal">正常</string> + <string name="touch_speed_fast">快</string> + <string name="touch_edit_layout">編輯版面配置</string> + <string name="touch_droiddeck_menu">DroidDeck 選單</string> + <string name="touch_drag_hint">將控制項拖曳到畫面上</string> + <string name="touch_arrange">排列</string> + <string name="touch_discard">放棄</string> + <string name="touch_saving">儲存中…</string> + <string name="touch_icon">圖示</string> + <string name="touch_narrower">寬−</string> + <string name="touch_wider">寬+</string> + <string name="touch_shorter">高−</string> + <string name="touch_taller">高+</string> + <string name="touch_hide">隱藏</string> + <string name="touch_color_opacity">顏色與不透明度…</string> + <string name="touch_copy_layout">複製這個版面配置</string> + <string name="touch_layout_copied">版面配置已複製,可貼到任何遊戲的觸控版面配置中</string> + <string name="touch_paste_layout">貼上複製的版面配置</string> + <string name="touch_reset_layout">還原為 Steam 的預設值</string> + <string name="touch_no_binding">這個控制項在 Steam 中沒有綁定,無法設定圖示</string> + <string name="touch_icon_for">%1$s 的圖示</string> + <string name="touch_layout_color">觸控版面配置顏色</string> + <string name="touch_done">完成</string> + <string name="touch_save_failed">無法儲存觸控版面配置</string> + <string name="touch_saved">觸控版面配置已儲存至 Steam</string> + <string name="touch_app_id">應用程式 %1$d</string> + <string name="touch_label_hint">標籤(選用)</string> + <string name="touch_icons_missing">找不到 Steam 的圖示</string> + <plurals name="touch_icons_found"> + <item quantity="other">來自 Steam 的 %1$d 個圖示</item> + </plurals> + <string name="touch_icon_color">圖示顏色</string> + <string name="touch_button_color">按鈕顏色</string> + <string name="touch_no_icon">沒有圖示</string> + + <string name="stores_sync_sign_in">正在檢查登入狀態…</string> + <string name="stores_sync_library">正在取得你的遊戲庫…</string> + <string name="stores_sync_game_list">正在取得遊戲清單…</string> + <string name="stores_sync_details">正在載入遊戲詳細資料…(%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="other">正在同步 %1$d 個遊戲…</item> + </plurals> + <string name="stores_sync_progress">正在同步… %1$d/%2$d</string> + <string name="stores_sync_cover_art">正在取得封面圖片…</string> + <string name="stores_sync_failed">同步失敗</string> + <string name="stores_sync_no_games">在你的 %1$s 遊戲庫中找不到遊戲</string> + <string name="stores_sync_amazon_no_answer">Amazon Games 未回應你的遊戲庫要求:請重新整理,或重新登入</string> + <string name="stores_sync_gog_unreachable">無法連線到 GOG</string> + <string name="stores_sync_gog_unreadable">無法讀取你的 GOG 遊戲庫</string> + <string name="stores_registering">正在向 Steam 註冊…</string> <string name="drawer_page_glasses">眼鏡</string> <string name="glasses_open">VITURE 眼鏡</string> <string name="glasses_control">眼鏡控制</string> @@ -2152,4 +2226,34 @@ <string name="stores_hltb_completionist">全收集</string> <string name="stores_hltb_hours">%1$s 小時</string> <string name="stores_hltb_minutes">%1$d 分鐘</string> + <string name="cardboard_title">Cardboard VR</string> + <string name="cardboard_mode">Cardboard 模式</string> + <string name="cardboard_mode_hint">為手機 VR 眼鏡提供左右眼畫面。</string> + <string name="cardboard_source">畫面</string> + <string name="cardboard_source_screen">虛擬螢幕</string> + <string name="cardboard_source_sbs">左右並排 3D</string> + <string name="cardboard_viewer">眼鏡</string> + <string name="cardboard_viewer_v2">Cardboard</string> + <string name="cardboard_viewer_v1">Cardboard(2014)</string> + <string name="cardboard_viewer_none">無鏡片</string> + <string name="cardboard_ipd">瞳距</string> + <string name="cardboard_screen_width">螢幕寬度</string> + <string name="cardboard_screen_distance">螢幕距離</string> + <string name="cardboard_head_tracking">頭部追蹤</string> + <string name="cardboard_no_sensor">這部裝置沒有旋轉感測器。</string> + <string name="cardboard_recenter">重設視角</string> + <string name="cardboard_recenter_button">重設</string> + <string name="cardboard_steamvr">SteamVR(實驗性)</string> + <string name="cardboard_steamvr_install">安裝</string> + <string name="cardboard_steamvr_install_hint">下載 Valve 的 ARM64 版 SteamVR(571 MB)。</string> + <string name="cardboard_steamvr_installing">正在下載並解壓縮 SteamVR…</string> + <string name="cardboard_steamvr_start">啟動</string> + <string name="cardboard_steamvr_ready_hint">以你的頭部作為頭戴裝置執行 SteamVR;畫面會切換為左右並排 3D。</string> + <string name="cardboard_steamvr_stop">停止</string> + <string name="cardboard_steamvr_running_hint">SteamVR 正在執行。</string> + <string name="cardboard_steamvr_unavailable">這個工作階段無法使用。</string> + <string name="cardboard_steamvr_failed">SteamVR 沒有回應;請查看工作階段記錄。</string> + <string name="drawer_component_downloads">下載元件</string> + <string name="drawer_component_downloads_hint">下載內容與「元件」頁面共用。在上方選擇已下載的版本,即可在下次啟動遊戲時使用。</string> + <string name="drawer_component_choose_download">選擇要下載的版本</string> </resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 229cda00f..d5c780ae8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -278,6 +278,9 @@ <string name="mode_suspend_steam_note">Auto pauses and resumes when shown. Native prepares Steam for sleep first. Manual waits for Resume. Never keeps running.</string> <string name="mode_background_downloads">Keep downloads running</string> <string name="mode_background_downloads_hint">Delay background suspension while Steam is actively downloading.</string> + <string name="mode_zram">zRAM compression</string> + <string name="mode_zram_hint">While a game runs, the Steam client\'s memory is compressed into the device\'s zram, leaving more RAM for the game. Applies the next time Steam starts.</string> + <string name="mode_zram_unsupported">Unavailable: this device has no zram or swap, or its kernel is older than 5.4.</string> <string name="mode_suspend_note">Auto pauses in the background and resumes when shown. Manual waits for Resume. Never keeps running.</string> <string name="mode_startup">Startup</string> <string name="mode_steam_startup">Start Steam with DroidDeck</string> @@ -713,6 +716,14 @@ <string name="setup_saved_logs">Saved session logs</string> <string name="setup_saved_logs_hint">Keeps the newest %1$d</string> <string name="setup_clear_logs">Clear logs</string> + <string name="setup_reset_settings">Reset all settings</string> + <string name="setup_reset_settings_hint">Puts every setting back to its default</string> + <string name="setup_reset_settings_button">Reset</string> + <string name="setup_reset_settings_title">Reset all settings?</string> + <string name="setup_reset_settings_body">Display, performance, controller, theme, language and launcher settings go back to their defaults. Your games, Steam sign-in, store accounts, downloaded drivers and storage folders are kept.</string> + <string name="setup_reset_settings_profiles">Also reset game profiles (each game\'s Proton, FEX, components and environment, and the Default profile\'s environment)</string> + <string name="setup_reset_settings_session_running">Stop the session before resetting settings.</string> + <string name="setup_reset_settings_partial">Settings were reset, but some parts could not be. The logs have the details.</string> <string name="setup_offline">Offline mode</string> <string name="setup_sign_in_first">Sign in to Steam first</string> <string name="setup_launcher">Launcher</string> @@ -1111,11 +1122,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> @@ -1173,16 +1181,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> @@ -1192,16 +1196,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> @@ -1215,68 +1215,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> @@ -1284,16 +1271,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> @@ -1308,9 +1292,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> @@ -1326,33 +1308,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> @@ -1367,28 +1341,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> @@ -1401,21 +1368,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> @@ -1424,7 +1384,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> @@ -1442,22 +1401,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> @@ -1475,7 +1429,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> @@ -1483,7 +1436,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> @@ -1497,15 +1449,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> @@ -1515,7 +1463,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> @@ -1526,19 +1473,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> @@ -1553,7 +1497,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> @@ -1568,42 +1511,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> @@ -1612,7 +1545,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> @@ -1623,7 +1555,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> @@ -1635,27 +1566,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> @@ -1671,63 +1596,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> @@ -1763,20 +1672,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> @@ -1784,20 +1686,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> @@ -1811,27 +1707,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> @@ -1839,16 +1730,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> @@ -1865,48 +1751,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> @@ -1931,27 +1807,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> @@ -1962,91 +1834,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> @@ -2075,7 +1918,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> @@ -2121,7 +1963,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> @@ -2156,7 +1997,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> @@ -2172,7 +2012,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> @@ -2186,7 +2025,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> @@ -2247,6 +2085,9 @@ <string name="stores_install_title">Install %1$s:</string> <string name="stores_target_internal">Internal</string> <string name="stores_target_free">%1$s free</string> + <string name="stores_target_free_unknown">Free space unknown</string> + <string name="stores_storage_insufficient">Not enough free space: need %1$s, only %2$s free</string> + <string name="stores_storage_low">Insufficient space (%1$s free, need %2$s)</string> <string name="stores_notification_channel">Store downloads</string> <string name="stores_notification_channel_hint">Shows game download progress and keeps downloads running in the background</string> <string name="stores_notification_one">Downloading</string> @@ -2302,13 +2143,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> @@ -2325,7 +2163,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> @@ -2336,7 +2173,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> @@ -2344,7 +2180,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> @@ -2358,6 +2193,74 @@ <string name="gog_achievements_enable_store">Enable GOG achievements in Stores settings</string> <string name="gog_achievements_unavailable">Achievements unavailable; check session logs</string> <string name="achievement_unlocked">Achievement unlocked</string> + <string name="touch_paste">Paste</string> + <string name="touch_dpad">D-pad</string> + <string name="touch_left_stick">Left stick</string> + <string name="touch_right_stick">Right stick</string> + <string name="touch_menu">Menu</string> + <string name="touch_keyboard">Keyboard</string> + <string name="touch_dpad_up">D-pad up</string> + <string name="touch_dpad_down">D-pad down</string> + <string name="touch_dpad_left">D-pad left</string> + <string name="touch_dpad_right">D-pad right</string> + <string name="touch_title">Touch controls</string> + <string name="touch_controller">Touch Controller</string> + <string name="touch_mouse">Mouse</string> + <string name="touch_on">On</string> + <string name="touch_off">Off</string> + <string name="touch_speed_slow">Slow</string> + <string name="touch_speed_normal">Normal</string> + <string name="touch_speed_fast">Fast</string> + <string name="touch_edit_layout">Edit layout</string> + <string name="touch_droiddeck_menu">DroidDeck menu</string> + <string name="touch_drag_hint">Drag controls onto the screen</string> + <string name="touch_arrange">Arrange</string> + <string name="touch_discard">Discard</string> + <string name="touch_saving">Saving…</string> + <string name="touch_icon">Icon</string> + <string name="touch_narrower">W−</string> + <string name="touch_wider">W+</string> + <string name="touch_shorter">H−</string> + <string name="touch_taller">H+</string> + <string name="touch_hide">Hide</string> + <string name="touch_color_opacity">Color and opacity…</string> + <string name="touch_copy_layout">Copy this layout</string> + <string name="touch_layout_copied">Layout copied; paste it into any game\'s touch layout</string> + <string name="touch_paste_layout">Paste copied layout</string> + <string name="touch_reset_layout">Reset to Steam\'s default</string> + <string name="touch_no_binding">This control has no binding in Steam to put an icon on</string> + <string name="touch_icon_for">Icon for %1$s</string> + <string name="touch_layout_color">Touch layout color</string> + <string name="touch_done">Done</string> + <string name="touch_save_failed">Could not save the touch layout</string> + <string name="touch_saved">Touch layout saved to Steam</string> + <string name="touch_app_id">App %1$d</string> + <string name="touch_label_hint">Label (optional)</string> + <string name="touch_icons_missing">Steam\'s icons were not found</string> + <plurals name="touch_icons_found"> + <item quantity="one">%1$d icon from Steam</item> + <item quantity="other">%1$d icons from Steam</item> + </plurals> + <string name="touch_icon_color">Icon color</string> + <string name="touch_button_color">Button color</string> + <string name="touch_no_icon">No icon</string> + + <string name="stores_sync_sign_in">Checking sign-in…</string> + <string name="stores_sync_library">Fetching your library…</string> + <string name="stores_sync_game_list">Fetching game list…</string> + <string name="stores_sync_details">Loading game details… (%1$d/%2$d)</string> + <plurals name="stores_sync_games"> + <item quantity="one">Syncing %1$d game…</item> + <item quantity="other">Syncing %1$d games…</item> + </plurals> + <string name="stores_sync_progress">Syncing… %1$d/%2$d</string> + <string name="stores_sync_cover_art">Fetching cover art…</string> + <string name="stores_sync_failed">Sync failed</string> + <string name="stores_sync_no_games">No games found in your %1$s library</string> + <string name="stores_sync_amazon_no_answer">Amazon Games did not answer for your library: refresh, or sign in again</string> + <string name="stores_sync_gog_unreachable">Couldn\'t reach GOG</string> + <string name="stores_sync_gog_unreadable">Could not read your GOG library</string> + <string name="stores_registering">Registering with Steam…</string> <string name="drawer_page_glasses">Glasses</string> <string name="glasses_open">VITURE glasses</string> <string name="glasses_control">Glasses control</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 86bafa9b1..c6ade4b06 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -1,11 +1,33 @@ <?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" /> + <locale android:name="ms" /> + <locale android:name="cs" /> + <locale android:name="da" /> + <locale android:name="de" /> <locale android:name="es" /> + <locale android:name="es-419" /> <locale android:name="fr" /> + <locale android:name="it" /> + <locale android:name="hu" /> + <locale android:name="nl" /> + <locale android:name="nb" /> + <locale android:name="pl" /> + <locale android:name="pt-BR" /> + <locale android:name="pt-PT" /> + <locale android:name="ro" /> + <locale android:name="fi" /> + <locale android:name="sv" /> + <locale android:name="vi" /> + <locale android:name="tr" /> + <locale android:name="el" /> + <locale android:name="bg" /> <locale android:name="ru" /> + <locale android:name="uk" /> + <locale android:name="ar" /> + <locale android:name="hi" /> + <locale android:name="th" /> <locale android:name="ja" /> <locale android:name="ko" /> <locale android:name="zh-CN" /> 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/JNI.md b/app/src/main/rust/stores/JNI.md index 9f1b38550..9cd314dfd 100644 --- a/app/src/main/rust/stores/JNI.md +++ b/app/src/main/rust/stores/JNI.md @@ -112,7 +112,45 @@ On-disk protocol (what a resumed run, by either engine, relies on): a file is as ## `com.droiddeck.launcher.stores.epic.EpicNative` -Two runs make up the native half of an Epic install: +The manager's default Epic install is the **stream** run (`nativeStream`, mode `adaptive`, +below): the chunks are fetched in parallel and written straight into staged files as they arrive; +on device it measured about 83 MB/s on a 71 GB install. The **lookahead** install +(`nativeInstallAdaptive`, mode `lookahead`) is its fallback when the stream engine's space plan +does not fit, and is selectable directly, as is the legacy batch pair (`nativeStart` + +`nativeAssemble`, mode `batch`), through the hidden `storesEpicMode` preference. + +### Lookahead install (snapshot schema v1) + +`nativeInstallAdaptive(request: String, listener: EpicNative.Listener): Long` +exports `Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeInstallAdaptive`. +Its exact Kotlin descriptor is +`(Ljava/lang/String;Lcom/droiddeck/launcher/stores/epic/EpicNative$Listener;)J`. +The listener must also implement `EpicNative.InstallListener.beforeSwap(): Boolean` (`()Z`). +Rust invokes it synchronously after staging/hash/sync and before the first atomic replacement; +false or a Java exception aborts without replacing that output. Java marks the sidecar updating +there. Later verified swaps preserve mixed-update/resume semantics. + +The UTF-8 JSON request contains: + +- `schema_version: 1`, absolute `install_dir`, `cache_dir`, owned `staging_dir`, + `same_volume`, signed `free_game`/`free_cache` (`-1` unknown), `max_workers`, CDN `hosts`. +- `chunks`: distinct `guid`, relative CDN `path`, decoded `size`, `exact_size`, `wire_size`, + SHA-1 hex `sha1` (empty/all-zero means absent), `cached` verified length (`-1` absent). +- `files`: pending manifest-order relative `path`, `sha1`, current `old_size`, ordered `parts` + containing `guid`, unsigned `offset` and `size`. + +Parse, owned-namespace validation and adaptive storage planning happen synchronously. Errors +return zero after `onComplete(false, error, 0)` and are **terminal**, not fallback signals. +`onPlan` reports the selected lookahead/retention/peaks; `onProgress` counts staged file bytes +and verified swapped file counts, not fetched wire bytes. Fetch and staged assembly overlap +in one native run, using the same pure reservation/release policy as the peak planner. +Cancellation/release joins native workers; no worker survives the terminal callback/handle +release. Only `StoresNative.ensureLoaded() == false` selects bounded parallel Java; the same rule +holds for `nativeStream`. + +### Legacy batch APIs (mode `batch`, for A/B runs) + +Two explicit calls provide the legacy batch interface: 1. **Fetch** (`nativeStart`): fills the chunk cache with verified, decompressed chunks for the files the manager says are pending. The Kotlin manager still fetches and parses the manifest @@ -224,6 +262,57 @@ Bannerlator's `BlEpicDownload.run(...)` wraps `nativeStart` in a blocking call t `onComplete`, then `nativeRelease` in `finally`. The same wrapper shape fits `nativeAssemble` (cancel is honoured between files; a multi-GB file finishes before the run stops). +### `nativeStream` — the adaptive install + +| Method | JNI signature | Symbol | +|---|---|---| +| `nativeStream(...)`: Long | `([BLjava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;[IJJJLjava/lang/String;IILcom/droiddeck/launcher/stores/epic/EpicNative$StreamListener;)J` | `Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeStream` | + +```kotlin +@JvmStatic private external fun nativeStream( + manifest: ByteArray, installDir: String, chunkCacheDir: String, stagingDir: String, + cdnPrefixes: Array<String>, pendingFileIdx: IntArray, expectedBytes: Long, cacheBudget: Long, + overlapBudget: Long, caBundlePath: String, + maxWorkers: Int, processWorkers: Int, listener: StreamListener, +): Long +``` + +Parameters as `nativeStart`, plus `stagingDir` (the manager's owned +`<installDir>/.droiddeck-epic-staging`, which must exist; `""` = stage beside each file), and +except `expectedBytes` (the manager's `Σ fileSize` of the pending +files, `-1` = no cross-check; no chunk count) and `cacheBudget` (the most bytes of shared chunks +the cache may hold; the manager sizes it from free space, `0` = never cache, refetch instead) and +`overlapBudget` (the most planned bytes of *replaced* files - destination already holds data - +staged at once, since each sits beside its old copy until its swap; one is always allowed so the +run progresses; `-1` = no limit). Files are admitted in order and a chunk is dispatched only once +the file of its first use is admitted; fresh files are never held back. +`chunkCacheDir` is `""` (`<installDir>/.chunks`) or a scratch directory; the manager falls back to +`""` for a card install when internal storage cannot hold the budget. + +What the run does (`epic/stream.rs`): a chunk one file part uses is decompressed, verified and +written at the part's offset into a unique `stream-*.stage` file in `stagingDir`; a +chunk several parts use is also kept in the cache, decompressed, as `<GUID>` (a later copy after +an eviction as `<GUID>.<n>`) and deleted after its last covered part. The plan evicts by furthest +next use when the budget would be exceeded and fetches the evicted chunk again later. Each file, +once complete: synced, size and SHA-1 checked, `onBeforeReplace()` once for the whole run, then +renamed over its destination. Cancel or failure removes every unfinished stage file before +`onComplete`, leaves the destinations and the cache; success removes the cache directory. + +Planning runs on the calling thread and also prepares the cache (keeps verified chunks this +install needs, deletes everything else, as the Java writer does). A refusal returns `0` after a +synchronous `onComplete(false, reason, 0)`; nothing was fetched or written, and the manager fails +the install with that reason (it does not switch engines once the library is loaded). + +| `StreamListener` method | JNI signature | When | +|---|---|---| +| `onStreamProgress(bytesWritten: Long, bytesTotal: Long, filesDone: Int, filesTotal: Int, filesBytes: Long)` | `(JJIIJ)V` | after written parts (at most every 50 ms) and after every finished file; `filesBytes` = sizes of the files swapped in | +| `onLog(line: String)` | `(Ljava/lang/String;)V` | engine lines: `stream-plan …`, the fetch core's lines, `stream-summary mode=adaptive …` (peak cache, peak staging, refetches, direct vs cached bytes) | +| `onBeforeReplace(): Boolean` | `()Z` | once, before the first file replaces its destination; `false` or a throw fails the run with nothing replaced | +| `onComplete(success: Boolean, error: String, bytes: Long)` | `(ZLjava/lang/String;J)V` | terminal, exactly once; `bytes` = file bytes written; `error` = `"cancelled"` on cancel | + +Unlike the batch runs, the Kotlin wrapper (`EpicNative.stream`) waits for `onComplete` after a +cancel without a time limit: the stage-file cleanup must be finished before the manager returns. + ## `com.droiddeck.launcher.stores.amazon.AmazonNative` The file-fetch pool of an Amazon Games install: one streamed HTTP GET per manifest file, SHA-256 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..9e131391b 100644 --- a/app/src/main/rust/stores/src/epic.rs +++ b/app/src/main/rust/stores/src/epic.rs @@ -1,46 +1,19 @@ -//! 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 adaptive; pub mod chunk; pub mod driver; pub mod jni; pub mod manifest; pub mod plan; +pub mod stream; -/// 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/adaptive.rs b/app/src/main/rust/stores/src/epic/adaptive.rs new file mode 100644 index 000000000..7113723ea --- /dev/null +++ b/app/src/main/rust/stores/src/epic/adaptive.rs @@ -0,0 +1,1585 @@ +//! Budgeted native install: ordered lookahead, shared peak schedule, fetch-core overlap and +//! verified per-file atomic swaps. The JNI snapshot is the Java delta pass's exact pending set. + +use crate::fetch_core::{run_fetch, FetchItem, FetchOptions, FetchSink, SinkError}; +use serde_json::Value; +use sha1::{Digest, Sha1}; +use std::collections::{HashMap, HashSet}; +use std::fs::{self, OpenOptions}; +use std::io::{BufWriter, Write}; +use std::path::{Component, Path, PathBuf}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Condvar, Mutex}; +use std::time::Duration; + +const MIB: u64 = 1024 * 1024; +const RETAINED: u64 = 64 * MIB; + +#[derive(Clone, Debug)] +pub struct Chunk { + guid: String, + path: String, + size: u64, + wire: u64, + sha: Option<[u8; 20]>, + cached: Option<u64>, + exact: bool, +} +#[derive(Clone, Debug)] +pub struct Part { + chunk: usize, + offset: u64, + size: u64, +} +#[derive(Clone, Debug)] +pub struct Output { + path: String, + sha: Option<[u8; 20]>, + old: u64, + parts: Vec<Part>, +} +#[derive(Clone, Debug)] +pub struct Request { + pub install: PathBuf, + pub cache: PathBuf, + pub staging: PathBuf, + pub same_volume: bool, + pub free_game: Option<u64>, + pub free_cache: Option<u64>, + pub chunks: Vec<Chunk>, + pub files: Vec<Output>, + pub hosts: Vec<String>, + pub max_workers: usize, +} + +fn uint(v: &Value, key: &str) -> Result<u64, String> { + v[key].as_u64().ok_or_else(|| format!("invalid {key}")) +} +fn text(v: &Value, key: &str) -> Result<String, String> { + v[key] + .as_str() + .map(str::to_string) + .ok_or_else(|| format!("invalid {key}")) +} +fn digest(v: &Value) -> Result<Option<[u8; 20]>, String> { + let s = v.as_str().ok_or("invalid sha1")?; + if s.is_empty() { + return Ok(None); + } + if s.len() != 40 || !s.is_ascii() { + return Err("invalid sha1".into()); + } + let mut out = [0; 20]; + for (i, b) in out.iter_mut().enumerate() { + *b = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).map_err(|_| "invalid sha1")?; + } + Ok(out.iter().any(|b| *b != 0).then_some(out)) +} +fn relative(s: &str) -> Result<(), String> { + if s.is_empty() + || s.contains('\\') + || s.contains(':') + || Path::new(s) + .components() + .any(|c| !matches!(c, Component::Normal(_))) + { + return Err(format!("unsafe manifest path {s}")); + } + Ok(()) +} +impl Request { + pub fn parse(json: &str) -> Result<Self, String> { + let v: Value = serde_json::from_str(json).map_err(|e| e.to_string())?; + if uint(&v, "schema_version")? != 1 { + return Err("unsupported adaptive request schema".into()); + } + let mut chunks = Vec::new(); + let mut by_guid = HashMap::new(); + for c in v["chunks"].as_array().ok_or("missing chunks")? { + let guid = text(c, "guid")?; + if guid.len() != 32 + || !guid.bytes().all(|b| b.is_ascii_hexdigit()) + || by_guid.contains_key(&guid) + { + return Err("invalid or duplicate chunk GUID".into()); + } + let path = text(c, "path")?; + relative(&path)?; + by_guid.insert(guid.clone(), chunks.len()); + chunks.push(Chunk { + guid, + path, + size: uint(c, "size")?, + wire: uint(c, "wire_size")?, + sha: digest(&c["sha1"])?, + cached: c["cached"].as_u64(), + exact: c["exact_size"].as_bool().ok_or("missing exact_size")?, + }); + } + let mut files = Vec::new(); + for f in v["files"].as_array().ok_or("missing files")? { + let path = text(f, "path")?; + relative(&path)?; + if path.split('/').next() == Some(".droiddeck-epic-staging") { + return Err("manifest conflicts with owned staging".into()); + } + let mut parts = Vec::new(); + for p in f["parts"].as_array().ok_or("missing parts")? { + let chunk = *by_guid + .get(&text(p, "guid")?) + .ok_or("missing chunk metadata")?; + let offset = uint(p, "offset")?; + let size = uint(p, "size")?; + if offset + .checked_add(size) + .filter(|n| *n <= chunks[chunk].size) + .is_none() + { + return Err("part exceeds decoded window".into()); + } + parts.push(Part { + chunk, + offset, + size, + }); + } + files.push(Output { + path, + parts, + sha: digest(&f["sha1"])?, + old: uint(f, "old_size")?, + }); + } + let install = PathBuf::from(text(&v, "install_dir")?); + let staging = PathBuf::from(text(&v, "staging_dir")?); + Self::validate_staging(&install, &staging)?; + let hosts = v["hosts"] + .as_array() + .ok_or("missing hosts")? + .iter() + .map(|h| { + h.as_str() + .map(str::to_string) + .ok_or_else(|| "invalid host".to_string()) + }) + .collect::<Result<Vec<_>, _>>()?; + if hosts.is_empty() { + return Err("no Epic CDN".into()); + } + let cache = PathBuf::from(text(&v, "cache_dir")?); + let declared_same = v["same_volume"].as_bool().ok_or("missing same_volume")?; + let same_volume = declared_same || Self::same_filesystem(&install, &cache); + let game_probe = v["free_game"].as_u64(); + let cache_probe = v["free_cache"].as_u64(); + let free_game = if same_volume { + match (game_probe, cache_probe) { + (Some(a), Some(b)) => Some(a.min(b)), + (a, b) => a.or(b), + } + } else { + game_probe + }; + Ok(Self { + install, + staging, + cache, + chunks, + files, + hosts, + same_volume, + free_game, + free_cache: cache_probe, + max_workers: uint(&v, "max_workers")?.clamp(1, 32) as usize, + }) + } + + fn validate_staging(install: &Path, staging: &Path) -> Result<(), String> { + let expected = fs::canonicalize(install) + .map_err(|e| e.to_string())? + .join(".droiddeck-epic-staging"); + let metadata = fs::symlink_metadata(staging).map_err(|e| e.to_string())?; + if !metadata.file_type().is_dir() + || fs::canonicalize(staging).map_err(|e| e.to_string())? != expected + { + return Err("unsafe staging directory; refusing to traverse it".into()); + } + let owner = staging.join("owner"); + let metadata = fs::symlink_metadata(&owner).map_err(|e| e.to_string())?; + if !metadata.file_type().is_file() + || metadata.len() != 26 + || fs::canonicalize(&owner).map_err(|e| e.to_string())? != expected.join("owner") + || fs::read(&owner).map_err(|e| e.to_string())? != b"DroidDeck Epic staging v1\n" + { + return Err("unowned staging directory".into()); + } + for entry in fs::read_dir(staging).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let name = entry.file_name(); + if name == "owner" { + continue; + } + if !entry.file_type().map_err(|e| e.to_string())?.is_file() + || !name.to_string_lossy().ends_with(".stage") + || fs::canonicalize(entry.path()).map_err(|e| e.to_string())? + != expected.join(&name) + { + return Err("unsafe staging entry".into()); + } + } + Ok(()) + } + + #[cfg(unix)] + fn same_filesystem(a: &Path, b: &Path) -> bool { + use std::os::unix::fs::MetadataExt; + match (fs::metadata(a), fs::metadata(b)) { + (Ok(a), Ok(b)) => a.dev() == b.dev(), + _ => false, + } + } + #[cfg(not(unix))] + fn same_filesystem(_a: &Path, _b: &Path) -> bool { + false + } + fn total(&self) -> u64 { + self.files + .iter() + .flat_map(|f| &f.parts) + .map(|p| p.size) + .sum() + } +} + +/// Resident includes full-size reservations for in-flight writes; only ready chunks can be read. +#[derive(Clone, Debug)] +struct Schedule { + order: Vec<usize>, + cursor: usize, + refs: Vec<usize>, + resident: Vec<Option<u64>>, + oldest: Vec<usize>, + lookahead: usize, + retained: u64, +} +impl Schedule { + fn new(req: &Request, lookahead: usize, retained: u64) -> Self { + let order: Vec<_> = req + .files + .iter() + .flat_map(|f| f.parts.iter().map(|p| p.chunk)) + .collect(); + let mut refs = vec![0; req.chunks.len()]; + for &c in &order { + refs[c] += 1; + } + let resident: Vec<_> = req.chunks.iter().map(|c| c.cached).collect(); + let oldest = (0..resident.len()) + .filter(|&c| resident[c].is_some()) + .collect(); + Self { + order, + cursor: 0, + refs, + resident, + oldest, + lookahead, + retained, + } + } + fn window(&self) -> HashSet<usize> { + self.order[self.cursor..self.order.len().min(self.cursor + self.lookahead)] + .iter() + .copied() + .collect() + } + fn fill(&mut self, req: &Request) -> Vec<usize> { + let mut fetch = Vec::new(); + for &c in &self.order[self.cursor..self.order.len().min(self.cursor + self.lookahead)] { + if self.resident[c].is_none() { + self.resident[c] = Some(req.chunks[c].size); + self.oldest.push(c); + fetch.push(c); + } + } + fetch + } + fn consume(&mut self) -> Vec<usize> { + let c = self.order[self.cursor]; + self.cursor += 1; + self.refs[c] -= 1; + let mut release = Vec::new(); + if self.refs[c] == 0 { + self.resident[c] = None; + release.push(c); + } + let pinned = self.window(); + let mut reusable: u64 = self + .oldest + .iter() + .filter(|c| !pinned.contains(c)) + .map(|&c| self.resident[c].unwrap_or(0)) + .sum(); + for &c in &self.oldest { + if reusable <= self.retained { + break; + } + if pinned.contains(&c) { + continue; + } + if let Some(bytes) = self.resident[c].take() { + reusable -= bytes; + release.push(c); + } + } + self.oldest.retain(|&c| self.resident[c].is_some()); + release + } + fn bytes(&self) -> u64 { + self.oldest + .iter() + .map(|&c| self.resident[c].unwrap_or(0)) + .sum() + } +} + +#[derive(Clone, Copy, Debug)] +pub struct Budget { + pub lookahead: usize, + pub retained: u64, + pub game_peak: u64, + pub cache_peak: u64, +} +fn peaks(req: &Request, lookahead: usize, retained: u64) -> (u64, u64) { + let mut s = Schedule::new(req, lookahead, retained); + let initial = s.bytes() as i128; + let mut game = 0i128; + let mut game_peak = 0i128; + let mut cache_peak = 0i128; + for file in &req.files { + for part in &file.parts { + s.fill(req); + let cache = s.bytes() as i128 - initial; + cache_peak = cache_peak.max(cache); + game_peak = game_peak.max(game + if req.same_volume { cache } else { 0 }); + game += part.size as i128; + game_peak = game_peak.max(game + if req.same_volume { cache } else { 0 }); + s.consume(); + } + game -= file.old as i128; + } + (game_peak.max(0) as u64, cache_peak.max(0) as u64) +} +fn fits(needed: u64, free: Option<u64>) -> bool { + free.is_none_or(|n| needed <= n) +} +pub fn choose_budget(req: &Request) -> Result<Budget, String> { + // Unknown probes deliberately choose a conservative bounded mode, never "0 free". + let unknown = req.free_game.is_none() || (!req.same_volume && req.free_cache.is_none()); + for lookahead in [128, 64, 32, 16, 8, 4, 2, 1] { + if unknown && lookahead > 8 { + continue; + } + for retained in [RETAINED, 16 * MIB, 0] { + let (game_peak, cache_peak) = peaks(req, lookahead, retained); + if fits(game_peak, req.free_game) + && (req.same_volume || fits(cache_peak, req.free_cache)) + { + return Ok(Budget { + lookahead, + retained, + game_peak, + cache_peak, + }); + } + } + } + let (game, cache) = peaks(req, 1, 0); + Err(format!("Not enough free space for safe staged Epic replacement: minimum game additional={game}, cache additional={cache}; free game={:?}, cache={:?}", req.free_game, req.free_cache)) +} + +fn remaining_peaks( + req: &Request, + schedule: &Schedule, + fi: usize, + pi: usize, + disk_cache: u64, +) -> (u64, u64) { + let mut s = schedule.clone(); + let mut game = 0i128; + let mut game_peak = 0i128; + let mut cache_peak = 0i128; + let outstanding = s.bytes() as i128 - disk_cache as i128; + cache_peak = cache_peak.max(outstanding); + if req.same_volume { + game_peak = game_peak.max(outstanding); + } + for (index, file) in req.files.iter().enumerate().skip(fi) { + for part in file.parts.iter().skip(if index == fi { pi } else { 0 }) { + s.fill(req); + let cache = s.bytes() as i128 - disk_cache as i128; + cache_peak = cache_peak.max(cache); + game_peak = game_peak.max(game + if req.same_volume { cache } else { 0 }); + game += part.size as i128; + game_peak = game_peak.max(game + if req.same_volume { cache } else { 0 }); + s.consume(); + } + game -= file.old as i128; + } + (game_peak.max(0) as u64, cache_peak.max(0) as u64) +} + +#[cfg(unix)] +fn available(path: &Path) -> Option<u64> { + use std::os::unix::ffi::OsStrExt; + let path = std::ffi::CString::new(path.as_os_str().as_bytes()).ok()?; + let mut stat = std::mem::MaybeUninit::<libc::statvfs>::uninit(); + if unsafe { libc::statvfs(path.as_ptr(), stat.as_mut_ptr()) } != 0 { + return None; + } + let stat = unsafe { stat.assume_init() }; + Some((stat.f_bavail as u64).saturating_mul(stat.f_frsize as u64)) +} +#[cfg(not(unix))] +fn available(_path: &Path) -> Option<u64> { + None +} + +/// Re-probe at admission boundaries. Already dispatched reservations cannot be withdrawn. +/// Shrink only unsent lookahead/reusable budget; never wait forever for unrelated space use. +fn live_admit( + req: &Request, + state: &mut State, + fi: usize, + pi: usize, + stage_remaining: u64, +) -> Result<(), String> { + let disk_cache = state + .schedule + .oldest + .iter() + .map(|&index| { + if state.ready[index] { + return state.disk_sizes[index]; + } + if !state.writing[index] { + return 0; + } + let c = &req.chunks[index]; + fs::metadata(req.cache.join(&c.guid)) + .map(|m| m.len()) + .unwrap_or(0) + + fs::metadata(super::chunk::part_path(&req.cache.join(&c.guid))) + .map(|m| m.len()) + .unwrap_or(0) + }) + .sum(); + let free_game = available(&req.install); + let free_cache = available(&req.cache); + admit_with_probes( + req, + state, + fi, + pi, + stage_remaining, + disk_cache, + free_game, + free_cache, + ) +} + +fn admit_with_probes( + req: &Request, + state: &mut State, + fi: usize, + pi: usize, + stage_remaining: u64, + disk_cache: u64, + free_game: Option<u64>, + free_cache: Option<u64>, +) -> Result<(), String> { + loop { + let incoming: u64 = state + .schedule + .window() + .iter() + .filter(|&&c| state.schedule.resident[c].is_none()) + .map(|&c| req.chunks[c].size) + .sum(); + let cache_now = state + .schedule + .bytes() + .saturating_add(incoming) + .saturating_sub(disk_cache); + let game_now = stage_remaining.saturating_add(if req.same_volume { cache_now } else { 0 }); + if fits(game_now, free_game) && (req.same_volume || fits(cache_now, free_cache)) { + return Ok(()); + } + // Full remaining simulation is only needed when live admission headroom is exhausted. + let (game, cache) = remaining_peaks(req, &state.schedule, fi, pi, disk_cache); + if fits(game, free_game) && (req.same_volume || fits(cache, free_cache)) { + return Ok(()); + } + if state.schedule.oldest.iter().any(|&c| !state.ready[c]) { + return Err("Not enough free space for already reserved Epic downloads".into()); + } + if state.schedule.retained > 0 { + state.schedule.retained = 0; + continue; + } + if state.schedule.lookahead > 1 { + state.schedule.lookahead /= 2; + continue; + } + return Err(format!("Not enough free space during Epic install: need additional game={game} cache={cache}, free game={free_game:?} cache={free_cache:?}")); + } +} + +struct State { + schedule: Schedule, + ready: Vec<bool>, + writing: Vec<bool>, + disk_sizes: Vec<u64>, + pending: Vec<usize>, + error: Option<String>, + done: bool, +} +struct Shared { + state: Mutex<State>, + changed: Condvar, + stop: AtomicBool, +} +struct Sink<'a> { + req: &'a Request, + shared: &'a Shared, +} +impl FetchSink for Sink<'_> { + fn process(&self, item: &FetchItem, body: Vec<u8>) -> Result<u64, SinkError> { + if self.shared.stop.load(Ordering::Acquire) { + return Err(SinkError::Fatal("cancelled".into())); + } + let c = item.id as usize; + let chunk = &self.req.chunks[c]; + self.shared.state.lock().unwrap().writing[c] = true; + let written = super::chunk::write_verified_chunk_bounded( + &body, + chunk.sha.as_ref(), + &self.req.cache.join(&chunk.guid), + chunk.size, + chunk.exact, + ); + let mut state = self.shared.state.lock().unwrap(); + state.writing[c] = false; + let written = written.map_err(SinkError::Retry)?; + state.ready[c] = true; + state.disk_sizes[c] = written; + self.shared.changed.notify_all(); + Ok(chunk.wire) + } +} + +/// JNI owns this entire blocking run. All scoped workers join before completion is signalled. +pub fn run( + req: &Request, + budget: Budget, + cancel: &AtomicBool, + before_swap: &(dyn Fn() -> Result<(), String> + Sync), + progress: &(dyn Fn(u64, u64, u64, u64) + Sync), + log: &(dyn Fn(&str) + Sync), +) -> Result<u64, String> { + let shared = Shared { + state: Mutex::new(State { + schedule: Schedule::new(req, budget.lookahead, budget.retained), + ready: req.chunks.iter().map(|c| c.cached.is_some()).collect(), + writing: vec![false; req.chunks.len()], + disk_sizes: req.chunks.iter().map(|c| c.cached.unwrap_or(0)).collect(), + pending: Vec::new(), + error: None, + done: false, + }), + changed: Condvar::new(), + stop: AtomicBool::new(false), + }; + log(&format!( + "adaptive-native lookahead={} retained={} game_peak={} cache_peak={} workers={}", + budget.lookahead, + budget.retained, + budget.game_peak, + budget.cache_peak, + req.max_workers.min(budget.lookahead) + )); + let result = std::thread::scope(|scope| { + scope.spawn(|| { + crate::priority::background(); + let sink = Sink { + req, + shared: &shared, + }; + loop { + let batch = { + let mut state = shared.state.lock().unwrap(); + while state.pending.is_empty() + && !state.done + && !shared.stop.load(Ordering::Acquire) + { + state = shared + .changed + .wait_timeout(state, Duration::from_millis(100)) + .unwrap() + .0; + } + if state.done || shared.stop.load(Ordering::Acquire) { + break; + } + std::mem::take(&mut state.pending) + }; + let items = batch + .into_iter() + .map(|c| FetchItem { + id: c as u64, + urls: req + .hosts + .iter() + .map(|h| format!("{h}/{}", req.chunks[c].path)) + .collect(), + reserve: req.chunks[c].wire, + range: None, + }) + .collect(); + let opts = FetchOptions { + max_workers: req.max_workers.min(budget.lookahead), + per_host_cap: super::plan::per_host_cap( + req.max_workers.min(budget.lookahead), + req.hosts.len(), + ), + timeout: Duration::from_secs(60), + process_workers: 2, + headers: vec![("User-Agent".into(), super::USER_AGENT.into())], + label: "epic adaptive".into(), + ..Default::default() + }; + let fetched = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + run_fetch( + items, + &req.hosts, + &opts, + &sink, + &shared.stop, + &|_, _| {}, + log, + ) + })); + let outcome = match fetched { + Ok(outcome) => outcome, + Err(_) => { + let mut state = shared.state.lock().unwrap(); + state.error = Some("native fetch panicked".into()); + shared.stop.store(true, Ordering::Release); + shared.changed.notify_all(); + break; + } + }; + if !outcome.ok() { + let mut state = shared.state.lock().unwrap(); + state.error = Some(outcome.error.unwrap_or_else(|| "cancelled".into())); + shared.stop.store(true, Ordering::Release); + shared.changed.notify_all(); + break; + } + } + }); + // Cancel forwarding is independent of a writer blocked on disk I/O. + scope.spawn(|| { + while !shared.stop.load(Ordering::Acquire) { + if cancel.load(Ordering::Acquire) { + shared.stop.store(true, Ordering::Release); + shared.changed.notify_all(); + break; + } + std::thread::sleep(Duration::from_millis(50)); + } + }); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + assemble(req, &shared, cancel, before_swap, progress) + })) + .unwrap_or_else(|_| Err("native assembly panicked".into())); + shared.stop.store(true, Ordering::Release); + { + let mut state = shared.state.lock().unwrap(); + state.done = true; + shared.changed.notify_all(); + } + result + }); + // No .part writer survives this point; preserve verified resident chunks for resume. + let resident = shared.state.lock().unwrap().schedule.oldest.clone(); + for index in resident { + let _ = fs::remove_file(super::chunk::part_path( + &req.cache.join(&req.chunks[index].guid), + )); + } + if result.is_ok() { + fs::remove_dir_all(&req.cache).map_err(|e| format!("clear cache: {e}"))?; + } + result +} + +fn await_chunk( + req: &Request, + shared: &Shared, + cancel: &AtomicBool, + c: usize, + fi: usize, + pi: usize, + stage_remaining: u64, +) -> Result<(), String> { + let mut state = shared.state.lock().unwrap(); + if cancel.load(Ordering::Acquire) { + return Err("cancelled".into()); + } + live_admit(req, &mut state, fi, pi, stage_remaining)?; + let fetch = state.schedule.fill(req); + state.pending.extend(fetch); + shared.changed.notify_all(); + while !state.ready[c] { + if cancel.load(Ordering::Acquire) { + return Err("cancelled".into()); + } + if let Some(error) = &state.error { + return Err(error.clone()); + } + if shared.stop.load(Ordering::Acquire) { + return Err("cancelled".into()); + } + state = shared + .changed + .wait_timeout(state, Duration::from_millis(100)) + .unwrap() + .0; + } + Ok(()) +} + +fn assemble( + req: &Request, + shared: &Shared, + cancel: &AtomicBool, + before_swap: &(dyn Fn() -> Result<(), String> + Sync), + progress: &(dyn Fn(u64, u64, u64, u64) + Sync), +) -> Result<u64, String> { + let mut done = 0; + let mut marked = false; + let total = req.total(); + for (fi, file) in req.files.iter().enumerate() { + Request::validate_staging(&req.install, &req.staging)?; + if cancel.load(Ordering::Acquire) { + return Err("cancelled".into()); + } + let output = req.install.join(&file.path); + fs::create_dir_all(output.parent().ok_or("missing output parent")?) + .map_err(|e| e.to_string())?; + let stage = req + .staging + .join(format!("native-{}-{fi}.stage", std::process::id())); + let result = (|| { + let stage_file = OpenOptions::new() + .write(true) + .create_new(true) + .open(&stage) + .map_err(|e| e.to_string())?; + let mut out = BufWriter::with_capacity(64 * 1024, stage_file); + let mut sha = Sha1::new(); + let mut written = 0; + let file_total: u64 = file.parts.iter().map(|p| p.size).sum(); + for (pi, part) in file.parts.iter().enumerate() { + // A live free-space probe must include already buffered staging bytes. + out.flush().map_err(|e| e.to_string())?; + await_chunk( + req, + shared, + cancel, + part.chunk, + fi, + pi, + file_total - written, + )?; + written += super::assemble::copy_chunk_part( + &req.cache.join(&req.chunks[part.chunk].guid), + part.offset, + part.size, + &mut out, + &mut sha, + cancel, + )?; + let mut state = shared.state.lock().unwrap(); + for c in state.schedule.consume() { + if !state.ready[c] { + return Err("attempt to release in-flight chunk".into()); + } + fs::remove_file(req.cache.join(&req.chunks[c].guid)) + .map_err(|e| e.to_string())?; + state.ready[c] = false; + } + done += part.size; + progress(done, total, fi as u64, req.files.len() as u64); + } + out.flush().map_err(|e| e.to_string())?; + out.get_ref().sync_all().map_err(|e| e.to_string())?; + drop(out); + if written != file_total { + return Err("file size mismatch".into()); + } + if file + .sha + .is_some_and(|expected| sha.finalize().as_slice() != expected) + { + return Err(format!("SHA-1 mismatch for {}", file.path)); + } + if cancel.load(Ordering::Acquire) { + return Err("cancelled".into()); + } + if !marked { + before_swap()?; + marked = true; + } + if cancel.load(Ordering::Acquire) { + return Err("cancelled".into()); + } + // Android/Linux rename replaces atomically on the same output volume. Never delete first. + fs::rename(&stage, &output) + .map_err(|e| format!("atomic replacement {}: {e}", file.path))?; + progress(done, total, fi as u64 + 1, req.files.len() as u64); + Ok(()) + })(); + let _ = fs::remove_file(&stage); + result?; + } + Ok(done) +} + +#[cfg(test)] +mod tests { + use super::super::chunk::test_support::{build_chunk_body, sha1_of}; + use super::*; + use std::io::Read; + use std::net::TcpListener; + use std::sync::atomic::{AtomicU64, AtomicUsize}; + use std::sync::{Arc, Barrier}; + use std::thread; + use std::time::{Instant, SystemTime, UNIX_EPOCH}; + + static SEQUENCE: AtomicU64 = AtomicU64::new(0); + struct Fixture { + root: PathBuf, + req: Request, + } + impl Fixture { + fn new(count: usize, size: usize) -> Self { + let root = PathBuf::from("target").join(format!( + "epic-adaptive-{}-{}-{}", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(), + SEQUENCE.fetch_add(1, Ordering::Relaxed) + )); + let install = root.join("game"); + let cache = root.join("cache"); + let staging = install.join(".droiddeck-epic-staging"); + fs::create_dir_all(&staging).unwrap(); + fs::create_dir_all(&cache).unwrap(); + fs::write(staging.join("owner"), b"DroidDeck Epic staging v1\n").unwrap(); + let data = vec![42; size]; + let sha = sha1_of(&data); + let chunks: Vec<_> = (0..count) + .map(|c| Chunk { + guid: format!("{c:032X}"), + path: format!("chunks/{c}"), + size: size as u64, + wire: size as u64 + 41, + sha: Some(sha), + cached: None, + exact: true, + }) + .collect(); + let files = (0..count) + .map(|c| Output { + path: format!("file-{c}.bin"), + sha: Some(sha), + old: 0, + parts: vec![Part { + chunk: c, + offset: 0, + size: size as u64, + }], + }) + .collect(); + Self { + root, + req: Request { + install, + cache, + staging, + chunks, + files, + same_volume: false, + free_game: Some(u64::MAX), + free_cache: Some(u64::MAX), + hosts: vec!["http://127.0.0.1:1".into()], + max_workers: 8, + }, + } + } + } + impl Drop for Fixture { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.root); + } + } + + struct Server { + address: String, + requests: Arc<AtomicUsize>, + active: Arc<AtomicUsize>, + stop: Arc<AtomicBool>, + worker: Option<thread::JoinHandle<()>>, + } + impl Server { + fn start(data: Vec<u8>, barrier_count: usize, stalled: bool) -> Self { + Self::start_mode(data, barrier_count, stalled.then_some(usize::MAX)) + } + + fn start_mode(data: Vec<u8>, barrier_count: usize, stall_chunk: Option<usize>) -> Self { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = format!("http://{}", listener.local_addr().unwrap()); + listener.set_nonblocking(true).unwrap(); + let requests = Arc::new(AtomicUsize::new(0)); + let active = Arc::new(AtomicUsize::new(0)); + let stop = Arc::new(AtomicBool::new(false)); + let requests2 = requests.clone(); + let active2 = active.clone(); + let stop2 = stop.clone(); + let worker = thread::spawn(move || { + let body = Arc::new(build_chunk_body(&data, false, 0, None)); + let barrier = Arc::new(Barrier::new(barrier_count.max(1))); + let mut handlers = Vec::new(); + while !stop2.load(Ordering::Acquire) { + let (mut socket, _) = match listener.accept() { + Ok(s) => s, + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + thread::sleep(Duration::from_millis(5)); + continue; + } + Err(e) => panic!("{e}"), + }; + let body = body.clone(); + let requests = requests2.clone(); + let active = active2.clone(); + let stop = stop2.clone(); + let barrier = barrier.clone(); + handlers.push(thread::spawn(move || { + active.fetch_add(1, Ordering::Relaxed); + let _ = socket.set_read_timeout(Some(Duration::from_secs(5))); + let mut request = Vec::new(); + let mut byte = [0]; + while socket.read(&mut byte).unwrap_or(0) > 0 { + request.push(byte[0]); + if request.ends_with(b"\r\n\r\n") { + break; + } + } + let n = requests.fetch_add(1, Ordering::Relaxed); + if n < barrier_count { + barrier.wait(); + } + let _ = write!( + socket, + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + let stalled = stall_chunk.is_some_and(|c| { + c == usize::MAX + || String::from_utf8_lossy(&request) + .contains(&format!("/chunks/{c} ")) + }); + if stalled { + // Cancellation must close the native client's connection without data. + let _ = socket.set_read_timeout(Some(Duration::from_millis(100))); + loop { + if stop.load(Ordering::Acquire) { + break; + } + match socket.read(&mut byte) { + Ok(0) => break, + Ok(_) => {} + Err(e) + if e.kind() == std::io::ErrorKind::WouldBlock + || e.kind() == std::io::ErrorKind::TimedOut => {} + Err(_) => break, + } + } + } else { + let _ = socket.write_all(&body); + } + active.fetch_sub(1, Ordering::Relaxed); + })); + } + for handler in handlers { + handler.join().unwrap(); + } + }); + Self { + address, + requests, + active, + stop, + worker: Some(worker), + } + } + } + impl Drop for Server { + fn drop(&mut self) { + self.stop.store(true, Ordering::Release); + if let Some(worker) = self.worker.take() { + worker.join().unwrap(); + } + } + } + + #[test] + fn situation_table_selects_large_tight_minimal_and_rejects_no_fit() { + let mut f = Fixture::new(300, MIB as usize); + assert_eq!(choose_budget(&f.req).unwrap().lookahead, 128); + f.req.free_cache = Some(8 * MIB); + assert_eq!(choose_budget(&f.req).unwrap().lookahead, 8); + f.req.free_cache = Some(MIB); + assert_eq!(choose_budget(&f.req).unwrap().lookahead, 1); + f.req.free_cache = Some(MIB - 1); + assert!(choose_budget(&f.req) + .unwrap_err() + .contains("Not enough free space")); + f.req.free_cache = None; + assert_eq!(choose_budget(&f.req).unwrap().lookahead, 8); + f.req.free_game = Some(0); + assert!(choose_budget(&f.req).is_err()); + } + + #[test] + fn planner_counts_all_incoming_old_outputs_and_existing_cache_release() { + let mut f = Fixture::new(20, 100); + f.req.same_volume = true; + for file in &mut f.req.files { + file.old = 100; + } + assert_eq!(peaks(&f.req, 8, 0), (900, 800)); + for chunk in &mut f.req.chunks { + chunk.cached = Some(100); + } + assert_eq!(peaks(&f.req, 8, 0), (100, 0)); + f.req.same_volume = false; + assert_eq!(peaks(&f.req, 8, 0), (100, 0)); + } + + #[test] + fn shared_references_deduplicate_and_distant_chunks_refetch_under_budget() { + let mut f = Fixture::new(12, (40 * MIB) as usize); + f.req.files.extend(f.req.files.clone()); + let mut schedule = Schedule::new(&f.req, 8, RETAINED); + assert_eq!(schedule.fill(&f.req), (0..8).collect::<Vec<_>>()); + assert!(schedule.fill(&f.req).is_empty()); + assert!(schedule.consume().is_empty()); + assert_eq!(schedule.fill(&f.req), vec![8]); + assert_eq!(schedule.consume(), vec![0]); + let mut refetched = false; + for _ in 2..24 { + refetched |= schedule.fill(&f.req).contains(&0); + assert!(schedule.bytes() <= RETAINED + 8 * 40 * MIB); + let removed = schedule.consume(); + assert!(removed.iter().all(|c| !schedule.window().contains(c))); + } + assert!(refetched); + assert_eq!(schedule.bytes(), 0); + } + + #[test] + fn native_fetch_overlaps_across_files_and_never_fetches_outside_window() { + let mut f = Fixture::new(30, 4096); + let server = Server::start(vec![42; 4096], 8, false); + f.req.hosts = vec![server.address.clone()]; + f.req.free_cache = Some(8 * 4096); + let budget = choose_budget(&f.req).unwrap(); + assert_eq!(budget.lookahead, 8); + let cancel = AtomicBool::new(false); + let swapped = AtomicUsize::new(0); + let progress = |_, _, files, _| { + if files == 0 { + cancel.store(true, Ordering::Release); + } + }; + let result = run( + &f.req, + budget, + &cancel, + &|| { + swapped.fetch_add(1, Ordering::Relaxed); + Ok(()) + }, + &progress, + &|_| {}, + ); + assert!(result.is_err()); + assert_eq!(server.requests.load(Ordering::Relaxed), 8); + assert_eq!(swapped.load(Ordering::Relaxed), 0); + assert_eq!(fs::read_dir(&f.req.staging).unwrap().count(), 1); + assert!(fs::read_dir(&f.req.cache).unwrap().all(|e| !e + .unwrap() + .path() + .to_string_lossy() + .ends_with(".part"))); + } + + #[test] + fn native_success_stages_verifies_swaps_and_reclaims_both_cache_layouts() { + for same_volume in [false, true] { + let mut f = Fixture::new(20, 4096); + let server = Server::start(vec![42; 4096], 8, false); + f.req.hosts = vec![server.address.clone()]; + f.req.same_volume = same_volume; + if same_volume { + f.req.cache = f.req.install.join(".chunks"); + fs::create_dir_all(&f.req.cache).unwrap(); + } + for file in &mut f.req.files { + fs::write(f.req.install.join(&file.path), b"old").unwrap(); + file.old = 3; + } + let swaps = AtomicUsize::new(0); + let before = || { + assert_eq!(fs::read(f.req.install.join("file-0.bin")).unwrap(), b"old"); + swaps.fetch_add(1, Ordering::Relaxed); + Ok(()) + }; + assert_eq!( + run( + &f.req, + choose_budget(&f.req).unwrap(), + &AtomicBool::new(false), + &before, + &|_, _, _, _| {}, + &|_| {} + ) + .unwrap(), + 20 * 4096 + ); + assert_eq!(swaps.load(Ordering::Relaxed), 1); + assert!(!f.req.cache.exists()); + for file in &f.req.files { + assert_eq!( + fs::read(f.req.install.join(&file.path)).unwrap(), + vec![42; 4096] + ); + } + assert_eq!(fs::read_dir(&f.req.staging).unwrap().count(), 1); + } + } + + #[test] + fn native_file_hash_failure_and_updating_failure_keep_old_output() { + for updating_failure in [false, true] { + let mut f = Fixture::new(1, 4096); + let server = Server::start(vec![42; 4096], 1, false); + f.req.hosts = vec![server.address.clone()]; + fs::write(f.req.install.join("file-0.bin"), b"old").unwrap(); + if !updating_failure { + f.req.files[0].sha = Some([1; 20]); + } + let before = || Err("updating record failed".into()); + let result = run( + &f.req, + choose_budget(&f.req).unwrap(), + &AtomicBool::new(false), + &before, + &|_, _, _, _| {}, + &|_| {}, + ); + assert!(result.is_err()); + assert_eq!(fs::read(f.req.install.join("file-0.bin")).unwrap(), b"old"); + assert_eq!(fs::read_dir(&f.req.staging).unwrap().count(), 1); + } + } + + #[test] + fn native_cancellation_closes_stalled_parallel_requests_and_joins_before_return() { + let mut f = Fixture::new(30, 4096); + let server = Server::start(vec![42; 4096], 8, true); + f.req.hosts = vec![server.address.clone()]; + f.req.free_cache = Some(8 * 4096); + let cancel = AtomicBool::new(false); + thread::scope(|scope| { + scope.spawn(|| { + let until = Instant::now() + Duration::from_secs(5); + while server.requests.load(Ordering::Relaxed) < 8 && Instant::now() < until { + thread::sleep(Duration::from_millis(10)); + } + cancel.store(true, Ordering::Release); + }); + let started = Instant::now(); + assert!(run( + &f.req, + choose_budget(&f.req).unwrap(), + &cancel, + &|| Ok(()), + &|_, _, _, _| {}, + &|_| {} + ) + .is_err()); + assert!(started.elapsed() < Duration::from_secs(7)); + }); + let until = Instant::now() + Duration::from_secs(2); + while server.active.load(Ordering::Relaxed) > 0 && Instant::now() < until { + thread::sleep(Duration::from_millis(10)); + } + assert_eq!(server.active.load(Ordering::Relaxed), 0); + assert_eq!(server.requests.load(Ordering::Relaxed), 8); + assert_eq!(fs::read_dir(&f.req.staging).unwrap().count(), 1); + assert_eq!(fs::read_dir(&f.req.cache).unwrap().count(), 0); + } + + #[test] + fn native_bounded_decode_never_publishes_oversized_or_truncated_chunks() { + let f = Fixture::new(1, 10); + let path = f.req.cache.join("chunk"); + let body = build_chunk_body(&[42; 11], true, 0, None); + assert!( + super::super::chunk::write_verified_chunk_bounded(&body, None, &path, 10, true) + .is_err() + ); + assert!(!path.exists()); + assert!(!super::super::chunk::part_path(&path).exists()); + let body = build_chunk_body(&[42; 10], false, 0, Some(11)); + assert!( + super::super::chunk::write_verified_chunk_bounded(&body, None, &path, 10, true) + .is_err() + ); + let data = vec![42; 512 * 1024]; + let mut compressed = build_chunk_body(&data, true, 0, None); + assert_eq!( + super::super::chunk::write_verified_chunk_bounded( + &compressed, + Some(&sha1_of(&data)), + &path, + data.len() as u64, + true + ) + .unwrap(), + data.len() as u64 + ); + fs::remove_file(&path).unwrap(); + compressed.pop(); + // A matching shorter payload header leaves the missing zlib stream end to detect failure. + let short = (compressed.len() - 41) as i32; + compressed[12..16].copy_from_slice(&short.to_le_bytes()); + assert!(super::super::chunk::write_verified_chunk_bounded( + &compressed, + None, + &path, + data.len() as u64, + false + ) + .is_err()); + assert!(!path.exists()); + } + + #[test] + fn live_admission_shrinks_unsent_work_but_never_withdraws_inflight_reservations() { + let f = Fixture::new(30, 4096); + let mut state = State { + schedule: Schedule::new(&f.req, 128, RETAINED), + ready: vec![false; 30], + writing: vec![false; 30], + disk_sizes: vec![0; 30], + pending: vec![], + error: None, + done: false, + }; + admit_with_probes( + &f.req, + &mut state, + 0, + 0, + 4096, + 0, + Some(u64::MAX), + Some(8 * 4096), + ) + .unwrap(); + assert_eq!(state.schedule.lookahead, 8); + assert_eq!(state.schedule.retained, 0); + assert_eq!(state.schedule.fill(&f.req).len(), 8); + assert!(admit_with_probes( + &f.req, + &mut state, + 0, + 0, + 4096, + 0, + Some(u64::MAX), + Some(4096) + ) + .is_err()); + assert_eq!(state.schedule.lookahead, 8); + assert!(state + .schedule + .oldest + .iter() + .all(|&c| state.schedule.resident[c].is_some())); + } + + #[test] + fn snapshot_schema_rejects_unowned_staging_bad_paths_and_bad_version() { + let f = Fixture::new(1, 10); + let mut snapshot = serde_json::json!({ + "schema_version": 1, + "install_dir": f.req.install, "cache_dir": f.req.cache, "staging_dir": f.req.staging, + "same_volume": false, "free_game": -1, "free_cache": 0, "max_workers": 32, + "hosts": ["http://127.0.0.1"], + "chunks": [{ "guid": "00000000000000000000000000000000", "path": "chunks/00/chunk", + "size": 10, "wire_size": 51, "sha1": "", "cached": -1, "exact_size": true }], + "files": [{ "path": "legitimate.part", "old_size": 0, "sha1": "", + "parts": [{ "guid": "00000000000000000000000000000000", "offset": 0, "size": 10 }] }] + }); + let parsed = Request::parse(&snapshot.to_string()).unwrap(); + assert_eq!( + parsed.free_game, + if parsed.same_volume { Some(0) } else { None } + ); + assert_eq!(parsed.free_cache, Some(0)); + assert_eq!(parsed.chunks[0].cached, None); + assert!(choose_budget(&parsed).is_err()); + snapshot["schema_version"] = 2.into(); + assert!(Request::parse(&snapshot.to_string()).is_err()); + snapshot["schema_version"] = 1.into(); + snapshot["files"][0]["path"] = "../outside".into(); + assert!(Request::parse(&snapshot.to_string()).is_err()); + snapshot["files"][0]["path"] = ".droiddeck-epic-staging/owner".into(); + assert!(Request::parse(&snapshot.to_string()).is_err()); + snapshot["files"][0]["path"] = "legitimate.part".into(); + fs::remove_file(f.req.staging.join("owner")).unwrap(); + assert!(Request::parse(&snapshot.to_string()).is_err()); + } + + #[test] + fn native_shared_chunk_survives_cancelled_file_boundary_and_is_reused_on_resume() { + let mut f = Fixture::new(3, 4096); + let server = Server::start(vec![42; 4096], 1, false); + f.req.hosts = vec![server.address.clone()]; + for file in &mut f.req.files { + file.parts[0].chunk = 0; + } + let cancel = AtomicBool::new(false); + let before = AtomicUsize::new(0); + let progress = |_, _, files, _| { + if files == 1 { + cancel.store(true, Ordering::Release); + } + }; + assert!(run( + &f.req, + choose_budget(&f.req).unwrap(), + &cancel, + &|| { + before.fetch_add(1, Ordering::Relaxed); + Ok(()) + }, + &progress, + &|_| {} + ) + .is_err()); + assert_eq!(before.load(Ordering::Relaxed), 1); + assert_eq!(server.requests.load(Ordering::Relaxed), 1); + assert_eq!( + fs::read(f.req.install.join("file-0.bin")).unwrap(), + vec![42; 4096] + ); + assert!(!f.req.install.join("file-1.bin").exists()); + assert!(f.req.cache.join(&f.req.chunks[0].guid).is_file()); + f.req.files.remove(0); + f.req.chunks[0].cached = Some(4096); + assert_eq!( + run( + &f.req, + choose_budget(&f.req).unwrap(), + &AtomicBool::new(false), + &|| Ok(()), + &|_, _, _, _| {}, + &|_| {} + ) + .unwrap(), + 8192 + ); + assert_eq!(server.requests.load(Ordering::Relaxed), 1); + assert!(!f.req.cache.exists()); + } + + #[test] + fn native_later_hash_failure_preserves_verified_swap_and_unreplaced_old_file() { + let mut f = Fixture::new(2, 4096); + let server = Server::start(vec![42; 4096], 2, false); + f.req.hosts = vec![server.address.clone()]; + for file in &mut f.req.files { + fs::write(f.req.install.join(&file.path), b"old").unwrap(); + file.old = 3; + } + f.req.files[1].sha = Some([1; 20]); + let marked = AtomicUsize::new(0); + let error = run( + &f.req, + choose_budget(&f.req).unwrap(), + &AtomicBool::new(false), + &|| { + marked.fetch_add(1, Ordering::Relaxed); + Ok(()) + }, + &|_, _, _, _| {}, + &|_| {}, + ) + .unwrap_err(); + assert!(error.contains("SHA-1 mismatch")); + assert_eq!(marked.load(Ordering::Relaxed), 1); + assert_eq!( + fs::read(f.req.install.join("file-0.bin")).unwrap(), + vec![42; 4096] + ); + assert_eq!(fs::read(f.req.install.join("file-1.bin")).unwrap(), b"old"); + assert_eq!(fs::read_dir(&f.req.staging).unwrap().count(), 1); + } + + #[test] + fn actual_native_file_and_cache_growth_never_exceeds_the_shared_planned_peak() { + let mut f = Fixture::new(20, 4096); + let server = Server::start(vec![42; 4096], 8, false); + f.req.hosts = vec![server.address.clone()]; + f.req.same_volume = true; + f.req.free_game = Some(21 * 4096); + for file in &mut f.req.files { + file.old = 4096; + fs::write(f.req.install.join(&file.path), vec![1; 4096]).unwrap(); + } + let budget = choose_budget(&f.req).unwrap(); + let observed = AtomicU64::new(0); + let progress = |_, _, _, _| { + let game: u64 = f + .req + .files + .iter() + .map(|file| fs::metadata(f.req.install.join(&file.path)).unwrap().len()) + .sum(); + let stage: u64 = fs::read_dir(&f.req.staging) + .unwrap() + .filter_map(Result::ok) + .filter(|e| e.file_name() != "owner") + .map(|e| e.metadata().map(|m| m.len()).unwrap_or(0)) + .sum(); + let cache: u64 = fs::read_dir(&f.req.cache) + .unwrap() + .filter_map(Result::ok) + .map(|e| e.metadata().map(|m| m.len()).unwrap_or(0)) + .sum(); + let additional = (game + stage + cache).saturating_sub(20 * 4096); + observed.fetch_max(additional, Ordering::Relaxed); + assert!( + additional <= budget.game_peak, + "{additional} > planned {}", + budget.game_peak + ); + }; + assert_eq!( + run( + &f.req, + budget, + &AtomicBool::new(false), + &|| Ok(()), + &progress, + &|_| {} + ) + .unwrap(), + 20 * 4096 + ); + assert!(observed.load(Ordering::Relaxed) > 8 * 4096); + } + + #[test] + fn native_assembly_swaps_a_verified_file_while_a_future_chunk_is_still_fetching() { + let mut f = Fixture::new(20, 4096); + let server = Server::start_mode(vec![42; 4096], 8, Some(7)); + f.req.hosts = vec![server.address.clone()]; + f.req.free_cache = Some(8 * 4096); + let cancel = AtomicBool::new(false); + let overlap = AtomicBool::new(false); + let progress = |_, _, files, _| { + if files == 1 { + assert!(server.active.load(Ordering::Relaxed) > 0); + assert_eq!( + fs::read(f.req.install.join("file-0.bin")).unwrap(), + vec![42; 4096] + ); + overlap.store(true, Ordering::Release); + cancel.store(true, Ordering::Release); + } + }; + assert!(run( + &f.req, + choose_budget(&f.req).unwrap(), + &cancel, + &|| Ok(()), + &progress, + &|_| {} + ) + .is_err()); + assert!(overlap.load(Ordering::Acquire)); + assert_eq!(server.requests.load(Ordering::Relaxed), 8); + assert_eq!(fs::read_dir(&f.req.staging).unwrap().count(), 1); + } + + #[cfg(unix)] + #[test] + fn staging_validation_rejects_symlink_namespace_owner_and_entries() { + use std::os::unix::fs::symlink; + let f = Fixture::new(1, 4096); + let outside = f.root.join("outside"); + fs::create_dir(&outside).unwrap(); + fs::write(outside.join("owner"), b"DroidDeck Epic staging v1\n").unwrap(); + fs::write(outside.join("keep.stage"), b"keep").unwrap(); + fs::remove_dir_all(&f.req.staging).unwrap(); + symlink(fs::canonicalize(&outside).unwrap(), &f.req.staging).unwrap(); + assert!(Request::validate_staging(&f.req.install, &f.req.staging).is_err()); + assert!(run( + &f.req, + choose_budget(&f.req).unwrap(), + &AtomicBool::new(false), + &|| Ok(()), + &|_, _, _, _| {}, + &|_| {} + ) + .is_err()); + assert_eq!(fs::read(outside.join("keep.stage")).unwrap(), b"keep"); + fs::remove_file(&f.req.staging).unwrap(); + fs::create_dir(&f.req.staging).unwrap(); + symlink( + fs::canonicalize(outside.join("owner")).unwrap(), + f.req.staging.join("owner"), + ) + .unwrap(); + assert!(Request::validate_staging(&f.req.install, &f.req.staging).is_err()); + fs::remove_file(f.req.staging.join("owner")).unwrap(); + fs::write(f.req.staging.join("owner"), b"DroidDeck Epic staging v1\n").unwrap(); + symlink( + fs::canonicalize(outside.join("keep.stage")).unwrap(), + f.req.staging.join("linked.stage"), + ) + .unwrap(); + assert!(Request::validate_staging(&f.req.install, &f.req.staging).is_err()); + assert_eq!(fs::read(outside.join("keep.stage")).unwrap(), b"keep"); + fs::remove_file(f.req.staging.join("linked.stage")).unwrap(); + } +} diff --git a/app/src/main/rust/stores/src/epic/assemble.rs b/app/src/main/rust/stores/src/epic/assemble.rs index 580cb8c24..8094d64fc 100644 --- a/app/src/main/rust/stores/src/epic/assemble.rs +++ b/app/src/main/rust/stores/src/epic/assemble.rs @@ -1,30 +1,7 @@ -//! 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}; -use std::io::{BufWriter, Write}; +use std::io::{BufWriter, Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -33,33 +10,52 @@ 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. +/// Streaming assembly primitive used by the budgeted installer; never holds a decoded chunk +/// in memory and closes the reader before the caller releases the cache reservation. +pub fn copy_chunk_part( + cache_path: &Path, offset: u64, size: u64, output: &mut impl Write, + sha: &mut Sha1, cancel: &AtomicBool, +) -> Result<u64, String> { + let mut input = File::open(cache_path).map_err(|e| format!("read chunk: {e}"))?; + let length = input.metadata().map_err(|e| e.to_string())?.len(); + if offset.checked_add(size).filter(|end| *end <= length).is_none() { + return Err("chunk part exceeds cached data".into()); + } + input.seek(SeekFrom::Start(offset)).map_err(|e| e.to_string())?; + let mut left = size; + let mut buffer = [0u8; 64 * 1024]; + while left > 0 { + if cancel.load(Ordering::Acquire) { return Err("cancelled".into()); } + let take = left.min(buffer.len() as u64) as usize; + let count = input.read(&mut buffer[..take]) + .map_err(|e| e.to_string())?; + if count == 0 { return Err("short cached chunk".into()); } + output.write_all(&buffer[..count]).map_err(|e| e.to_string())?; + sha.update(&buffer[..count]); + left -= count as u64; + } + Ok(size) +} + 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 +64,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 +97,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 +110,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 +121,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 +147,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 +154,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 +202,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 +251,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 +275,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 +313,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 +401,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..e07c26c8c 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,15 +67,18 @@ 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>, written: u64, + limit: u64, } impl<W: Write> Write for HashingWriter<W> { fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + if self.written.saturating_add(buf.len() as u64) > self.limit { + return Err(io::Error::other("chunk exceeds reserved decoded window")); + } let n = self.inner.write(buf)?; if let Some(sha) = self.sha.as_mut() { sha.update(&buf[..n]); @@ -110,29 +92,35 @@ 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); + let result = write_part(body, expected_sha1, &tmp, final_path, u64::MAX, false); if result.is_err() { let _ = fs::remove_file(&tmp); } + + result +} + +pub fn write_verified_chunk_bounded( + body: &[u8], expected_sha1: Option<&[u8; 20]>, final_path: &Path, + decoded_limit: u64, exact_size: bool, +) -> Result<u64, String> { + let tmp = part_path(final_path); + let result = write_part(body, expected_sha1, &tmp, final_path, decoded_limit, exact_size); + if result.is_err() { let _ = fs::remove_file(tmp); } result } @@ -141,30 +129,53 @@ fn write_part( expected_sha1: Option<&[u8; 20]>, tmp: &Path, final_path: &Path, + limit: u64, + exact_size: bool, ) -> Result<u64, String> { let hdr = parse_chunk_header(body)?; let payload = chunk_payload(body, &hdr)?; + if limit != u64::MAX && (hdr.compressed_size < 0 || payload.len() != hdr.compressed_size as usize) { + return Err("truncated chunk body".into()); + } let file = File::create(tmp).map_err(|e| format!("open {}: {e}", tmp.display()))?; let mut out = HashingWriter { inner: BufWriter::with_capacity(128 * 1024, file), sha: expected_sha1.map(|_| Sha1::new()), written: 0, + limit, }; 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}"))?; + if limit == u64::MAX { + let mut dec = flate2::read::ZlibDecoder::new(payload); + io::copy(&mut dec, &mut out).map_err(|e| format!("inflate: {e}"))?; + } else { + let mut dec = flate2::Decompress::new(true); + let mut buffer = [0u8; 128 * 1024]; + loop { + let input = dec.total_in() as usize; + let before = dec.total_out(); + let status = dec.decompress(&payload[input..], &mut buffer, flate2::FlushDecompress::None) + .map_err(|e| format!("inflate: {e}"))?; + let count = (dec.total_out() - before) as usize; + out.write_all(&buffer[..count]).map_err(|e| format!("inflate write: {e}"))?; + if status == flate2::Status::StreamEnd { break; } + if count == 0 && dec.total_in() as usize == input { + return Err("truncated compressed Epic chunk".into()); + } + } + } } else { out.write_all(payload).map_err(|e| format!("write: {e}"))?; } 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 exact_size && written != limit { + return Err("chunk decoded size mismatch".into()); + } if let (Some(expected), Some(actual)) = (expected_sha1, digest) { if actual.as_slice() != &expected[..] { @@ -172,16 +183,88 @@ 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) } +/// The window a chunk decompresses to: the manifest's, or 1 MiB for a legacy JSON manifest that +/// names none (`EpicDownloadManager.chunkWindowSize`). The streaming installer also plans its +/// shared-chunk cache with this size. +pub fn chunk_window_bytes(window_size: i32) -> u64 { + if window_size > 0 { + window_size as u64 + } else { + 1024 * 1024 + } +} + +/// One downloaded body → the verified, decompressed chunk in memory, for the streaming installer +/// (`stream.rs`), which writes most chunks straight into the files that use them and never puts +/// them in a cache. The checks are the bounded Java writer's (`downloadChunkStreaming`), which is +/// stricter than [`write_verified_chunk`]: a stored payload shorter than `compressedSize` is +/// truncated, a zlib stream must reach its end, the output may not grow past the chunk's window +/// and must fill it exactly when the manifest names one, and the SHA-1 must match when the +/// manifest has one. `Err` = "try another CDN", exactly like a cache write that failed. +pub fn decode_verified_chunk( + body: &[u8], + window_size: i32, + expected_sha1: Option<&[u8; 20]>, +) -> Result<Vec<u8>, String> { + let hdr = parse_chunk_header(body)?; + let start = hdr.payload_start(); + if start > body.len() { + return Err("Stream ended during skip".to_string()); + } + let want = if hdr.compressed_size <= 0 { + 0 + } else { + hdr.compressed_size as usize + }; + let avail = body.len() - start; + let cap = chunk_window_bytes(window_size) as usize; + let out = if hdr.is_compressed() { + // Java stops reading once the inflater finishes, so a body may carry less than + // `compressedSize` as long as the stream itself is complete. + let payload = &body[start..start + want.min(avail)]; + let mut out: Vec<u8> = Vec::with_capacity(cap + 1); + let mut inflater = flate2::Decompress::new(true); + let status = inflater + .decompress_vec(payload, &mut out, flate2::FlushDecompress::Finish) + .map_err(|e| format!("inflate: {e}"))?; + if out.len() > cap { + return Err("chunk exceeds expected window size".to_string()); + } + if status != flate2::Status::StreamEnd { + return Err("truncated compressed Epic chunk".to_string()); + } + out + } else { + if avail < want { + return Err("truncated Epic chunk".to_string()); + } + if want > cap { + return Err("chunk exceeds expected window size".to_string()); + } + body[start..start + want].to_vec() + }; + if window_size > 0 && out.len() != window_size as usize { + return Err(format!( + "chunk is {} bytes, window is {window_size}", + out.len() + )); + } + if let Some(expected) = expected_sha1 { + if Sha1::digest(&out).as_slice() != &expected[..] { + return Err("Chunk SHA-1 mismatch (streaming)".to_string()); + } + } + Ok(out) +} + #[cfg(test)] 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 +282,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 +363,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 +427,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; } @@ -361,12 +441,35 @@ mod tests { let _ = fs::remove_dir_all(&dir); } + #[test] + fn decode_in_memory_applies_the_bounded_writer_checks() { + let data = sample_data(); + let window = data.len() as i32; + let sha = sha1_of(&data); + let zipped = build_chunk_body(&data, true, 2, None); + assert_eq!(decode_verified_chunk(&zipped, window, Some(&sha)).unwrap(), data); + let stored = build_chunk_body(&data, false, 0, None); + assert_eq!(decode_verified_chunk(&stored, window, Some(&sha)).unwrap(), data); + // No window and no hash (legacy JSON manifest): accepted up to 1 MiB. + assert_eq!(decode_verified_chunk(&zipped, 0, None).unwrap(), data); + // Window mismatch, SHA-1 mismatch, truncation, oversize. + assert!(decode_verified_chunk(&zipped, window + 1, None).is_err()); + let err = decode_verified_chunk(&zipped, window, Some(&[7; 20])).unwrap_err(); + assert!(err.contains("SHA-1 mismatch"), "{err}"); + let cut = zipped.len() - 500; + assert!(decode_verified_chunk(&zipped[..cut], window, None).is_err()); + assert!(decode_verified_chunk(&stored[..stored.len() - 1], 0, None) + .unwrap_err() + .contains("truncated")); + assert!(decode_verified_chunk(&zipped, window - 1, None).is_err()); + } + #[test] fn stale_part_file_is_cleared_before_the_attempt() { 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..29edbe4b7 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; @@ -31,6 +9,7 @@ use jni::{JNIEnv, JavaVM}; use super::assemble::{build_assemble_plan, run_assemble, AssembleRequest}; use super::driver::{build_plan, run_plan, EpicRequest}; +use super::stream::{build_stream_plan, run_stream, Hooks, StreamProgress, StreamRequest}; #[cfg(target_os = "android")] #[link(name = "log")] @@ -59,10 +38,9 @@ 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>, + worker: Option<thread::JoinHandle<()>>, } fn clear_pending_exception(env: &mut JNIEnv) { @@ -200,7 +178,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); @@ -314,12 +291,11 @@ pub extern "system" fn Java_com_droiddeck_launcher_stores_epic_EpicNative_native let cancel = Arc::new(AtomicBool::new(false)); let thread_cancel = Arc::clone(&cancel); - let handle = Box::new(EpicDownloadHandle { cancel }); - - thread::spawn(move || { + let worker = thread::spawn(move || { crate::priority::background(); run_on_thread(vm, listener, plan, req, thread_cancel); }); + let handle = Box::new(EpicDownloadHandle { cancel, worker: Some(worker) }); Box::into_raw(handle) as jlong } @@ -334,8 +310,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 +345,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, @@ -425,9 +394,7 @@ pub extern "system" fn Java_com_droiddeck_launcher_stores_epic_EpicNative_native let cancel = Arc::new(AtomicBool::new(false)); let thread_cancel = Arc::clone(&cancel); - let handle = Box::new(EpicDownloadHandle { cancel }); - - thread::spawn(move || { + let worker = thread::spawn(move || { crate::priority::background(); let progress = |bytes_done: u64, bytes_total: u64, files_done: u64, files_total: u64| { let Ok(mut env) = vm.attach_current_thread_as_daemon() else { @@ -454,6 +421,194 @@ pub extern "system" fn Java_com_droiddeck_launcher_stores_epic_EpicNative_native outcome.bytes_written, ); }); + let handle = Box::new(EpicDownloadHandle { cancel, worker: Some(worker) }); + + Box::into_raw(handle) as jlong +} + +fn call_on_stream_progress(env: &mut JNIEnv, listener: &JObject, p: &StreamProgress) { + if listener.is_null() { + return; + } + let _ = env.call_method( + listener, + "onStreamProgress", + "(JJIIJ)V", + &[ + JValue::Long(p.bytes_written as jlong), + JValue::Long(p.bytes_total as jlong), + JValue::Int(p.files_done.min(i32::MAX as u64) as jint), + JValue::Int(p.files_total.min(i32::MAX as u64) as jint), + JValue::Long(p.files_bytes as jlong), + ], + ); + clear_pending_exception(env); +} + +/// `boolean onBeforeReplace()`: the Java side marks an existing install `updating`. A thrown +/// exception or `false` is a refusal. +fn call_on_before_replace(env: &mut JNIEnv, listener: &JObject) -> Result<(), String> { + if listener.is_null() { + return Ok(()); + } + let result = env + .call_method(listener, "onBeforeReplace", "()Z", &[]) + .and_then(|v| v.z()); + let threw = env.exception_check().unwrap_or(false); + clear_pending_exception(env); + match result { + Ok(true) if !threw => Ok(()), + _ => Err("could not mark the install as updating".to_string()), + } +} + +/// `EpicNative.nativeStream(...)`: the adaptive install (see `stream.rs`). Parse, plan and cache +/// preparation run on the calling thread, so a refusal returns 0 (after a synchronous +/// `onComplete(false, reason, 0)`) before anything is fetched or written, and the manager runs +/// the bounded Java writer instead. The run itself is on a new thread; `onComplete` fires after +/// every unfinished staging file of a cancelled or failed run is gone. +#[no_mangle] +pub extern "system" fn Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeStream( + mut env: JNIEnv, + _class: JClass, + manifest: JByteArray, + install_dir: JString, + chunk_cache_dir: JString, + staging_dir: JString, + cdn_prefixes: JObjectArray, + pending_file_idx: JIntArray, + expected_bytes: jlong, + cache_budget: jlong, + overlap_budget: jlong, + ca_bundle_path: JString, + max_workers: jint, + process_workers: jint, + listener: JObject, +) -> jlong { + if listener.is_null() { + android_log("nativeStream: null listener"); + return 0; + } + let Ok(manifest_bytes) = env.convert_byte_array(manifest) else { + clear_pending_exception(&mut env); + call_on_complete(&mut env, &listener, false, "manifest bytes unreadable", 0); + return 0; + }; + let install_dir = jstring_to_string(&mut env, &install_dir).unwrap_or_default(); + let chunk_cache_dir = jstring_to_string(&mut env, &chunk_cache_dir).unwrap_or_default(); + let staging_dir = jstring_to_string(&mut env, &staging_dir).unwrap_or_default(); + let cdn_prefixes = string_array_to_vec(&mut env, &cdn_prefixes); + let pending_file_indices: Vec<usize> = int_array_to_vec(&env, &pending_file_idx) + .into_iter() + .map(|i| i.max(0) as usize) + .collect(); + let ca_bundle_path = jstring_to_string(&mut env, &ca_bundle_path).unwrap_or_default(); + let max_workers = max_workers.max(1) as usize; + let process_workers = process_workers.max(1) as usize; + let label = { + let base = std::path::Path::new(&install_dir) + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_default(); + format!("epic app={base}") + }; + log_both( + &mut env, + &listener, + &format!( + "engine=rust-stream label=\"{label}\" install_dir={install_dir} cache_dir={} cdns={} pending_files={} cache_budget={} workers={max_workers} process_workers={process_workers} manifest_bytes={}", + if chunk_cache_dir.is_empty() { "<installDir>/.chunks" } else { chunk_cache_dir.as_str() }, + cdn_prefixes.len(), + pending_file_indices.len(), + cache_budget.max(0), + manifest_bytes.len() + ), + ); + + let req = StreamRequest { + manifest_bytes, + install_dir, + chunk_cache_dir, + staging_dir, + cdn_prefixes, + pending_file_indices, + expected_bytes: if expected_bytes >= 0 { + Some(expected_bytes as u64) + } else { + None + }, + cache_budget: cache_budget.max(0) as u64, + // Negative = no limit (nothing the manager knows of is being replaced). + replace_overlap_budget: if overlap_budget < 0 { + u64::MAX + } else { + overlap_budget as u64 + }, + ca_bundle_path, + max_workers, + process_workers, + label, + }; + let plan = match build_stream_plan(&req) { + Ok(plan) => plan, + Err(err) => { + log_both(&mut env, &listener, &format!("not started: {err}")); + call_on_complete(&mut env, &listener, false, &err, 0); + return 0; + } + }; + let Ok(vm) = env.get_java_vm() else { + clear_pending_exception(&mut env); + call_on_complete(&mut env, &listener, false, "JavaVM unavailable", 0); + return 0; + }; + let Ok(listener) = env.new_global_ref(&listener) else { + clear_pending_exception(&mut env); + call_on_complete(&mut env, &listener, false, "listener ref failed", 0); + return 0; + }; + + let cancel = Arc::new(AtomicBool::new(false)); + let thread_cancel = Arc::clone(&cancel); + let worker = thread::spawn(move || { + crate::priority::background(); + let progress = |p: &StreamProgress| { + let Ok(mut env) = vm.attach_current_thread_as_daemon() else { + return; + }; + call_on_stream_progress(&mut env, listener.as_obj(), p); + }; + let before_replace = || { + let Ok(mut env) = vm.attach_current_thread_as_daemon() else { + return Err("JavaVM attach failed".to_string()); + }; + call_on_before_replace(&mut env, listener.as_obj()) + }; + let log = |line: &str| { + android_log(line); + let Ok(mut env) = vm.attach_current_thread_as_daemon() else { + return; + }; + call_on_log(&mut env, listener.as_obj(), line); + }; + let hooks = Hooks { + progress: &progress, + before_replace: &before_replace, + log: &log, + }; + let outcome = run_stream(&plan, &req, thread_cancel.as_ref(), &hooks); + let Ok(mut env) = vm.attach_current_thread_as_daemon() else { + return; + }; + call_on_complete( + &mut env, + listener.as_obj(), + outcome.success, + &outcome.error, + outcome.bytes_written, + ); + }); + let handle = Box::new(EpicDownloadHandle { cancel, worker: Some(worker) }); Box::into_raw(handle) as jlong } @@ -481,7 +636,73 @@ 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) }); + let mut handle = unsafe { Box::from_raw(handle as *mut EpicDownloadHandle) }; + if let Some(worker) = handle.worker.take() { let _ = worker.join(); } +} + +/// ABI v1 snapshot: selected chunks/files, verified cache lengths, old output lengths and both +/// measured free-space probes. Parse/budget failures are terminal, never fallback requests. +#[no_mangle] +pub extern "system" fn Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeInstallAdaptive( + mut env: JNIEnv, _class: JClass, request: JString, listener: JObject, +) -> jlong { + if listener.is_null() { return 0; } + let parsed = jstring_to_string(&mut env, &request).ok_or("unreadable adaptive request".to_string()) + .and_then(|s| super::adaptive::Request::parse(&s)) + .and_then(|req| super::adaptive::choose_budget(&req).map(|budget| (req, budget))); + let (req, budget) = match parsed { + Ok(plan) => plan, + Err(error) => { + call_on_complete(&mut env, &listener, false, &error, 0); + return 0; + } + }; + call_on_plan(&mut env, &listener, req.chunks.len() as u64, 0, + &format!("lookahead={} retained={} game_peak={} cache_peak={}", + budget.lookahead, budget.retained, budget.game_peak, budget.cache_peak)); + let Ok(vm) = env.get_java_vm() else { + call_on_complete(&mut env, &listener, false, "JavaVM unavailable", 0); + return 0; + }; + let Ok(listener) = env.new_global_ref(&listener) else { + call_on_complete(&mut env, &listener, false, "listener ref failed", 0); + return 0; + }; + let cancel = Arc::new(AtomicBool::new(false)); + let thread_cancel = cancel.clone(); + let worker = thread::spawn(move || { + crate::priority::background(); + let progress = |done, total, files, files_total| { + if let Ok(mut env) = vm.attach_current_thread_as_daemon() { + call_on_progress(&mut env, listener.as_obj(), done, total, files, files_total); + } + }; + let before_swap = || -> Result<(), String> { + let mut env = vm.attach_current_thread_as_daemon().map_err(|e| e.to_string())?; + match env.call_method(listener.as_obj(), "beforeSwap", "()Z", &[]) { + Ok(value) => if value.z().unwrap_or(false) { Ok(()) } else { Err("Could not mark installed record updating before replacement".into()) }, + _ => { + clear_pending_exception(&mut env); + Err("Could not mark installed record updating before replacement".into()) + } + } + }; + let log = |line: &str| { + android_log(line); + if let Ok(mut env) = vm.attach_current_thread_as_daemon() { + call_on_log(&mut env, listener.as_obj(), line); + } + }; + let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| + super::adaptive::run(&req, budget, &thread_cancel, &before_swap, &progress, &log))) + .unwrap_or_else(|_| Err("Native Epic install panicked".into())); + let (success, error, bytes) = match outcome { + Ok(bytes) => (true, String::new(), bytes), + Err(error) => (false, error, 0), + }; + if let Ok(mut env) = vm.attach_current_thread_as_daemon() { + call_on_complete(&mut env, listener.as_obj(), success, &error, bytes); + } + }); + Box::into_raw(Box::new(EpicDownloadHandle { cancel, worker: Some(worker) })) as jlong } 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/epic/stream.rs b/app/src/main/rust/stores/src/epic/stream.rs new file mode 100644 index 000000000..5489801dd --- /dev/null +++ b/app/src/main/rust/stores/src/epic/stream.rs @@ -0,0 +1,1956 @@ +//! The adaptive Epic install: chunks fetched in parallel and written straight into the files. +//! +//! The batch engine ([`super::driver`] + [`super::assemble`]) fetched every chunk into a cache +//! first and assembled the files afterwards: fast, but the cache and the game existed side by side +//! (twice the game's size on disk) and every byte was written twice. The bounded Java writer fixed +//! the space by fetching one chunk at a time, at the cost of the parallel fetch. This module keeps +//! the fetch core's parallel, adaptive download (window ramp, per-host caps, the in-flight byte +//! budget, retries, host rotation, stall handoff — none of it changes) and keeps the bounded +//! writer's safety rules, by deciding up front what each downloaded chunk is for: +//! +//! - **A chunk exactly one file part uses** is decompressed, verified and written straight into +//! that file's staging file at the part's offset (a positioned write, so chunks may arrive in +//! any order). It never touches a cache. +//! - **A chunk several parts use** is written into every part whose file is already open, and the +//! decompressed copy is kept in the cache (`<cache>/<GUID>`, the same verified form and name the +//! batch engine and the Java writer use, so a resume on any engine reuses it) until its last +//! covered part is written; then it is deleted. Decompressed, not raw: a later part is then a +//! plain read and write with no second inflate or hash, and the file is exactly what a resume +//! already knows how to check. +//! - The cache has a **byte budget** (from the Java planner, sized from free space). The plan +//! simulates the whole install in file order and, when the budget would be exceeded, evicts the +//! cached chunk whose next use is furthest away (Belady's rule, the fewest refetches for that +//! budget). An evicted chunk is downloaded again for its later uses, as its own fetch item. +//! +//! **Files open in order.** The fetch items are queued in the order of their first use, so the +//! fetch core works through the game front to back. A file is opened (its staging file created) +//! only once the download has reached it: every file before the first chunk still to arrive, plus +//! the file of every chunk that has arrived. That keeps the set of half-written files to what the +//! fetch window spans, and it is also why the cache never holds more than the plan allowed: a +//! cached chunk is written into every newly reached file at once, so what stays in the cache is +//! exactly what the simulation kept at that point of the file order. +//! +//! **Updates are paced by a replacement-overlap budget.** A staged file that replaces an existing +//! one sits beside the old copy until its swap, so every replaced file open at once costs its full +//! new size in extra space (on a fresh install a staged file simply becomes the file). Files are +//! admitted in order; a replaced file is admitted only while the planned sizes of admitted, +//! not-yet-swapped replaced files stay within the budget the manager computed from free space, +//! and one is always admitted when none is open, so the install always progresses. The fetch core +//! dispatches a chunk only once the file of its first use is admitted ([`run_fetch_gated`]). +//! Admission never changes the order files are reached in, only when, so the cache simulation +//! below (which works in file order) stays exact: the cache holds at any moment what the plan holds +//! at that point of the order. Nothing waits on a gated file: every chunk an admitted file needs is +//! first used at or before that file, so it was dispatched before the gate closed. +//! +//! **A slow disk throttles the download by itself.** Every write happens in the fetch core's +//! process pool, inside [`FetchSink::process`]; the core frees a body's share of its in-flight +//! byte budget only when `process` returns, so while the disk is behind, the budget fills and no +//! new request goes out. Memory is that budget plus one decompressed chunk per pool thread. +//! +//! **Each file is replaced safely**, with the bounded Java writer's rules: written into a unique +//! `.stage` file in the manager's owned staging directory (`<installDir>/.droiddeck-epic-staging`, +//! whose leftovers the manager recovers before the next run; a sibling `.droiddeck-epic-*.stage` +//! when no directory is given), never a manifest path, so a game file legitimately named `x.part` +//! is safe. Its SHA-1 computed as the bytes go out (parts written out of order are +//! read back once the bytes before them are in, while they are still in the page cache), then +//! synced, its size and hash checked, the install marked `updating` once (the Java callback, +//! before the first replacement), and the stage renamed over the destination. On cancel or failure +//! every unfinished stage is removed and the old files are untouched; verified files that were +//! already swapped stay, and the next run's delta pass skips them. The cache is left as it stands +//! for that run, and removed when the install completes. + +use std::collections::{BTreeSet, HashMap, HashSet}; +use std::fs::{self, File, OpenOptions}; +use std::io::ErrorKind; +use std::os::unix::fs::FileExt; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; +use std::sync::Mutex; +use std::time::Instant; + +use sha1::{Digest, Sha1}; + +use crate::fetch_core::{run_fetch_gated, FetchItem, FetchOptions, FetchSink, SinkError}; + +use super::assemble::relative_path; +use super::chunk::{chunk_window_bytes, decode_verified_chunk, part_path}; +use super::driver::REQUEST_TIMEOUT; +use super::manifest::{parse_manifest, ChunkInfo, Manifest}; +use super::plan::{chunk_url, distinct_prefixes, per_host_cap, resolve_cache_dir}; + +/// "No further use" in the schedule's next-use table. +const NONE: usize = usize::MAX; + +/// Progress callbacks are rate-limited to one per this many milliseconds (a finished file always +/// reports): a large game has hundreds of thousands of parts, each a JNI call otherwise. +const PROGRESS_EVERY_MS: u64 = 50; + +// ───────────────────────────────────────────────────────────────────────────────────────────── +// The schedule: which download serves which file part +// ───────────────────────────────────────────────────────────────────────────────────────────── + +/// One download of a chunk (or one cached copy left by an earlier run) and the file parts it +/// serves. Positions index the install's part sequence: every pending file's parts, file by file. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ScheduledEvent { + /// Index into `manifest.unique_chunks`. + pub chunk: usize, + /// The part positions this copy serves, in order. + pub uses: Vec<usize>, + /// Already in the cache when the run started; not fetched. + pub preloaded: bool, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Schedule { + pub events: Vec<ScheduledEvent>, + /// Position → the event that serves it. + pub use_event: Vec<usize>, + /// Cached chunks left by an earlier run that this run will not use (no pending part needs + /// them, or keeping them would exceed the budget): deleted before anything is fetched. + pub discarded: Vec<usize>, + /// The most the cache holds at any point of the file order. + pub peak_cache_bytes: u64, +} + +/// Plans the downloads for the part sequence `chunk_of_use` (the chunk each part position reads) +/// with at most `budget` bytes of cached chunks (`chunk_bytes[c]` each). A chunk with one use is +/// a plain download. A chunk with more uses stays cached from one use to the next; when the cache +/// would exceed the budget, the cached chunk needed furthest in the future (possibly the one just +/// downloaded) is dropped, and its next use becomes a new download. `preloaded` chunks start in +/// the cache, the soonest needed first while they fit. +pub fn schedule( + chunk_of_use: &[usize], + chunk_bytes: &[u64], + preloaded: &[usize], + budget: u64, +) -> Schedule { + let n = chunk_of_use.len(); + let mut next_use = vec![NONE; n]; + let mut first_use: HashMap<usize, usize> = HashMap::new(); + for p in (0..n).rev() { + let c = chunk_of_use[p]; + next_use[p] = first_use.get(&c).copied().unwrap_or(NONE); + first_use.insert(c, p); + } + + let mut out = Schedule { + use_event: vec![NONE; n], + ..Schedule::default() + }; + // Cached chunk → (its event, the position it is next needed at), and the same keyed by next + // use so the furthest one is the last entry. + let mut resident: HashMap<usize, (usize, usize)> = HashMap::new(); + let mut by_next: BTreeSet<(usize, usize)> = BTreeSet::new(); + let mut bytes: u64 = 0; + + let mut pre: Vec<(usize, usize)> = Vec::new(); + let mut seen_pre: HashSet<usize> = HashSet::new(); + for &c in preloaded { + if !seen_pre.insert(c) { + continue; + } + match first_use.get(&c) { + Some(&p) => pre.push((p, c)), + None => out.discarded.push(c), + } + } + pre.sort_unstable(); + for (p, c) in pre { + let size = chunk_bytes[c]; + if bytes + size <= budget { + let e = out.events.len(); + out.events.push(ScheduledEvent { + chunk: c, + uses: Vec::new(), + preloaded: true, + }); + resident.insert(c, (e, p)); + by_next.insert((p, c)); + bytes += size; + } else { + out.discarded.push(c); + } + } + out.peak_cache_bytes = bytes; + + for p in 0..n { + let c = chunk_of_use[p]; + let next = next_use[p]; + if let Some((e, _)) = resident.remove(&c) { + by_next.remove(&(p, c)); + out.events[e].uses.push(p); + out.use_event[p] = e; + if next == NONE { + bytes -= chunk_bytes[c]; + } else { + resident.insert(c, (e, next)); + by_next.insert((next, c)); + } + continue; + } + let e = out.events.len(); + out.events.push(ScheduledEvent { + chunk: c, + uses: vec![p], + preloaded: false, + }); + out.use_event[p] = e; + if next == NONE { + continue; + } + resident.insert(c, (e, next)); + by_next.insert((next, c)); + bytes += chunk_bytes[c]; + while bytes > budget { + let Some(&(far, victim)) = by_next.iter().next_back() else { + break; + }; + by_next.remove(&(far, victim)); + resident.remove(&victim); + bytes -= chunk_bytes[victim]; + } + out.peak_cache_bytes = out.peak_cache_bytes.max(bytes); + } + out +} + +// ───────────────────────────────────────────────────────────────────────────────────────────── +// The plan +// ───────────────────────────────────────────────────────────────────────────────────────────── + +/// Everything the JNI layer hands over for one run. +pub struct StreamRequest { + pub manifest_bytes: Vec<u8>, + pub install_dir: String, + /// `""` = `<installDir>/.chunks`; a path = scratch cache (internal storage for a card install). + pub chunk_cache_dir: String, + /// The manager's owned staging directory on the install volume; `""` = stage beside each file. + pub staging_dir: String, + pub cdn_prefixes: Vec<String>, + /// Indices into `manifest.files` of the files still to write (Java's delta pass), in order. + pub pending_file_indices: Vec<usize>, + /// Java's `Σ fileSize` over the pending files, for a cross-check (`None` = skip). + pub expected_bytes: Option<u64>, + /// Most bytes of shared chunks the cache may hold. + pub cache_budget: u64, + /// Most planned bytes of replaced files (destination already there) staged at once; one is + /// always allowed. Fresh files are not limited by it. + pub replace_overlap_budget: u64, + pub ca_bundle_path: String, + pub max_workers: usize, + pub process_workers: usize, + pub label: String, +} + +#[derive(Debug)] +pub struct PlanPart { + /// Index into `manifest.unique_chunks`. + pub chunk: usize, + pub chunk_offset: usize, + pub file_offset: u64, + pub size: usize, +} + +#[derive(Debug)] +pub struct PlanFile { + pub rel_path: String, + pub size: u64, + /// The manifest's SHA-1 when it has one (all-zero = none, as the Java delta pass treats it). + pub sha1: Option<[u8; 20]>, + pub parts: Vec<PlanPart>, + /// Position of the file's first part in the install's part sequence. + pub first_use: usize, + /// The destination exists with data: staging this file costs space until its swap. + pub replaces: bool, +} + +#[derive(Debug)] +pub struct StreamPlan { + pub manifest: Manifest, + pub install_dir: PathBuf, + pub cache_dir: PathBuf, + pub scratch_cache: bool, + pub files: Vec<PlanFile>, + /// Position → (file, part index in that file). + pub use_file: Vec<usize>, + pub use_part: Vec<usize>, + pub schedule: Schedule, + /// The events that are fetched, in fetch order; a fetch item's id indexes this. + pub fetch_events: Vec<usize>, + /// Event → the file of its first use (0 for a preloaded copy nothing ends up using). + pub event_first_file: Vec<usize>, + /// Event → it is its chunk's first copy, cached as `<GUID>` (later copies get `<GUID>.<event>`, + /// so a copy being deleted can never remove the next one). + pub event_first_copy: Vec<bool>, + /// Every manifest file's destination: a staging name may never be one of them. + pub reserved: HashSet<PathBuf>, + /// Where staging files go; `None` = beside each destination. + pub staging_dir: Option<PathBuf>, + pub bytes_total: u64, + pub budget: u64, + pub overlap_budget: u64, + pub hosts: Vec<String>, + pub direct_chunks: usize, + pub shared_chunks: usize, + pub refetches: usize, +} + +/// A cached chunk left by an earlier run is reused only when it is what the manifest says: +/// the window size when there is one, the SHA-1 when there is one (`isValidCachedChunk`). +pub fn cached_chunk_is_valid(path: &Path, chunk: &ChunkInfo) -> bool { + let Ok(meta) = fs::metadata(path) else { + return false; + }; + if !meta.is_file() { + return false; + } + if chunk.window_size > 0 && meta.len() != chunk.window_size as u64 { + return false; + } + let Some(expected) = chunk.verifiable_sha1() else { + return true; + }; + match fs::read(path) { + Ok(data) => Sha1::digest(&data).as_slice() == &expected[..], + Err(_) => false, + } +} + +/// Parse, check, clean the cache and schedule. `Err` = nothing was fetched or written (stale cache +/// entries may have been removed, as the Java writer's own preparation does); the manager then +/// runs the bounded Java writer instead. +pub fn build_stream_plan(req: &StreamRequest) -> Result<StreamPlan, String> { + if req.install_dir.is_empty() { + return Err("stream: empty install dir".to_string()); + } + let manifest = parse_manifest(&req.manifest_bytes).map_err(|e| format!("stream: {e}"))?; + let by_guid = manifest.chunk_index_by_guid(); + + let mut files: Vec<PlanFile> = Vec::with_capacity(req.pending_file_indices.len()); + let mut use_file: Vec<usize> = Vec::new(); + let mut use_part: Vec<usize> = Vec::new(); + let mut chunk_of_use: Vec<usize> = Vec::new(); + let mut bytes_total: u64 = 0; + for &fi in &req.pending_file_indices { + let Some(f) = manifest.files.get(fi) else { + return Err(format!( + "stream: pending file index {fi} out of range ({} files)", + manifest.files.len() + )); + }; + let rel_path = relative_path(f); + let first_use = chunk_of_use.len(); + let mut parts = Vec::with_capacity(f.parts.len()); + let mut file_offset: u64 = 0; + for (k, part) in f.parts.iter().enumerate() { + let guid = part.guid_str(); + let Some(&ci) = by_guid.get(&guid) else { + return Err(format!("Missing chunk metadata {guid} for {rel_path}")); + }; + let size = part.size as u32 as usize; + let chunk_offset = usize::try_from(part.offset) + .map_err(|_| format!("Chunk {guid} is too short for {rel_path}"))?; + let window = chunk_window_bytes(manifest.unique_chunks[ci].window_size); + if (chunk_offset as u64).saturating_add(size as u64) > window { + return Err(format!("Chunk {guid} is too short for {rel_path}")); + } + parts.push(PlanPart { + chunk: ci, + chunk_offset, + file_offset, + size, + }); + file_offset += size as u64; + use_file.push(files.len()); + use_part.push(k); + chunk_of_use.push(ci); + } + bytes_total += file_offset; + let replaces = fs::metadata(Path::new(&req.install_dir).join(&rel_path)) + .map(|m| m.is_file() && m.len() > 0) + .unwrap_or(false); + files.push(PlanFile { + replaces, + rel_path, + size: file_offset, + sha1: f.sha1.filter(|h| h.iter().any(|&b| b != 0)), + parts, + first_use, + }); + } + if let Some(expected) = req.expected_bytes { + if expected != bytes_total { + return Err(format!( + "stream: byte total mismatch java={expected} rust={bytes_total}" + )); + } + } + let hosts = distinct_prefixes(&req.cdn_prefixes); + if hosts.is_empty() { + return Err("stream: no CDN prefixes".to_string()); + } + + let install_dir = PathBuf::from(&req.install_dir); + let staging_dir = if req.staging_dir.is_empty() { + None + } else { + let dir = PathBuf::from(&req.staging_dir); + if !dir.is_dir() { + return Err(format!("stream: staging dir {} missing", dir.display())); + } + Some(dir) + }; + let cache_dir = resolve_cache_dir(&req.install_dir, &req.chunk_cache_dir); + fs::create_dir_all(&cache_dir) + .map_err(|e| format!("stream: mkdirs {}: {e}", cache_dir.display()))?; + let reserved: HashSet<PathBuf> = manifest + .files + .iter() + .map(|f| install_dir.join(relative_path(f))) + .collect(); + + // The cache keeps only verified chunks this install still needs, as the Java writer's + // `prepareBoundedCache` does: anything else (a stale chunk, a `.part`, a later copy from a + // cancelled run) would only take space the files need. + let needed: HashSet<usize> = chunk_of_use.iter().copied().collect(); + let mut needed_by_name: HashMap<String, usize> = HashMap::new(); + for &ci in &needed { + needed_by_name.insert(manifest.unique_chunks[ci].guid_str(), ci); + } + let entries = fs::read_dir(&cache_dir) + .map_err(|e| format!("stream: list {}: {e}", cache_dir.display()))?; + let mut preloaded: Vec<usize> = Vec::new(); + for entry in entries.flatten() { + let path = entry.path(); + let name = entry.file_name().to_string_lossy().into_owned(); + if let Some(&ci) = needed_by_name.get(&name) { + if cached_chunk_is_valid(&path, &manifest.unique_chunks[ci]) { + preloaded.push(ci); + continue; + } + } + remove_entry(&path); + } + + let chunk_bytes: Vec<u64> = manifest + .unique_chunks + .iter() + .map(|c| chunk_window_bytes(c.window_size)) + .collect(); + let schedule = schedule(&chunk_of_use, &chunk_bytes, &preloaded, req.cache_budget); + for &ci in &schedule.discarded { + remove_entry(&cache_dir.join(manifest.unique_chunks[ci].guid_str())); + } + + let mut fetch_events = Vec::new(); + let mut event_first_file = Vec::with_capacity(schedule.events.len()); + let mut event_first_copy = Vec::with_capacity(schedule.events.len()); + let mut copies: HashMap<usize, usize> = HashMap::new(); + for (e, ev) in schedule.events.iter().enumerate() { + event_first_file.push(ev.uses.first().map(|&p| use_file[p]).unwrap_or(0)); + let count = copies.entry(ev.chunk).or_insert(0); + event_first_copy.push(*count == 0); + *count += 1; + if !ev.preloaded { + fetch_events.push(e); + } + } + let mut uses_per_chunk: HashMap<usize, usize> = HashMap::new(); + for &c in &chunk_of_use { + *uses_per_chunk.entry(c).or_insert(0) += 1; + } + let direct_chunks = uses_per_chunk.values().filter(|&&n| n == 1).count(); + let shared_chunks = uses_per_chunk.len() - direct_chunks; + let refetches: usize = copies.values().map(|&n| n - 1).sum(); + + Ok(StreamPlan { + manifest, + install_dir, + cache_dir, + scratch_cache: !req.chunk_cache_dir.is_empty(), + files, + use_file, + use_part, + schedule, + fetch_events, + event_first_file, + event_first_copy, + reserved, + staging_dir, + bytes_total, + budget: req.cache_budget, + overlap_budget: req.replace_overlap_budget, + hosts, + direct_chunks, + shared_chunks, + refetches, + }) +} + +fn remove_entry(path: &Path) { + if path.is_dir() { + let _ = fs::remove_dir_all(path); + } else { + let _ = fs::remove_file(path); + } +} + +// ───────────────────────────────────────────────────────────────────────────────────────────── +// The run +// ───────────────────────────────────────────────────────────────────────────────────────────── + +/// What the Java side sees of a run's progress. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct StreamProgress { + /// File bytes written so far (into staging files). + pub bytes_written: u64, + pub bytes_total: u64, + pub files_done: u64, + pub files_total: u64, + /// Sizes of the files verified and swapped into place. + pub files_bytes: u64, +} + +/// The run's callbacks. All may be called from any engine thread. +pub struct Hooks<'a> { + pub progress: &'a (dyn Fn(&StreamProgress) + Sync), + /// Called once, before the first file replaces its destination: the Java side marks an + /// existing install `updating`. `Err` fails the run and nothing is replaced. + pub before_replace: &'a (dyn Fn() -> Result<(), String> + Sync), + pub log: &'a (dyn Fn(&str) + Sync), +} + +#[derive(Clone, Debug, Default)] +pub struct StreamOutcome { + pub success: bool, + pub cancelled: bool, + pub error: String, + pub bytes_written: u64, + pub files_done: u64, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum EventState { + /// Not downloaded yet. + Pending, + /// Downloaded, its cache copy being written; uses are only claimed by the downloading thread. + Caching, + /// In the cache: newly reached files read it from there. + Cached, + /// Every use is claimed; no cache copy (left). + Done, +} + +struct Shared { + state: Vec<EventState>, + /// Event → uses not written yet. + remaining: Vec<u32>, + /// Event → bytes its cache copy takes. + cached_size: Vec<u64>, + /// Position → a thread has taken this part to write. + claimed: Vec<bool>, + /// File → parts not written yet. + parts_left: Vec<u32>, + /// Files `[0, active)` have been reached and may be written. + active: usize, + /// One past the highest first-use file of a downloaded event. + reached: usize, + /// `fetch_events[..next_fetch]` have all been downloaded. + next_fetch: usize, + /// Files `[0, admitted)` passed the replacement-overlap budget; `active` never passes it. + admitted: usize, + /// Planned bytes of admitted replaced files not swapped in yet. + overlap_open: u64, + peak_overlap: u64, + cache_bytes: u64, + peak_cache: u64, +} + +#[derive(Default)] +struct FileWriter { + staging: Option<PathBuf>, + file: Option<File>, + sha: Option<Sha1>, + /// Parts `[0, hashed)` are in the SHA-1. + hashed: usize, + done: Vec<bool>, + /// Bytes written into the staging file. + written: u64, +} + +#[derive(Default)] +struct Stats { + written: AtomicU64, + direct: AtomicU64, + from_cache: AtomicU64, + cache_stored: AtomicU64, + staging_now: AtomicU64, + staging_peak: AtomicU64, + files_done: AtomicU64, + files_bytes: AtomicU64, + fetched: AtomicU64, + last_progress_ms: AtomicU64, +} + +/// Parts to write from cache copies (event → positions), and part-less files to finish. +#[derive(Default)] +struct Work { + from_cache: HashMap<usize, Vec<usize>>, + empty_files: Vec<usize>, +} + +/// Unique part of every staging name this process creates. +static STAGE_SEQ: AtomicU64 = AtomicU64::new(0); + +/// The run's state; it is the fetch core's sink. +pub struct Engine<'a> { + plan: &'a StreamPlan, + cancel: &'a AtomicBool, + hooks: &'a Hooks<'a>, + shared: Mutex<Shared>, + writers: Vec<Mutex<FileWriter>>, + replace_gate: Mutex<Option<Result<(), String>>>, + stats: Stats, + started: Instant, + /// `Shared::admitted`, readable by the fetch driver without the lock. + admitted: AtomicUsize, +} + +fn fatal_write(rel: &str, err: impl std::fmt::Display) -> SinkError { + SinkError::Fatal(format!("Could not assemble or atomically replace {rel}: {err}")) +} + +impl<'a> Engine<'a> { + pub fn new(plan: &'a StreamPlan, cancel: &'a AtomicBool, hooks: &'a Hooks<'a>) -> Self { + let events = &plan.schedule.events; + let mut state = Vec::with_capacity(events.len()); + let mut cached_size = vec![0u64; events.len()]; + let mut cache_bytes = 0u64; + for (e, ev) in events.iter().enumerate() { + if !ev.preloaded { + state.push(EventState::Pending); + continue; + } + let path = cache_path(plan, e); + if ev.uses.is_empty() { + // Evicted before its first use in the plan: it only takes space. + let _ = fs::remove_file(&path); + state.push(EventState::Done); + continue; + } + let size = fs::metadata(&path) + .map(|m| m.len()) + .unwrap_or(plan_chunk_bytes(plan, ev.chunk)); + cached_size[e] = size; + cache_bytes += size; + state.push(EventState::Cached); + } + Engine { + plan, + cancel, + hooks, + shared: Mutex::new(Shared { + state, + remaining: events.iter().map(|ev| ev.uses.len() as u32).collect(), + cached_size, + claimed: vec![false; plan.use_file.len()], + parts_left: plan.files.iter().map(|f| f.parts.len() as u32).collect(), + active: 0, + reached: 0, + next_fetch: 0, + admitted: 0, + overlap_open: 0, + peak_overlap: 0, + cache_bytes, + peak_cache: cache_bytes, + }), + writers: plan.files.iter().map(|_| Mutex::new(FileWriter::default())).collect(), + replace_gate: Mutex::new(None), + stats: Stats::default(), + started: Instant::now(), + admitted: AtomicUsize::new(0), + } + } + + /// The fetch driver's gate: fetch item `id` may go out once its first file is admitted. + pub fn may_dispatch(&self, id: usize) -> bool { + let p = self.plan; + match p.fetch_events.get(id) { + Some(&e) => p.event_first_file[e] < self.admitted.load(Ordering::Acquire), + None => true, + } + } + + /// The most planned bytes of replaced files that were staged at once. + pub fn peak_replace_overlap(&self) -> u64 { + self.shared.lock().map(|st| st.peak_overlap).unwrap_or_default() + } + + /// Keep reaching files until nothing new can be written: a file that finishes can admit the + /// next replaced file, whose parts may already be in the cache. Iterative, so a long run of + /// cache-only files never deepens the stack. + fn settle(&self) -> Result<(), SinkError> { + loop { + let mut work = Work::default(); + { + let mut st = self.shared.lock().expect("stream state poisoned"); + self.advance(&mut st, &mut work); + } + if work.from_cache.is_empty() && work.empty_files.is_empty() { + return Ok(()); + } + self.run_work(work)?; + } + } + + /// Before the first fetch: the files before the first chunk to download can be written from + /// the cache an earlier run left (or are empty). + pub fn start(&self) -> Result<(), SinkError> { + self.settle() + } + + /// Reach every file that can be written now: all files before the first chunk still to + /// download, and every file up to the furthest first use of a chunk that has arrived. Parts + /// of newly reached files whose chunk is in the cache are claimed for writing. + fn advance(&self, st: &mut Shared, work: &mut Work) { + let p = self.plan; + while st.admitted < p.files.len() { + let file = &p.files[st.admitted]; + if file.replaces { + if st.overlap_open > 0 && st.overlap_open + file.size > p.overlap_budget { + break; + } + st.overlap_open += file.size; + st.peak_overlap = st.peak_overlap.max(st.overlap_open); + } + st.admitted += 1; + } + self.admitted.store(st.admitted, Ordering::Release); + while st.next_fetch < p.fetch_events.len() + && st.state[p.fetch_events[st.next_fetch]] != EventState::Pending + { + st.next_fetch += 1; + } + let frontier = match p.fetch_events.get(st.next_fetch) { + Some(&e) => p.event_first_file[e], + None => p.files.len(), + }; + let target = st.active.max(frontier).max(st.reached).min(st.admitted); + for f in st.active..target { + let file = &p.files[f]; + if file.parts.is_empty() { + work.empty_files.push(f); + continue; + } + for k in 0..file.parts.len() { + let pos = file.first_use + k; + let e = p.schedule.use_event[pos]; + if st.state[e] == EventState::Cached && !st.claimed[pos] { + st.claimed[pos] = true; + work.from_cache.entry(e).or_default().push(pos); + } + } + } + st.active = target; + } + + /// One downloaded chunk body (fetch item `id`). + fn on_body(&self, id: usize, body: Vec<u8>) -> Result<u64, SinkError> { + let p = self.plan; + let Some(&e) = p.fetch_events.get(id) else { + return Err(SinkError::Fatal(format!("item id {id} out of range"))); + }; + let ev = &p.schedule.events[e]; + let chunk = &p.manifest.unique_chunks[ev.chunk]; + let data = decode_verified_chunk(&body, chunk.window_size, chunk.verifiable_sha1()) + .map_err(|reason| SinkError::Retry(format!("{} {reason}", chunk.guid_str())))?; + drop(body); + self.stats.fetched.fetch_add(1, Ordering::Relaxed); + + let mut work = Work::default(); + let mut now: Vec<usize> = Vec::new(); + let caching; + { + let mut st = self.shared.lock().expect("stream state poisoned"); + if st.state[e] != EventState::Pending { + return Ok(chunk.credit_bytes()); + } + // Provisional: until the cache copy exists, nobody else may claim this chunk's uses. + st.state[e] = EventState::Caching; + st.reached = st.reached.max(p.event_first_file[e] + 1); + self.advance(&mut st, &mut work); + for &pos in &ev.uses { + if p.use_file[pos] < st.active && !st.claimed[pos] { + st.claimed[pos] = true; + now.push(pos); + } + } + caching = now.len() < ev.uses.len(); + if !caching { + st.state[e] = EventState::Done; + } + } + self.write_from_memory(&data, &now)?; + self.run_work(work)?; + if caching { + let path = cache_path(p, e); + write_cache_file(&path, &data).map_err(|err| { + SinkError::Fatal(format!("cache {}: {err}", chunk.guid_str())) + })?; + self.stats + .cache_stored + .fetch_add(data.len() as u64, Ordering::Relaxed); + let mut more = Vec::new(); + { + let mut st = self.shared.lock().expect("stream state poisoned"); + st.state[e] = EventState::Cached; + st.cached_size[e] = data.len() as u64; + st.cache_bytes += data.len() as u64; + st.peak_cache = st.peak_cache.max(st.cache_bytes); + // Files reached while the copy was being written. + for &pos in &ev.uses { + if p.use_file[pos] < st.active && !st.claimed[pos] { + st.claimed[pos] = true; + more.push(pos); + } + } + } + self.write_from_memory(&data, &more)?; + } + self.settle()?; + Ok(chunk.credit_bytes()) + } + + fn write_from_memory(&self, data: &[u8], positions: &[usize]) -> Result<(), SinkError> { + for &pos in positions { + let written = self.write_use(pos, data)?; + self.stats.direct.fetch_add(written, Ordering::Relaxed); + self.complete_use(pos)?; + } + Ok(()) + } + + fn run_work(&self, work: Work) -> Result<(), SinkError> { + let mut groups: Vec<(usize, Vec<usize>)> = work.from_cache.into_iter().collect(); + groups.sort_unstable(); + for (e, positions) in groups { + let ev = &self.plan.schedule.events[e]; + let guid = self.plan.manifest.unique_chunks[ev.chunk].guid_str(); + let data = fs::read(cache_path(self.plan, e)) + .map_err(|err| SinkError::Fatal(format!("read cached chunk {guid}: {err}")))?; + for pos in positions { + let written = self.write_use(pos, &data)?; + self.stats.from_cache.fetch_add(written, Ordering::Relaxed); + self.complete_use(pos)?; + } + } + for f in work.empty_files { + self.finalize(f)?; + } + Ok(()) + } + + /// Write the part at `pos` from its chunk's bytes into its file's staging file. + fn write_use(&self, pos: usize, chunk_data: &[u8]) -> Result<u64, SinkError> { + let p = self.plan; + let f = p.use_file[pos]; + let k = p.use_part[pos]; + let file = &p.files[f]; + let part = &file.parts[k]; + let guid = p.manifest.unique_chunks[part.chunk].guid_str(); + let end = part + .chunk_offset + .checked_add(part.size) + .filter(|&end| end <= chunk_data.len()) + .ok_or_else(|| { + SinkError::Fatal(format!("Chunk {guid} is too short for {}", file.rel_path)) + })?; + let slice = &chunk_data[part.chunk_offset..end]; + { + let mut guard = self.writers[f].lock().expect("writer poisoned"); + if guard.file.is_none() { + self.open_staging(f, &mut guard)?; + } + let FileWriter { + file: out, + sha, + hashed, + done, + written, + .. + } = &mut *guard; + let out = out.as_ref().expect("staging open"); + out.write_all_at(slice, part.file_offset) + .map_err(|err| fatal_write(&file.rel_path, err))?; + done[k] = true; + *written += slice.len() as u64; + // SHA-1 in file order: the part just written comes from memory, parts that arrived + // earlier out of order are read back from the staging file. + if let Some(sha) = sha.as_mut() { + while *hashed < file.parts.len() && done[*hashed] { + let h = *hashed; + if h == k { + sha.update(slice); + } else { + let q = &file.parts[h]; + let mut buf = vec![0u8; q.size]; + out.read_exact_at(&mut buf, q.file_offset) + .map_err(|err| fatal_write(&file.rel_path, err))?; + sha.update(&buf); + } + *hashed += 1; + } + } + } + let len = slice.len() as u64; + let staged = self.stats.staging_now.fetch_add(len, Ordering::Relaxed) + len; + self.stats.staging_peak.fetch_max(staged, Ordering::Relaxed); + self.stats.written.fetch_add(len, Ordering::Relaxed); + self.report(false); + Ok(len) + } + + /// A part is written: release its chunk copy after its last use, finish its file after its + /// last part. + fn complete_use(&self, pos: usize) -> Result<(), SinkError> { + let p = self.plan; + let e = p.schedule.use_event[pos]; + let f = p.use_file[pos]; + let (release, finish) = { + let mut st = self.shared.lock().expect("stream state poisoned"); + st.remaining[e] = st.remaining[e].saturating_sub(1); + let release = st.remaining[e] == 0 && st.state[e] == EventState::Cached; + if release { + st.state[e] = EventState::Done; + st.cache_bytes = st.cache_bytes.saturating_sub(st.cached_size[e]); + } + st.parts_left[f] = st.parts_left[f].saturating_sub(1); + (release, st.parts_left[f] == 0) + }; + if release { + let _ = fs::remove_file(cache_path(p, e)); + } + if finish { + self.finalize(f)?; + } + Ok(()) + } + + /// A unique `.stage` file in the owned staging directory (or beside the destination), never a + /// manifest path (`createStagingFile`). The destination's directory is created either way. + fn open_staging(&self, f: usize, w: &mut FileWriter) -> Result<(), SinkError> { + let file = &self.plan.files[f]; + let dest = self.plan.install_dir.join(&file.rel_path); + let parent = dest + .parent() + .map(Path::to_path_buf) + .unwrap_or_else(|| self.plan.install_dir.clone()); + fs::create_dir_all(&parent).map_err(|err| fatal_write(&file.rel_path, err))?; + let (stage_dir, prefix) = match &self.plan.staging_dir { + Some(dir) => (dir.clone(), "stream-"), + None => (parent.clone(), ".droiddeck-epic-"), + }; + for _ in 0..64 { + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.subsec_nanos()) + .unwrap_or(0); + let seq = STAGE_SEQ.fetch_add(1, Ordering::Relaxed); + let path = stage_dir.join(format!( + "{prefix}{}-{seq}-{nanos}.stage", + std::process::id() + )); + if self.plan.reserved.contains(&path) { + continue; + } + match OpenOptions::new() + .read(true) + .write(true) + .create_new(true) + .open(&path) + { + Ok(out) => { + w.staging = Some(path); + w.file = Some(out); + w.sha = file.sha1.map(|_| Sha1::new()); + w.hashed = 0; + w.done = vec![false; file.parts.len()]; + w.written = 0; + return Ok(()); + } + Err(err) if err.kind() == ErrorKind::AlreadyExists => continue, + Err(err) => return Err(fatal_write(&file.rel_path, err)), + } + } + Err(fatal_write(&file.rel_path, "no free staging name")) + } + + /// All parts are in: sync, check size and SHA-1, mark the install updating (once), rename + /// over the destination. On any failure the staging file goes and the destination stays. + fn finalize(&self, f: usize) -> Result<(), SinkError> { + let mut guard = self.writers[f].lock().expect("writer poisoned"); + if guard.file.is_none() { + self.open_staging(f, &mut guard)?; + } + let result = self.finish_file(f, &mut guard); + let staged = guard.written; + if result.is_err() { + guard.file = None; + if let Some(path) = guard.staging.take() { + let _ = fs::remove_file(&path); + } + } + drop(guard); + self.stats + .staging_now + .fetch_sub(staged.min(self.stats.staging_now.load(Ordering::Relaxed)), Ordering::Relaxed); + if result.is_ok() { + let size = self.plan.files[f].size; + if self.plan.files[f].replaces { + let mut st = self.shared.lock().expect("stream state poisoned"); + st.overlap_open = st.overlap_open.saturating_sub(size); + } + self.stats.files_done.fetch_add(1, Ordering::Relaxed); + self.stats.files_bytes.fetch_add(size, Ordering::Relaxed); + self.report(true); + } + result + } + + fn finish_file(&self, f: usize, w: &mut FileWriter) -> Result<(), SinkError> { + let file = &self.plan.files[f]; + let rel = file.rel_path.as_str(); + let out = w.file.take().expect("staging open"); + out.sync_all().map_err(|err| fatal_write(rel, err))?; + if self.cancel.load(Ordering::Relaxed) { + return Err(SinkError::Fatal("cancelled".to_string())); + } + let len = out.metadata().map_err(|err| fatal_write(rel, err))?.len(); + drop(out); + if len != file.size { + return Err(SinkError::Fatal(format!("Size mismatch for {rel}"))); + } + if let (Some(expected), Some(sha)) = (file.sha1.as_ref(), w.sha.take()) { + if w.hashed != file.parts.len() || sha.finalize().as_slice() != &expected[..] { + return Err(SinkError::Fatal(format!("SHA-1 mismatch for {rel}"))); + } + } + // Once a swap can mix versions, the old installed record is no longer truthful. + self.mark_updating().map_err(|err| fatal_write(rel, err))?; + if self.cancel.load(Ordering::Relaxed) { + return Err(SinkError::Fatal("cancelled".to_string())); + } + let staging = w.staging.clone().expect("staging path"); + let dest = self.plan.install_dir.join(&file.rel_path); + fs::rename(&staging, &dest).map_err(|err| fatal_write(rel, err))?; + w.staging = None; + Ok(()) + } + + fn mark_updating(&self) -> Result<(), String> { + let mut gate = self.replace_gate.lock().expect("replace gate poisoned"); + if let Some(result) = gate.as_ref() { + return result.clone(); + } + let result = (self.hooks.before_replace)(); + *gate = Some(result.clone()); + result + } + + fn report(&self, force: bool) { + let now_ms = self.started.elapsed().as_millis() as u64; + let last = self.stats.last_progress_ms.load(Ordering::Relaxed); + if !force { + if now_ms.saturating_sub(last) < PROGRESS_EVERY_MS { + return; + } + if self + .stats + .last_progress_ms + .compare_exchange(last, now_ms, Ordering::Relaxed, Ordering::Relaxed) + .is_err() + { + return; + } + } else { + self.stats.last_progress_ms.store(now_ms, Ordering::Relaxed); + } + (self.hooks.progress)(&self.snapshot()); + } + + fn snapshot(&self) -> StreamProgress { + StreamProgress { + bytes_written: self.stats.written.load(Ordering::Relaxed), + bytes_total: self.plan.bytes_total, + files_done: self.stats.files_done.load(Ordering::Relaxed), + files_total: self.plan.files.len() as u64, + files_bytes: self.stats.files_bytes.load(Ordering::Relaxed), + } + } + + /// Cancel or failure: every unfinished staging file goes; the destinations are untouched and + /// the cache stays for the next run. Call only when no other thread is writing. + pub fn discard_staging(&self) -> usize { + let mut removed = 0; + for writer in &self.writers { + let mut guard = writer.lock().unwrap_or_else(|e| e.into_inner()); + guard.file = None; + if let Some(path) = guard.staging.take() { + if fs::remove_file(&path).is_ok() { + removed += 1; + } + } + } + removed + } + + /// Files not yet swapped into place. + pub fn unfinished_files(&self) -> u64 { + self.plan.files.len() as u64 - self.stats.files_done.load(Ordering::Relaxed) + } + + pub fn summary(&self) -> String { + let secs = self.started.elapsed().as_secs_f64().max(0.001); + let written = self.stats.written.load(Ordering::Relaxed); + let (peak_cache, peak_overlap) = self + .shared + .lock() + .map(|st| (st.peak_cache, st.peak_overlap)) + .unwrap_or_default(); + format!( + "stream-summary mode=adaptive files={}/{} bytes_written={written} direct_bytes={} from_cache_bytes={} cache_stored_bytes={} peak_cache_bytes={peak_cache} budget={} plan_peak_cache_bytes={} peak_staging_bytes={} peak_replace_overlap_bytes={peak_overlap} overlap_budget={} fetched_chunks={} refetches={} elapsed={secs:.1}s avg_MBps={:.2}", + self.stats.files_done.load(Ordering::Relaxed), + self.plan.files.len(), + self.stats.direct.load(Ordering::Relaxed), + self.stats.from_cache.load(Ordering::Relaxed), + self.stats.cache_stored.load(Ordering::Relaxed), + self.plan.budget, + self.plan.schedule.peak_cache_bytes, + self.stats.staging_peak.load(Ordering::Relaxed), + self.plan.overlap_budget, + self.stats.fetched.load(Ordering::Relaxed), + self.plan.refetches, + written as f64 / secs / (1024.0 * 1024.0), + ) + } +} + +impl FetchSink for Engine<'_> { + fn process(&self, item: &FetchItem, body: Vec<u8>) -> Result<u64, SinkError> { + self.on_body(item.id as usize, body) + } +} + +fn plan_chunk_bytes(plan: &StreamPlan, chunk: usize) -> u64 { + chunk_window_bytes(plan.manifest.unique_chunks[chunk].window_size) +} + +/// Where event `e`'s copy lives in the cache. +fn cache_path(plan: &StreamPlan, e: usize) -> PathBuf { + let guid = plan.manifest.unique_chunks[plan.schedule.events[e].chunk].guid_str(); + if plan.event_first_copy[e] { + plan.cache_dir.join(guid) + } else { + plan.cache_dir.join(format!("{guid}.{e}")) + } +} + +/// `.part` + rename, so a cache file under its final name is always complete. +fn write_cache_file(path: &Path, data: &[u8]) -> Result<(), String> { + let tmp = part_path(path); + let _ = fs::remove_file(&tmp); + let result = fs::write(&tmp, data) + .map_err(|e| format!("write {}: {e}", tmp.display())) + .and_then(|()| fs::rename(&tmp, path).map_err(|e| format!("rename: {e}"))); + if result.is_err() { + let _ = fs::remove_file(&tmp); + } + result +} + +/// Run the plan to the end: the fetch core downloads the chunks in order, the engine writes them. +pub fn run_stream( + plan: &StreamPlan, + req: &StreamRequest, + cancel: &AtomicBool, + hooks: &Hooks<'_>, +) -> StreamOutcome { + let log = hooks.log; + let host_cap = per_host_cap(req.max_workers, plan.hosts.len()); + log(&format!( + "stream-plan chunk_dir={} cache_dir={} scratch={} {} files={} bytes={} uses={} chunks_direct={} chunks_shared={} fetches={} preloaded={} refetches={} budget={} plan_peak_cache={} replaced_files={} overlap_budget={} hosts={} workers={} per_host_cap={host_cap} process_workers={}", + plan.manifest.chunk_dir, + plan.cache_dir.display(), + plan.scratch_cache, + crate::priority::log_field(), + plan.files.len(), + plan.bytes_total, + plan.use_file.len(), + plan.direct_chunks, + plan.shared_chunks, + plan.fetch_events.len(), + plan.schedule.events.len() - plan.fetch_events.len(), + plan.refetches, + plan.budget, + plan.schedule.peak_cache_bytes, + plan.files.iter().filter(|f| f.replaces).count(), + plan.overlap_budget, + plan.hosts.len(), + req.max_workers, + req.process_workers + )); + for (i, h) in plan.hosts.iter().enumerate() { + log(&format!("host[{i}]={h}")); + } + + let engine = Engine::new(plan, cancel, hooks); + let mut outcome = StreamOutcome::default(); + let mut error: Option<String> = match engine.start() { + Ok(()) => None, + Err(SinkError::Fatal(m)) | Err(SinkError::Retry(m)) => Some(m), + }; + + if error.is_none() && !cancel.load(Ordering::Relaxed) && !plan.fetch_events.is_empty() { + let items: Vec<FetchItem> = plan + .fetch_events + .iter() + .enumerate() + .map(|(id, &e)| { + let chunk = &plan.manifest.unique_chunks[plan.schedule.events[e].chunk]; + FetchItem { + id: id as u64, + urls: plan + .hosts + .iter() + .map(|h| chunk_url(h, &plan.manifest.chunk_dir, chunk)) + .collect(), + reserve: if chunk.file_size > 0 { + chunk.file_size as u64 + } else { + 0 + }, + range: None, + } + }) + .collect(); + let opts = FetchOptions { + max_workers: req.max_workers.max(1), + 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(), + ..FetchOptions::default() + }; + // Progress is the engine's (file bytes written); the core's own credit only feeds its + // `fetch-end` line. + let no_progress = |_: u64, _: u64| {}; + let gate = |id: usize| engine.may_dispatch(id); + let fetched = run_fetch_gated( + items, + &plan.hosts, + &opts, + &engine, + cancel, + &no_progress, + log, + &gate, + ); + error = fetched.error; + } + + let progress = engine.snapshot(); + outcome.bytes_written = progress.bytes_written; + outcome.files_done = progress.files_done; + if cancel.load(Ordering::Relaxed) { + let removed = engine.discard_staging(); + outcome.cancelled = true; + outcome.error = "cancelled".to_string(); + log(&engine.summary()); + log(&format!( + "cancelled ({}/{} files, {removed} staging files removed)", + progress.files_done, progress.files_total + )); + return outcome; + } + if error.is_none() && engine.unfinished_files() > 0 { + error = Some(format!( + "stream ended with {} files unfinished", + engine.unfinished_files() + )); + } + if let Some(err) = error { + let removed = engine.discard_staging(); + outcome.error = err; + log(&engine.summary()); + log(&format!( + "FAIL {} ({}/{} files ok, {removed} staging files removed)", + outcome.error, progress.files_done, progress.files_total + )); + return outcome; + } + outcome.success = true; + log(&engine.summary()); + match fs::remove_dir_all(&plan.cache_dir) { + Ok(()) => log(&format!("cache removed {}", plan.cache_dir.display())), + Err(e) => log(&format!("cache not removed {}: {e}", plan.cache_dir.display())), + } + outcome +} + +#[cfg(test)] +mod tests { + use super::super::chunk::test_support::{build_chunk_body, sha1_of, temp_dir}; + use super::super::manifest::test_support::{build_manifest, TestChunk, TestFile}; + use super::*; + + // ── schedule ── + + #[test] + fn single_use_chunks_are_plain_downloads_and_shared_ones_stay_cached() { + // Chunk 0 is used at positions 0 and 2, chunk 1 once, chunk 2 once. + let s = schedule(&[0, 1, 0, 2], &[10, 10, 10], &[], 100); + assert_eq!(s.events.len(), 3); + assert_eq!(s.events[0].uses, vec![0, 2]); + assert_eq!(s.events[1].uses, vec![1]); + assert_eq!(s.events[2].uses, vec![3]); + assert_eq!(s.use_event, vec![0, 1, 0, 2]); + assert_eq!(s.peak_cache_bytes, 10, "only the shared chunk is ever cached"); + assert!(s.discarded.is_empty()); + } + + #[test] + fn a_chunk_bigger_than_the_budget_is_downloaded_for_every_use() { + let s = schedule(&[0, 1, 0, 0], &[10, 1], &[], 5); + let chunk0: Vec<&ScheduledEvent> = s.events.iter().filter(|e| e.chunk == 0).collect(); + assert_eq!(chunk0.len(), 3, "never cached → one download per use"); + assert!(chunk0.iter().all(|e| e.uses.len() == 1)); + assert_eq!(s.peak_cache_bytes, 0); + } + + #[test] + fn eviction_drops_the_chunk_needed_furthest_away() { + // Budget fits two chunks. At position 2 chunk 2 arrives (next use 6) while 0 (next 3) + // and 1 (next 4) are cached: chunk 2 itself is the furthest and is not kept. + let uses = [0, 1, 2, 0, 1, 9, 2]; + let s = schedule(&uses, &[4, 4, 4, 0, 0, 0, 0, 0, 0, 1], &[], 8); + let copies = |c: usize| s.events.iter().filter(|e| e.chunk == c).count(); + assert_eq!(copies(0), 1); + assert_eq!(copies(1), 1); + assert_eq!(copies(2), 2, "chunk 2 downloaded again for its later use"); + assert!(s.peak_cache_bytes <= 8); + // Same sequence with room for all three: no refetch. + let s = schedule(&uses, &[4, 4, 4, 0, 0, 0, 0, 0, 0, 1], &[], 12); + assert_eq!(s.events.iter().filter(|e| e.chunk == 2).count(), 1); + assert_eq!(s.peak_cache_bytes, 12); + } + + #[test] + fn preloaded_chunks_serve_their_uses_until_the_budget_says_otherwise() { + // Chunk 0 cached by an earlier run, chunk 1 too but nothing needs it, chunk 2 cached but + // over budget. + let s = schedule(&[0, 2, 0], &[4, 4, 4], &[0, 1, 2], 4); + assert!(s.events[0].preloaded); + assert_eq!(s.events[0].chunk, 0); + assert_eq!(s.events[0].uses, vec![0, 2]); + let mut discarded = s.discarded.clone(); + discarded.sort_unstable(); + assert_eq!(discarded, vec![1, 2]); + let fetched: Vec<&ScheduledEvent> = s.events.iter().filter(|e| !e.preloaded).collect(); + assert_eq!(fetched.len(), 1); + assert_eq!(fetched[0].chunk, 2); + } + + // ── engine (no network: bodies are handed to the sink directly) ── + + const A: [u32; 4] = [1, 0, 0, 0]; + const B: [u32; 4] = [2, 0, 0, 0]; + const C: [u32; 4] = [3, 0, 0, 0]; + + fn data(seed: u32, len: usize) -> Vec<u8> { + (0..len as u32).map(|i| ((i * 7 + seed) % 251) as u8).collect() + } + + struct Fixture { + dir: PathBuf, + install: PathBuf, + chunks: Vec<([u32; 4], Vec<u8>)>, + files: Vec<(String, Vec<([u32; 4], i32, i32)>)>, + } + + impl Fixture { + /// Three files: `a.bin` = A[0..100] + B[0..50]; `dir/b.bin` = A[100..300] (A is shared); + /// `c.bin` = C whole. + fn new(tag: &str) -> Self { + let dir = temp_dir(tag); + let install = dir.join("game"); + fs::create_dir_all(&install).unwrap(); + Fixture { + dir, + install, + chunks: vec![(A, data(1, 300)), (B, data(2, 50)), (C, data(3, 80))], + files: vec![ + ("a.bin".to_string(), vec![(A, 0, 100), (B, 0, 50)]), + ("dir\\b.bin".to_string(), vec![(A, 100, 200)]), + ("c.bin".to_string(), vec![(C, 0, 80)]), + ], + } + } + + fn chunk(&self, guid: [u32; 4]) -> &Vec<u8> { + &self.chunks.iter().find(|(g, _)| *g == guid).unwrap().1 + } + + fn expected(&self, file: usize) -> Vec<u8> { + let mut out = Vec::new(); + for (g, off, size) in &self.files[file].1 { + out.extend_from_slice(&self.chunk(*g)[*off as usize..(*off + *size) as usize]); + } + out + } + + fn manifest(&self) -> Vec<u8> { + let chunks: Vec<TestChunk> = self + .chunks + .iter() + .map(|(g, d)| TestChunk { + guid: *g, + hash: g[0] as u64, + sha1: sha1_of(d), + group: 1, + window: d.len() as i32, + file_size: d.len() as u64, + }) + .collect(); + let files: Vec<TestFile> = (0..self.files.len()) + .map(|i| TestFile { + name: self.files[i].0.clone(), + sha1: sha1_of(&self.expected(i)), + tags: vec![], + parts: self.files[i].1.clone(), + }) + .collect(); + build_manifest(&chunks, &files, 21, true) + } + + fn request(&self, pending: Vec<usize>, budget: u64) -> StreamRequest { + StreamRequest { + manifest_bytes: self.manifest(), + install_dir: self.install.to_str().unwrap().to_string(), + chunk_cache_dir: self.dir.join("cache").to_str().unwrap().to_string(), + staging_dir: String::new(), + cdn_prefixes: vec!["http://127.0.0.1:9/Builds/x".to_string()], + pending_file_indices: pending, + expected_bytes: None, + cache_budget: budget, + replace_overlap_budget: u64::MAX, + ca_bundle_path: String::new(), + max_workers: 4, + process_workers: 2, + label: "epic test".to_string(), + } + } + + fn body_for(&self, plan: &StreamPlan, id: usize) -> Vec<u8> { + let ev = &plan.schedule.events[plan.fetch_events[id]]; + let guid = plan.manifest.unique_chunks[ev.chunk].guid; + build_chunk_body(self.chunk(guid), true, 0, None) + } + + fn stages(&self) -> Vec<PathBuf> { + let mut out = Vec::new(); + let mut dirs = vec![self.install.clone()]; + while let Some(d) = dirs.pop() { + for entry in fs::read_dir(&d).unwrap().flatten() { + let path = entry.path(); + if path.is_dir() { + dirs.push(path); + } else if path.to_string_lossy().ends_with(".stage") { + out.push(path); + } + } + } + out + } + } + + impl Drop for Fixture { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.dir); + } + } + + struct Recorder { + marks: Mutex<u32>, + last: Mutex<StreamProgress>, + fail_mark: bool, + } + + impl Recorder { + fn new() -> Self { + Recorder { + marks: Mutex::new(0), + last: Mutex::new(StreamProgress::default()), + fail_mark: false, + } + } + } + + fn with_engine<R>( + plan: &StreamPlan, + cancel: &AtomicBool, + rec: &Recorder, + body: impl FnOnce(&Engine) -> R, + ) -> R { + let progress = |p: &StreamProgress| *rec.last.lock().unwrap() = *p; + let before = || { + *rec.marks.lock().unwrap() += 1; + if rec.fail_mark { + Err("mark failed".to_string()) + } else { + Ok(()) + } + }; + let log = |_: &str| {}; + let hooks = Hooks { + progress: &progress, + before_replace: &before, + log: &log, + }; + let engine = Engine::new(plan, cancel, &hooks); + body(&engine) + } + + #[test] + fn plan_counts_direct_and_shared_chunks_and_orders_fetches_by_first_use() { + let fx = Fixture::new("stream-plan"); + let plan = build_stream_plan(&fx.request(vec![0, 1, 2], 1 << 20)).unwrap(); + assert_eq!(plan.direct_chunks, 2, "B and C"); + assert_eq!(plan.shared_chunks, 1, "A"); + assert_eq!(plan.fetch_events.len(), 3); + assert_eq!(plan.refetches, 0); + assert_eq!(plan.bytes_total, 150 + 200 + 80); + let order: Vec<[u32; 4]> = plan + .fetch_events + .iter() + .map(|&e| plan.manifest.unique_chunks[plan.schedule.events[e].chunk].guid) + .collect(); + assert_eq!(order, vec![A, B, C]); + let mut req = fx.request(vec![0, 1, 2], 1 << 20); + req.expected_bytes = Some(1); + assert!(build_stream_plan(&req).unwrap_err().contains("byte total mismatch")); + } + + #[test] + fn out_of_order_arrival_writes_verified_files_and_empties_the_cache() { + let fx = Fixture::new("stream-ooo"); + let plan = build_stream_plan(&fx.request(vec![0, 1, 2], 1 << 20)).unwrap(); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + // C, then B, then A: the shared chunk last. + for id in (0..plan.fetch_events.len()).rev() { + engine.process(&item(id), fx.body_for(&plan, id)).unwrap(); + } + assert_eq!(engine.unfinished_files(), 0); + }); + for i in 0..3 { + let rel = fx.files[i].0.replace('\\', "/"); + assert_eq!(fs::read(fx.install.join(&rel)).unwrap(), fx.expected(i), "{rel}"); + } + assert!(fx.stages().is_empty()); + assert_eq!(*rec.marks.lock().unwrap(), 1, "marked updating once"); + let last = *rec.last.lock().unwrap(); + assert_eq!(last.files_done, 3); + assert_eq!(last.bytes_written, 430); + assert_eq!(last.files_bytes, 430); + let leftovers = fs::read_dir(&plan.cache_dir).unwrap().count(); + assert_eq!(leftovers, 0, "the shared chunk is released after its last use"); + } + + #[test] + fn a_shared_chunk_arriving_first_is_cached_until_its_later_file_is_reached() { + let fx = Fixture::new("stream-cache"); + let plan = build_stream_plan(&fx.request(vec![0, 1, 2], 1 << 20)).unwrap(); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + engine.process(&item(0), fx.body_for(&plan, 0)).unwrap(); // A + let cached = plan.cache_dir.join(guid_to(A)); + assert!(cached.is_file(), "A waits in the cache for dir/b.bin"); + engine.process(&item(1), fx.body_for(&plan, 1)).unwrap(); // B: reaches b.bin + assert!(!cached.exists(), "released after its last use"); + engine.process(&item(2), fx.body_for(&plan, 2)).unwrap(); // C + }); + assert_eq!(fs::read(fx.install.join("dir/b.bin")).unwrap(), fx.expected(1)); + } + + #[test] + fn a_zero_budget_refetches_shared_chunks_and_never_caches() { + let fx = Fixture::new("stream-budget"); + let plan = build_stream_plan(&fx.request(vec![0, 1, 2], 0)).unwrap(); + assert_eq!(plan.refetches, 1, "A downloaded once per file"); + assert_eq!(plan.fetch_events.len(), 4); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + for id in [3, 1, 0, 2] { + engine.process(&item(id), fx.body_for(&plan, id)).unwrap(); + assert_eq!(fs::read_dir(&plan.cache_dir).unwrap().count(), 0); + } + assert_eq!(engine.unfinished_files(), 0); + }); + for i in 0..3 { + let rel = fx.files[i].0.replace('\\', "/"); + assert_eq!(fs::read(fx.install.join(&rel)).unwrap(), fx.expected(i)); + } + } + + #[test] + fn resume_writes_only_pending_files_and_reuses_a_cached_chunk() { + let fx = Fixture::new("stream-resume"); + // An earlier run left A verified in the cache, plus a stale entry and a later copy. + let cache = fx.dir.join("cache"); + fs::create_dir_all(&cache).unwrap(); + fs::write(cache.join(guid_to(A)), fx.chunk(A)).unwrap(); + fs::write(cache.join("FFFFFFFF000000000000000000000000"), b"stale").unwrap(); + fs::write(cache.join(format!("{}.7", guid_to(A))), fx.chunk(A)).unwrap(); + // a.bin was verified by the delta pass; only dir/b.bin and c.bin are pending. + let plan = build_stream_plan(&fx.request(vec![1, 2], 1 << 20)).unwrap(); + assert_eq!(plan.fetch_events.len(), 1, "only C is downloaded"); + assert_eq!(fs::read_dir(&cache).unwrap().count(), 1, "stale entries removed"); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + assert!(fx.install.join("dir/b.bin").is_file(), "written from the cache at start"); + engine.process(&item(0), fx.body_for(&plan, 0)).unwrap(); + assert_eq!(engine.unfinished_files(), 0); + }); + assert!(!fx.install.join("a.bin").exists()); + assert_eq!(fs::read(fx.install.join("c.bin")).unwrap(), fx.expected(2)); + } + + #[test] + fn cancel_keeps_old_files_removes_staging_and_keeps_the_cache() { + let fx = Fixture::new("stream-cancel"); + fs::write(fx.install.join("a.bin"), b"old a").unwrap(); + fs::write(fx.install.join("c.bin"), b"old c").unwrap(); + let plan = build_stream_plan(&fx.request(vec![0, 1, 2], 1 << 20)).unwrap(); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + // A arrives: a.bin half written, A cached for dir/b.bin. + engine.process(&item(0), fx.body_for(&plan, 0)).unwrap(); + assert_eq!(fx.stages().len(), 1); + cancel.store(true, Ordering::Relaxed); + // B completes a.bin, but a cancelled run never swaps it in. + assert!(engine.process(&item(1), fx.body_for(&plan, 1)).is_err()); + assert!(engine.discard_staging() <= 2); + }); + assert!(fx.stages().is_empty()); + assert_eq!(fs::read(fx.install.join("a.bin")).unwrap(), b"old a"); + assert_eq!(fs::read(fx.install.join("c.bin")).unwrap(), b"old c"); + assert_eq!(*rec.marks.lock().unwrap(), 0, "nothing was replaced"); + assert!(plan.cache_dir.join(guid_to(A)).is_file(), "kept for the resume"); + } + + #[test] + fn a_hash_failure_keeps_the_old_file_and_marks_nothing() { + let fx = Fixture::new("stream-hash"); + fs::write(fx.install.join("c.bin"), b"old c").unwrap(); + let mut plan = build_stream_plan(&fx.request(vec![2], 1 << 20)).unwrap(); + // The manifest promises another hash than the bytes the chunk yields. + plan.files[0].sha1 = Some([9; 20]); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + let err = with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + let res = engine.process(&item(0), fx.body_for(&plan, 0)); + engine.discard_staging(); + res + }); + match err { + Err(SinkError::Fatal(m)) => assert!(m.contains("SHA-1 mismatch for c.bin"), "{m}"), + other => panic!("expected a fatal hash failure, got {other:?}"), + } + assert_eq!(fs::read(fx.install.join("c.bin")).unwrap(), b"old c"); + assert!(fx.stages().is_empty()); + assert_eq!(*rec.marks.lock().unwrap(), 0); + } + + #[test] + fn a_corrupt_body_is_a_retry_that_changes_nothing() { + let fx = Fixture::new("stream-corrupt"); + let plan = build_stream_plan(&fx.request(vec![2], 1 << 20)).unwrap(); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + let mut body = fx.body_for(&plan, 0); + let n = body.len(); + body[n - 3] ^= 0xFF; + assert!(matches!(engine.process(&item(0), body), Err(SinkError::Retry(_)))); + assert!(fx.stages().is_empty()); + engine.process(&item(0), fx.body_for(&plan, 0)).unwrap(); + }); + assert_eq!(fs::read(fx.install.join("c.bin")).unwrap(), fx.expected(2)); + } + + #[test] + fn a_failed_mark_stops_before_any_replacement() { + let fx = Fixture::new("stream-mark"); + fs::write(fx.install.join("c.bin"), b"old c").unwrap(); + let plan = build_stream_plan(&fx.request(vec![2], 1 << 20)).unwrap(); + let cancel = AtomicBool::new(false); + let mut rec = Recorder::new(); + rec.fail_mark = true; + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + let err = engine.process(&item(0), fx.body_for(&plan, 0)).unwrap_err(); + assert!(matches!(err, SinkError::Fatal(ref m) if m.contains("atomically replace"))); + }); + assert_eq!(fs::read(fx.install.join("c.bin")).unwrap(), b"old c"); + assert!(fx.stages().is_empty()); + } + + #[test] + fn an_owned_staging_dir_holds_the_stages_and_is_left_empty() { + let fx = Fixture::new("stream-staging"); + let staging = fx.install.join(".droiddeck-epic-staging"); + fs::create_dir_all(&staging).unwrap(); + let mut req = fx.request(vec![0, 1, 2], 1 << 20); + req.staging_dir = staging.to_str().unwrap().to_string(); + let plan = build_stream_plan(&req).unwrap(); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + engine.process(&item(0), fx.body_for(&plan, 0)).unwrap(); + let stages: Vec<_> = fs::read_dir(&staging).unwrap().flatten().collect(); + assert_eq!(stages.len(), 1, "a.bin is staged in the owned directory"); + assert!(stages[0].file_name().to_string_lossy().ends_with(".stage")); + for id in 1..plan.fetch_events.len() { + engine.process(&item(id), fx.body_for(&plan, id)).unwrap(); + } + }); + assert_eq!(fs::read_dir(&staging).unwrap().count(), 0); + assert_eq!(fs::read(fx.install.join("dir/b.bin")).unwrap(), fx.expected(1)); + let mut missing = fx.request(vec![0], 1 << 20); + missing.staging_dir = fx.dir.join("nope").to_str().unwrap().to_string(); + assert!(build_stream_plan(&missing).unwrap_err().contains("staging dir")); + } + + #[test] + fn a_manifest_file_named_like_a_stage_is_never_used_for_staging() { + let fx = Fixture::new("stream-reserved"); + let plan = build_stream_plan(&fx.request(vec![0, 1, 2], 1 << 20)).unwrap(); + assert!(plan.reserved.contains(&fx.install.join("dir/b.bin"))); + assert!(plan.reserved.contains(&fx.install.join("a.bin"))); + } + + // ── replacement-overlap gate ── + + /// `n` files of `len` bytes, one chunk each. + fn big_fixture(tag: &str, n: usize, len: usize) -> Fixture { + let dir = temp_dir(tag); + let install = dir.join("game"); + fs::create_dir_all(&install).unwrap(); + let guid = |i: usize| [0x100 + i as u32, 0, 0, 0]; + Fixture { + dir, + install, + chunks: (0..n).map(|i| (guid(i), data(i as u32 + 10, len))).collect(), + files: (0..n) + .map(|i| (format!("f{i}.bin"), vec![(guid(i), 0, len as i32)])) + .collect(), + } + } + + /// Drive the engine the way the fetch core does: items in order while the gate allows, + /// bodies handed back newest first (the most out-of-order the core can be). + fn drive_gated(fx: &Fixture, plan: &StreamPlan, engine: &Engine) -> Result<(), SinkError> { + engine.start()?; + let mut next = 0; + let mut in_flight: Vec<usize> = Vec::new(); + while next < plan.fetch_events.len() || !in_flight.is_empty() { + while next < plan.fetch_events.len() && engine.may_dispatch(next) { + in_flight.push(next); + next += 1; + } + let id = in_flight.pop().expect("gate closed with nothing in flight: deadlock"); + engine.process(&item(id), fx.body_for(plan, id))?; + } + Ok(()) + } + + fn write_old(fx: &Fixture, files: &[usize]) { + for &i in files { + fs::write(fx.install.join(&fx.files[i].0), b"old").unwrap(); + } + } + + #[test] + fn replaced_files_stay_within_the_overlap_budget_and_complete() { + let fx = big_fixture("stream-overlap", 6, 1000); + write_old(&fx, &[0, 1, 2, 3, 4, 5]); + let mut req = fx.request((0..6).collect(), 1 << 20); + req.replace_overlap_budget = 2500; + let plan = build_stream_plan(&req).unwrap(); + assert!(plan.files.iter().all(|f| f.replaces)); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + assert!(engine.may_dispatch(1), "two 1000-byte files fit 2500"); + assert!(!engine.may_dispatch(2), "a third does not"); + drive_gated(&fx, &plan, engine).unwrap(); + assert_eq!(engine.peak_replace_overlap(), 2000); + assert_eq!(engine.unfinished_files(), 0); + }); + for i in 0..6 { + assert_eq!(fs::read(fx.install.join(&fx.files[i].0)).unwrap(), fx.expected(i)); + } + assert!(fx.stages().is_empty()); + } + + #[test] + fn a_budget_below_the_largest_file_still_progresses_one_at_a_time() { + let fx = big_fixture("stream-overlap-one", 4, 800); + write_old(&fx, &[0, 1, 2, 3]); + let mut req = fx.request((0..4).collect(), 1 << 20); + req.replace_overlap_budget = 1; + let plan = build_stream_plan(&req).unwrap(); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + assert!(engine.may_dispatch(0)); + assert!(!engine.may_dispatch(1)); + drive_gated(&fx, &plan, engine).unwrap(); + assert_eq!(engine.peak_replace_overlap(), 800); + assert_eq!(engine.unfinished_files(), 0); + }); + for i in 0..4 { + assert_eq!(fs::read(fx.install.join(&fx.files[i].0)).unwrap(), fx.expected(i)); + } + } + + #[test] + fn a_fresh_install_is_never_gated() { + let fx = big_fixture("stream-overlap-fresh", 5, 600); + let mut req = fx.request((0..5).collect(), 1 << 20); + req.replace_overlap_budget = 1; + let plan = build_stream_plan(&req).unwrap(); + assert!(plan.files.iter().all(|f| !f.replaces)); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + assert!((0..5).all(|id| engine.may_dispatch(id))); + drive_gated(&fx, &plan, engine).unwrap(); + assert_eq!(engine.peak_replace_overlap(), 0); + }); + assert_eq!(fs::read(fx.install.join("f4.bin")).unwrap(), fx.expected(4)); + } + + #[test] + fn mixed_fresh_and_replaced_files_gate_only_the_replacements() { + let fx = big_fixture("stream-overlap-mixed", 6, 700); + write_old(&fx, &[1, 3, 5]); + let mut req = fx.request((0..6).collect(), 1 << 20); + req.replace_overlap_budget = 700; + let plan = build_stream_plan(&req).unwrap(); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + // f0 fresh, f1 replaced (admitted), f2 fresh, f3 replaced (waits for f1's swap). + assert!(engine.may_dispatch(2)); + assert!(!engine.may_dispatch(3)); + drive_gated(&fx, &plan, engine).unwrap(); + assert_eq!(engine.peak_replace_overlap(), 700); + assert_eq!(engine.unfinished_files(), 0); + }); + for i in 0..6 { + assert_eq!(fs::read(fx.install.join(&fx.files[i].0)).unwrap(), fx.expected(i)); + } + } + + #[test] + fn a_shared_chunk_for_a_gated_file_waits_in_the_cache() { + // f0 and f2 are replaced and share chunk A; f1 is fresh. With room for one replaced file, + // f2 is admitted only after f0's swap, and A is cached until then. + let dir = temp_dir("stream-overlap-shared"); + let install = dir.join("game"); + fs::create_dir_all(&install).unwrap(); + let fx = Fixture { + dir, + install, + chunks: vec![(A, data(1, 300)), (B, data(2, 50))], + files: vec![ + ("f0.bin".to_string(), vec![(A, 0, 100)]), + ("f1.bin".to_string(), vec![(B, 0, 50)]), + ("f2.bin".to_string(), vec![(A, 100, 200)]), + ], + }; + write_old(&fx, &[0, 2]); + let mut req = fx.request(vec![0, 1, 2], 1 << 20); + req.replace_overlap_budget = 150; + let plan = build_stream_plan(&req).unwrap(); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + drive_gated(&fx, &plan, engine).unwrap(); + assert!(engine.peak_replace_overlap() <= 200); + assert_eq!(engine.unfinished_files(), 0); + }); + for i in 0..3 { + assert_eq!(fs::read(fx.install.join(&fx.files[i].0)).unwrap(), fx.expected(i)); + } + assert_eq!(fs::read_dir(&plan.cache_dir).unwrap().count(), 0); + } + + #[test] + fn cancelling_a_gated_update_keeps_every_old_file() { + let fx = big_fixture("stream-overlap-cancel", 4, 900); + write_old(&fx, &[0, 1, 2, 3]); + let mut req = fx.request((0..4).collect(), 1 << 20); + req.replace_overlap_budget = 1000; + let plan = build_stream_plan(&req).unwrap(); + let cancel = AtomicBool::new(false); + let rec = Recorder::new(); + with_engine(&plan, &cancel, &rec, |engine| { + engine.start().unwrap(); + engine.process(&item(0), fx.body_for(&plan, 0)).unwrap(); // f0 swapped in + assert!(engine.may_dispatch(1)); + cancel.store(true, Ordering::Relaxed); + assert!(engine.process(&item(1), fx.body_for(&plan, 1)).is_err()); + engine.discard_staging(); + }); + assert_eq!(fs::read(fx.install.join("f0.bin")).unwrap(), fx.expected(0)); + for i in 1..4 { + assert_eq!(fs::read(fx.install.join(&fx.files[i].0)).unwrap(), b"old"); + } + assert!(fx.stages().is_empty()); + } + + fn item(id: usize) -> FetchItem { + FetchItem { + id: id as u64, + urls: vec!["http://127.0.0.1:9/x".to_string()], + reserve: 0, + range: None, + } + } + + fn guid_to(g: [u32; 4]) -> String { + super::super::manifest::guid_to_string(&g) + } +} diff --git a/app/src/main/rust/stores/src/fetch_core.rs b/app/src/main/rust/stores/src/fetch_core.rs index e700ee3a2..d0a5fa36c 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], @@ -221,6 +117,25 @@ pub fn run_fetch( cancel: &AtomicBool, progress: &(dyn Fn(u64, u64) + Sync), log: &(dyn Fn(&str) + Sync), +) -> FetchOutcome { + run_fetch_gated(items, hosts, opts, sink, cancel, progress, log, &|_: usize| true) +} + +/// [`run_fetch`] with an admission gate: the next fresh item (by index) is only dispatched once +/// `gate(index)` says so. Items are still taken strictly in order, and retries are never held +/// back. While the gate is closed the driver idles as it does on a full window, so a gate that +/// opens as the sink makes progress (e.g. as files finish) paces the run without stalling the +/// requests already in flight. The adaptivity is unchanged. +#[allow(clippy::too_many_arguments)] +pub fn run_fetch_gated( + items: Vec<FetchItem>, + hosts: &[String], + opts: &FetchOptions, + sink: &dyn FetchSink, + cancel: &AtomicBool, + progress: &(dyn Fn(u64, u64) + Sync), + log: &(dyn Fn(&str) + Sync), + gate: &(dyn Fn(usize) -> bool + Sync), ) -> FetchOutcome { let started = Instant::now(); let label = opts.label.as_str(); @@ -254,8 +169,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 +181,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 +191,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 +198,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 +217,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 +246,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 +254,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 +289,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 +370,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) @@ -484,6 +378,7 @@ mode={}", Ok(rt) => { rt.block_on(run_driver(DriverCtx { items: items.as_slice(), + gate, hosts: hosts.as_slice(), opts, label, @@ -515,7 +410,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 +446,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 +455,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 +467,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 +477,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 +492,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 +501,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 +535,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 +549,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 +559,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 +568,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 +601,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 +623,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 +652,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 +698,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 +730,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 +789,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 +826,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,24 +838,21 @@ 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>, } struct DriverCtx<'a> { items: &'a [FetchItem], + gate: &'a (dyn Fn(usize) -> bool + Sync), hosts: &'a [String], opts: &'a FetchOptions, label: &'a str, @@ -1027,6 +874,7 @@ struct DriverCtx<'a> { async fn run_driver(ctx: DriverCtx<'_>) { let DriverCtx { items, + gate, hosts, opts, label, @@ -1061,11 +909,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 +919,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 +928,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 +964,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 +1003,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,31 +1015,26 @@ 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"); (p.idx, p.attempts) - } else if next_item < items.len() { + } else if next_item < items.len() && gate(next_item) { (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 +1051,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 +1063,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 +1099,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 +1110,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 +1125,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 +1144,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 +1164,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 +1179,7 @@ async fn run_driver(ctx: DriverCtx<'_>) { }) .is_err() { - break; // process side gone + break; } } Ok(Fetched::Streamed(total_len)) => { @@ -1374,7 +1196,7 @@ async fn run_driver(ctx: DriverCtx<'_>) { }) .is_err() { - break; // process side gone + break; } } Err(err) => { @@ -1423,14 +1245,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 +1256,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 +1292,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 +1342,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 +1351,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 +1440,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 +1513,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 +1534,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 +1600,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 +1680,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 +1704,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 +1804,6 @@ stalled_hosts={}", } } -// ───────────────────────────────────────────────────────────────────────────────────────────── -// Tests -// ───────────────────────────────────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { @@ -2154,8 +1943,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 +1961,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 +1972,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 +1984,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 +1995,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 +2078,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 +2224,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 +2312,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 +2351,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 +2370,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/agent/AgentSecurityTest.kt b/app/src/test/java/com/droiddeck/launcher/agent/AgentSecurityTest.kt index 499645c3e..df4bb15fc 100644 --- a/app/src/test/java/com/droiddeck/launcher/agent/AgentSecurityTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/agent/AgentSecurityTest.kt @@ -7,6 +7,7 @@ import android.util.Base64 import androidx.compose.ui.semantics.SemanticsConfiguration import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.text.AnnotatedString +import com.droiddeck.launcher.R import com.droiddeck.launcher.runtime.LinuxRuntime import org.json.JSONObject import org.junit.Assert.* @@ -68,6 +69,13 @@ class AgentSecurityTest { assertFalse(AgentAccess.commandsAllowed(context)) } + @Test fun theRefusalNamesTheSetupTabThatHoldsTheToggle() { + val refusal = refused("AGENT_COMMANDS_DISABLED") { AgentAccess.requireCommands(context) } + val tab = context.getString(R.string.setup_tab_diagnostics) + val toggle = context.getString(R.string.setup_agent_commands) + assertTrue(refusal.message, refusal.message!!.contains("Setup > $tab > $toggle")) + } + @Test fun runCannotBypassTheToggle() { refused("AGENT_COMMANDS_DISABLED") { AgentAccess.requireStart(context, JSONObject().put("mode", "run")) } AgentAccess.requireStart(context, JSONObject().put("mode", "steam")) diff --git a/app/src/test/java/com/droiddeck/launcher/core/AppLanguageTest.kt b/app/src/test/java/com/droiddeck/launcher/core/AppLanguageTest.kt new file mode 100644 index 000000000..552e571fb --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/core/AppLanguageTest.kt @@ -0,0 +1,57 @@ +package com.droiddeck.launcher.core + +import java.io.File +import java.util.Locale +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class AppLanguageTest { + private fun tag(languageTag: String) = AppLanguage.supportedTag(Locale.forLanguageTag(languageTag)) + + private val folders = mapOf( + "id" to "in", "ms" to "ms", "cs" to "cs", "da" to "da", "de" to "de", "es" to "es", + "es-419" to "b+es+419", "fr" to "fr", "it" to "it", "hu" to "hu", "nl" to "nl", "nb" to "nb", + "pl" to "pl", "pt-BR" to "pt", "pt-PT" to "pt-rPT", "ro" to "ro", "fi" to "fi", "sv" to "sv", + "vi" to "vi", "tr" to "tr", "el" to "el", "bg" to "bg", "ru" to "ru", "uk" to "uk", "ar" to "ar", + "hi" to "hi", "th" to "th", "ja" to "ja", "ko" to "ko", "zh-CN" to "zh-rCN", "zh-TW" to "zh-rTW", + "zh-HK" to "zh-rHK", + ) + + @Test fun everyLanguageReadsAsItself() { + AppLanguage.supported.forEach { assertEquals(it, tag(it)) } + } + + @Test fun everyLanguageHasItsOwnName() { + val names = AppLanguage.supported.map(AppLanguage::nativeName) + assertEquals(names.size, names.toSet().size) + AppLanguage.supported.forEach { assertTrue(it, AppLanguage.nativeName(it) != it) } + } + + @Test fun systemLocalesFindTheirLanguage() { + assertEquals("es-419", tag("es-MX")) + assertEquals("es-419", tag("es-US")) + assertEquals("es", tag("es-ES")) + assertEquals("pt-BR", tag("pt-BR")) + assertEquals("pt-BR", tag("pt")) + assertEquals("pt-PT", tag("pt-PT")) + assertEquals("pt-PT", tag("pt-AO")) + assertEquals("id", tag("in-ID")) + assertEquals("id", tag("id-ID")) + assertEquals("de", tag("de-AT")) + assertEquals("ar", tag("ar-EG")) + assertEquals("zh-HK", tag("zh-MO")) + assertNull(tag("sw-KE")) + assertNull(tag("nn-NO")) + } + + @Test fun everyLanguageShipsStringsAndIsOfferedToAndroid() { + assertEquals(AppLanguage.supported.toSet() - "en", folders.keys) + val res = File("src/main/res") + folders.values.forEach { assertTrue(it, File(res, "values-$it/strings.xml").isFile) } + val config = File(res, "xml/locales_config.xml").readText() + val offered = Regex("""android:name="([^"]+)"""").findAll(config).map { it.groupValues[1] }.toList() + assertEquals(AppLanguage.supported, offered) + } +} 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/core/ZramSupportTest.kt b/app/src/test/java/com/droiddeck/launcher/core/ZramSupportTest.kt new file mode 100644 index 000000000..056e3a055 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/core/ZramSupportTest.kt @@ -0,0 +1,37 @@ +package com.droiddeck.launcher.core + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class ZramSupportTest { + @Test + fun kernelVersionsFromAndroidReleases() { + assertTrue(ZramSupport.kernelAtLeast("6.12.38-android16-5-g665eafb62659-ab14778838-4k", 5, 4)) + assertTrue(ZramSupport.kernelAtLeast("5.4.0", 5, 4)) + assertFalse(ZramSupport.kernelAtLeast("4.19.157-perf+", 5, 4)) + assertFalse(ZramSupport.kernelAtLeast("", 5, 4)) + } + + @Test + fun underTwelveGbFromAdvertisedOrTotalMemory() { + val gb = 1_000_000_000L + val gib = 1L shl 30 + assertTrue(ZramSupport.belowTwelveGb(8 * gb, 0)) + assertTrue(ZramSupport.belowTwelveGb(8 * gib, 0)) + assertFalse(ZramSupport.belowTwelveGb(12 * gb, 0)) + assertFalse(ZramSupport.belowTwelveGb(12 * gib, 0)) + assertTrue(ZramSupport.belowTwelveGb(0, 7_400_000L * 1024)) + assertTrue(ZramSupport.belowTwelveGb(0, 9_300_000L * 1024)) + assertFalse(ZramSupport.belowTwelveGb(0, 11_100_000L * 1024)) + assertFalse(ZramSupport.belowTwelveGb(0, 15_600_380L * 1024)) + } + + @Test + fun swapTotalComesFromMeminfo() { + assertEquals(12582908L, ZramSupport.swapTotalKb(listOf("MemTotal: 15600380 kB", "SwapTotal: 12582908 kB"))) + assertEquals(0L, ZramSupport.swapTotalKb(listOf("SwapTotal: 0 kB"))) + assertEquals(0L, ZramSupport.swapTotalKb(emptyList())) + } +} 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 287a1ce71..40646d6b0 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/KeyboardHostTest.kt b/app/src/test/java/com/droiddeck/launcher/input/KeyboardHostTest.kt new file mode 100644 index 000000000..eff919752 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/input/KeyboardHostTest.kt @@ -0,0 +1,55 @@ +package com.droiddeck.launcher.input + +import android.app.Activity +import android.content.Context +import android.os.Looper +import android.view.inputmethod.InputMethodManager +import android.widget.FrameLayout +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class KeyboardHostTest { + private lateinit var imm: InputMethodManager + private lateinit var host: KeyboardHost + + @Before fun setUp() { + val activity = Robolectric.buildActivity(Activity::class.java).setup().get() + imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + host = KeyboardHost(activity) + activity.setContentView(FrameLayout(activity).apply { addView(host) }) + idle() + } + + @Config(sdk = [33]) + @Test fun toggleShowsAKeyboardTheSystemDismissed() { + host.show() + idle() + imm.hideSoftInputFromWindow(host.windowToken, 0) + idle() + host.toggle() + idle() + assertTrue(shadowOf(imm).isSoftInputVisible) + assertTrue(host.shown) + } + + @Config(sdk = [28]) + @Test fun toggleHidesAKeyboardThatIsShowing() { + host.show() + idle() + host.toggle() + idle() + assertFalse(shadowOf(imm).isSoftInputVisible) + assertFalse(host.shown) + } + + private fun idle() = shadowOf(Looper.getMainLooper()).idle() +} 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 5677b1519..add675539 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/QuestExperimentalTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/QuestExperimentalTest.kt @@ -23,15 +23,32 @@ class QuestExperimentalTest { ShadowBuild.setDevice("phone") } - @Test fun otherDevicesAndQuest2KeepTheirSettings() { - for (model in listOf("Phone", "Quest 2")) { + @Test fun otherDevicesKeepTheirSettings() { + for (model in listOf("Phone", "Quest", "Quest Pro")) { ShadowBuild.setModel(model) QuestExperimental.initialize(context) assertEquals(SessionPrefs.GPU_DRIVERS_AUTO, SessionPrefs.gpuDriverMode(context)) assertEquals("", SessionPrefs.androidDriver(context)) assertFalse(SessionPrefs.hdr(context, "steam")) } - assertFalse(QuestExperimental.supported("Quest 2", "hollywood", "Oculus")) + assertFalse(QuestExperimental.supported("Phone", "hollywood", "Other")) + } + + @Test fun quest2SelectsA6xxInsteadOfQuest3DriverAndPreservesLaterChoices() { + ShadowBuild.setModel("Quest 2") + context.getSharedPreferences("session", Context.MODE_PRIVATE).edit() + .putBoolean("questDefaultsV1", true).commit() + QuestExperimental.initialize(context) + assertEquals(SessionPrefs.GPU_DRIVERS_MANUAL, SessionPrefs.gpuDriverMode(context)) + assertEquals(QuestExperimental.DRIVER_A6XX, SessionPrefs.androidDriver(context)) + assertEquals("", SessionPrefs.linuxDriver(context)) + assertTrue(SessionPrefs.hdr(context, "steam")) + assertTrue(QuestExperimental.supported("unknown", "hollywood", "Oculus")) + assertTrue(QuestExperimental.supported("unknown", "hollywood", "Meta")) + assertFalse(QuestExperimental.quest2("Quest 3", "eureka", "Meta")) + SessionPrefs.setAndroidDriver(context, "chosen-a6xx-driver") + QuestExperimental.initialize(context) + assertEquals("chosen-a6xx-driver", SessionPrefs.androidDriver(context)) } @Test fun questDefaultsSelectBothCompatibilityPathsAndAreAppliedOnlyOnce() { 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/SettingsResetTest.kt b/app/src/test/java/com/droiddeck/launcher/session/SettingsResetTest.kt new file mode 100644 index 000000000..e636bee70 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/session/SettingsResetTest.kt @@ -0,0 +1,96 @@ +package com.droiddeck.launcher.session + +import android.content.Context +import com.droiddeck.launcher.core.AppUiPrefs +import com.droiddeck.launcher.gpu.FrameGen +import com.droiddeck.launcher.input.ControllerPrefs +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33]) +class SettingsResetTest { + private val app get() = RuntimeEnvironment.getApplication() + private fun prefs(name: String) = app.getSharedPreferences(name, Context.MODE_PRIVATE) + private val session get() = prefs("session") + + @Before fun clear() { + listOf("session", "controller", "app-ui", "frame_gen", "steam_touch", "agent").forEach { + prefs(it).edit().clear().commit() + } + } + + @Test fun settingsGoBackToTheirDefaults() { + SessionPrefs.setTheme(app, "ocean") + SessionPrefs.setNoXalia(app, false) + SessionPrefs.setFpsLimit(app, SessionService.MODE_STEAM, 30) + SessionPrefs.setHdr(app, SessionService.MODE_DESKTOP, true) + SessionPrefs.setResolutionChoice(app, SessionService.MODE_STEAM, "1280x800") + SessionPrefs.setGuestHostname(app, "MyDeck") + ControllerPrefs.setOpacity(app, 40) + AppUiPrefs.setScale(app, 150) + FrameGen.set(app, FrameGen.Mode(FrameGen.ENGINE_WINFG, 3)) + prefs("steam_touch").edit().putBoolean("gyro", false).commit() + + assertTrue(SettingsReset.resetAll(app, gameProfiles = false)) + + assertEquals("graphite", SessionPrefs.theme(app)) + assertTrue(SessionPrefs.noXalia(app)) + assertEquals(0, SessionPrefs.fpsLimit(app, SessionService.MODE_STEAM)) + assertFalse(SessionPrefs.hdr(app, SessionService.MODE_DESKTOP)) + assertFalse(SessionPrefs.resolutionChosen(app, SessionService.MODE_STEAM)) + assertEquals(SessionPrefs.DEFAULT_GUEST_HOSTNAME, SessionPrefs.guestHostname(app)) + assertEquals(100, ControllerPrefs.read(app).opacity) + assertEquals(AppUiPrefs.DEFAULT_SCALE, AppUiPrefs.scale(app)) + assertEquals(FrameGen.Mode.OFF.engine, FrameGen.engine(app)) + assertFalse(prefs("steam_touch").contains("gyro")) + } + + @Test fun libraryAndRecordsAreKept() { + val folder = "/storage/emulated/0/Games/Foo" + SessionPrefs.setAddedGameName(app, folder, "Foo") + SessionPrefs.setAddedGameExe(app, folder, "$folder/foo.exe") + SessionPrefs.addedGameAppId(app, folder, 1234L) + SessionPrefs.setAddedGameRemoved(app, "/storage/emulated/0/Games/Bar", true) + SessionPrefs.setGamesFoldersMigrated(app) + SessionPrefs.setGpuAutoInstalled(app, setOf("turnip-a")) + SessionPrefs.setRomsDir(app, "/storage/emulated/0/ROMs") + SessionPrefs.setGameStorage(app, "/storage/ABCD-1234", "SD Card") + SessionPrefs.setMicAsked(app) + prefs("steam_touch").edit().putString("copiedLayout", "a:1,2").commit() + + SettingsReset.resetAll(app, gameProfiles = false) + + assertEquals("Foo", SessionPrefs.addedGameName(app, folder)) + assertEquals("$folder/foo.exe", SessionPrefs.addedGameExe(app, folder)) + assertEquals(1234L, SessionPrefs.addedGameStoredAppId(app, folder)) + assertEquals(listOf("/storage/emulated/0/Games/Bar"), SessionPrefs.removedAddedGames(app)) + assertTrue(SessionPrefs.gamesFoldersMigrated(app)) + assertEquals(setOf("turnip-a"), SessionPrefs.gpuAutoInstalled(app)) + assertEquals("/storage/emulated/0/ROMs", SessionPrefs.romsDir(app)) + assertEquals("/storage/ABCD-1234", SessionPrefs.gameStorage(app)) + assertTrue(SessionPrefs.micAsked(app)) + assertEquals("a:1,2", prefs("steam_touch").getString("copiedLayout", null)) + } + + @Test fun gameTextureFilteringIsKeptUnlessGameProfilesAreReset() { + session.edit().putString("gameTextureFiltering", """{"440":{"anisotropy":16}}""").commit() + SettingsReset.resetAll(app, gameProfiles = false) + assertEquals(16, SessionPrefs.gameTextureFiltering(app, "440").anisotropy) + + SettingsReset.resetAll(app, gameProfiles = true) + assertTrue(SessionPrefs.gameTextureFiltering(app).isEmpty()) + } + + @Test fun perModeKeysAreSettings() { + listOf("resolutionCap.steam", "displayResolution.desktop", "customRes.steam", "hdr.steam", "fpsLimit.desktop", "suspendPolicy.steam") + .forEach { assertTrue(it, SettingsReset.isSessionSetting(it)) } + listOf("addedName:/x", "addedAppId:/x", "gpuAutoInstalled", "romsDir", "gameStorage", "micAsked", "gameTextureFiltering") + .forEach { assertFalse(it, SettingsReset.isSessionSetting(it)) } + } +} 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 4e4e30447..0c2a990d8 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/SteamLanguageTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/SteamLanguageTest.kt @@ -18,6 +18,18 @@ class SteamLanguageTest { assertEquals("tchinese", steam("zh-HK")) } + @Test fun everyAppLanguageButHindiHasSteamsOwn() { + com.droiddeck.launcher.core.AppLanguage.supported.forEach { + val steam = steam(it) + assertTrue("$it -> $steam", (steam == "english") == (it == "en" || it == "hi")) + } + assertEquals("brazilian", steam("pt-BR")) + assertEquals("portuguese", steam("pt-PT")) + assertEquals("latam", steam("es-419")) + assertEquals("indonesian", steam("id")) + assertEquals("malay", steam("ms")) + } + @Test fun chineseFollowsTheScriptBeforeTheRegion() { assertEquals("tchinese", steam("zh-Hant-CN")) assertEquals("schinese", steam("zh-Hans-HK")) @@ -30,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..de903c80f 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/StoreInstallRootTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/StoreInstallRootTest.kt @@ -11,8 +11,8 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowStatFs -/** 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 +22,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 +43,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) @@ -59,4 +56,87 @@ class StoreInstallRootTest { assertTrue(dir.absolutePath.startsWith(app.cacheDir.absolutePath)) assertEquals("a_b_c", dir.name) } + + @Test fun estimatedBytesNeededUsesKnownInstallSizeForAllStores() { + val item = CatalogItem(Store.EPIC, "game1", "Game 1", imageUrl = null, owned = true, sizeBytes = 10_000_000L) + assertEquals(10_000_000L, StoreInstallRoot.estimatedBytesNeeded(item)) + + val gogItem = CatalogItem(Store.GOG, "game2", "Game 2", imageUrl = null, owned = true, sizeBytes = 10_000_000L) + assertEquals(10_000_000L, StoreInstallRoot.estimatedBytesNeeded(gogItem)) + } + + @Test fun checkStorageReportsEnoughWhenSpaceIsUnknownOrAvailable() { + val zeroItem = CatalogItem(Store.EPIC, "game1", "Game 1", imageUrl = null, owned = true, sizeBytes = 0L) + val check = StoreInstallRoot.checkStorage(app, zeroItem, StoreInstallRoot.internalRoot(app)) + assertTrue(check.enough) + } + + @Test fun availableBytesUsesNearestExistingAncestorWithoutCreatingDirectories() { + val existing = File(app.filesDir, "storage-probe").apply { mkdirs() } + val missing = File(existing, "not-created/another-level/game") + ShadowStatFs.registerStats(existing.path, 1024, 100, 100) + + assertTrue(StoreInstallRoot.availableBytes(missing) > 0L) + assertFalse(File(existing, "not-created").exists()) + } + + @Test fun epicDefersFullSizeChecksToItsManifestDeltaForResume() { + val item = CatalogItem(Store.EPIC, "large", "Large", imageUrl = null, owned = true, sizeBytes = Long.MAX_VALUE) + val check = StoreInstallRoot.checkStorage(app, item, StoreInstallRoot.internalRoot(app)) + assertTrue(check.enough) + assertEquals(Long.MAX_VALUE, check.requiredBytes) + } + + @Test fun gogAndAmazonResumeChecksUseTheExistingVolumeAndDeferRemainingWork() { + val card = File(app.filesDir, "resume-card").apply { mkdirs() } + SessionPrefs.setGameStorage(app, card.path, "Card") + for (store in listOf(Store.GOG, Store.AMAZON)) { + val item = CatalogItem(store, "resume-${store.id}", "Partial ${store.id}", imageUrl = null, sizeBytes = Long.MAX_VALUE) + val folder = File(StoreInstallRoot.storeDir(File(card, "Games"), store), StoreInstallRoot.folderName(item.title, item.id)).apply { mkdirs() } + StoreInstalls.begin(folder, store, item.id, item.title, null, null) + val check = StoreInstallRoot.checkStorage(app, item, StoreInstallRoot.internalRoot(app)) + assertTrue(check.enough) + assertEquals(StoreInstallRoot.availableBytes(folder), check.availableBytes) + assertEquals(folder, StoreInstallRoot.folderFor(app, store, item.id, item.title)) + } + } + + @Test fun freshGogAndAmazonStillRejectInsufficientCatalogSpace() { + StoreInstallRoot.internalRoot(app).mkdirs() + ShadowStatFs.registerStats(StoreInstallRoot.internalRoot(app).path, 1024, 100, 100) + for (store in listOf(Store.GOG, Store.AMAZON)) { + val item = CatalogItem(store, "too-large-${store.id}", "Too large ${store.id}", imageUrl = null, sizeBytes = Long.MAX_VALUE) + assertFalse(StoreInstallRoot.checkStorage(app, item, StoreInstallRoot.internalRoot(app)).enough) + } + } + + @Test fun measuredZeroRejectsFreshGogAndAmazonRatherThanBeingUnknown() { + val root = File(app.filesDir, "zero-space").apply { mkdirs() } + ShadowStatFs.registerStats(root.path, 1024, 0, 0) + val missing = File(root, "not-created/game") + assertEquals(0L, StoreInstallRoot.availableBytes(missing)) + assertFalse(File(root, "not-created").exists()) + for (store in listOf(Store.GOG, Store.AMAZON)) { + val item = CatalogItem(store, "zero-${store.id}", "Zero ${store.id}", imageUrl = null, sizeBytes = 1L) + val check = StoreInstallRoot.checkStorage(app, item, missing) + assertFalse(check.enough) + assertEquals(0L, check.availableBytes) + } + } + + @Test fun failedProbesAreUnknownAndDoNotCreateOrWriteToTargets() { + val root = File(app.filesDir, "read-only-probe").apply { mkdirs() } + val missing = File(root, "not-created/game") + val fail: (File) -> Long = { throw java.io.IOException("unavailable") } + assertEquals(-1L, StoreInstallRoot.probeAvailableBytes(missing, fail, fail)) + assertEquals(-1L, StoreInstallRoot.probeAvailableBytes(missing, fail, { 0L })) + assertEquals(42L, StoreInstallRoot.probeAvailableBytes(missing, fail, { 42L })) + assertEquals(0L, StoreInstallRoot.probeAvailableBytes(missing, { 0L }, fail)) + assertFalse(File(root, "not-created").exists()) + assertTrue(root.listFiles().orEmpty().isEmpty()) + assertFalse(StoreInstallRoot.insufficientSpace(1L, -1L)) + assertTrue(StoreInstallRoot.insufficientSpace(1L, 0L)) + assertFalse(StoreInstallRoot.insufficientSpace(1L, 1L)) + assertFalse(StoreInstallRoot.insufficientSpace(0L, 0L)) + } } 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/amazon/AmazonResumeSpaceTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/amazon/AmazonResumeSpaceTest.kt new file mode 100644 index 000000000..6382f9519 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/stores/amazon/AmazonResumeSpaceTest.kt @@ -0,0 +1,43 @@ +package com.droiddeck.launcher.stores.amazon + +import java.io.File +import java.security.MessageDigest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowStatFs +import com.droiddeck.launcher.stores.StoreInstallRoot + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33]) +class AmazonResumeSpaceTest { + @Test fun remainingWorkRejectsMeasuredZeroButNotUnknownSpace() { + val root = File(RuntimeEnvironment.getApplication().filesDir, "amazon-zero").apply { mkdirs() } + ShadowStatFs.registerStats(root.path, 1024, 0, 0) + val files = listOf(AmazonManifest.ManifestFile("missing", 4, 0, null)) + val needed = AmazonDownloadManager.remainingInstallBytes(files, root) + assertEquals(4L, needed) + assertTrue(StoreInstallRoot.insufficientSpace(needed, StoreInstallRoot.availableBytes(root))) + assertFalse(StoreInstallRoot.insufficientSpace(needed, StoreInstallRoot.SPACE_UNKNOWN)) + } + + @Test fun remainingSpaceExcludesVerifiedFilesButIncludesCorruptAndPartialFiles() { + val root = File(RuntimeEnvironment.getApplication().filesDir, "amazon-resume").apply { mkdirs() } + val data = byteArrayOf(1, 2, 3, 4) + val hash = MessageDigest.getInstance("SHA-256").digest(data) + val files = listOf("good", "corrupt", "partial", "missing").map { + AmazonManifest.ManifestFile(it, 4, 0, hash) + } + File(root, "good").writeBytes(data) + File(root, "corrupt").writeBytes(ByteArray(4)) + File(root, "partial").writeBytes(byteArrayOf(1)) + assertEquals(12L, AmazonDownloadManager.remainingInstallBytes(files, root)) + for (file in files) File(root, file.unixPath()).writeBytes(data) + assertEquals(0L, AmazonDownloadManager.remainingInstallBytes(files, root)) + } +} 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/download/DownloadQueueRetryTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadQueueRetryTest.kt new file mode 100644 index 000000000..879e8de91 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadQueueRetryTest.kt @@ -0,0 +1,140 @@ +package com.droiddeck.launcher.stores.download + +import android.content.Context +import com.droiddeck.launcher.session.SessionPrefs +import com.droiddeck.launcher.stores.* +import java.io.File +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import org.junit.After +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith +import org.json.JSONArray +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowStatFs + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33]) +class DownloadQueueRetryTest { + private val app get() = RuntimeEnvironment.getApplication() + + @After fun cleanRows() { DownloadQueue.dismissFinished(); StoreSizes.known.clear() } + + private fun awaitFailure(key: String, error: String) { + val until = System.nanoTime() + TimeUnit.SECONDS.toNanos(5) + while (System.nanoTime() < until) { + val entry = DownloadQueue.entry(key) + if (entry?.state == DownloadState.FAILED && entry.error == error) return + Thread.sleep(10) + } + fail("Expected failed real job: ${DownloadQueue.entry(key)}") + } + + @Test fun storageRetryRechecksThenRunsTheRealBackendAndNormalRetryStillWorks() { + StoreInstallRoot.internalRoot(app).mkdirs() + ShadowStatFs.registerStats(StoreInstallRoot.internalRoot(app).path, 1024, 100, 100) + val item = CatalogItem(Store.GOG, "preflight-retry", "Preflight retry", imageUrl = null, sizeBytes = Long.MAX_VALUE) + val runs = AtomicInteger() + var lastTarget: File? = null + val previous = StoresState.backend(Store.GOG) + val backend = object : StoreBackend { + override val store = Store.GOG + override fun signIn(context: Context) {} + override fun signOut(context: Context) {} + override fun syncLibrary(context: Context, force: Boolean) {} + override fun loadShelves(context: Context, force: Boolean) {} + override fun uninstall(context: Context, game: InstalledStoreGame) {} + override fun install(context: Context, item: CatalogItem, root: File) { + lastTarget = root + DownloadQueue.enqueue(context, DownloadEntry(item.store, item.id, item.title)) { + object : DownloadQueue.DownloadJob { + override fun run(handle: DownloadQueue.JobHandle): String? { + throw IllegalStateException("backend-${runs.incrementAndGet()}") + } + override fun cancel(deleteFiles: Boolean) {} + } + } + } + } + StoresState.register(backend) + try { + StoresState.install(app, item) + assertEquals(DownloadState.FAILED, DownloadQueue.entry(item.key)?.state) + DownloadQueue.retry(app, item.key) + assertEquals(DownloadState.FAILED, DownloadQueue.entry(item.key)?.state) + assertEquals(0, runs.get()) + StoreSizes.known[item.key] = 1 + DownloadQueue.retry(app, item.key) + awaitFailure(item.key, "backend-1") + DownloadQueue.retry(app, item.key) + awaitFailure(item.key, "backend-2") + DownloadQueue.clear(app, item.key) + assertNull(DownloadQueue.entry(item.key)) + + // A card resume (including the restored paused path) must retain its real target. + val card = File(app.filesDir, "retry-card").apply { mkdirs() } + SessionPrefs.setGameStorage(app, card.path, "Card") + val target = File(card, "Games") + val folder = File(StoreInstallRoot.storeDir(target, item.store), StoreInstallRoot.folderName(item.title, item.id)).apply { mkdirs() } + StoreInstalls.begin(folder, item.store, item.id, item.title, null, null) + StoreSizes.known[item.key] = Long.MAX_VALUE + StoresState.unfinished = StoreInstallRoot.unfinished(app) + DownloadQueue.holdPaused(item.key) + StoresState.requestInstall(app, item) + assertEquals(target, lastTarget) + assertEquals(DownloadState.PAUSED, DownloadQueue.entry(item.key)?.state) + assertEquals(2, runs.get()) + val saved = File(app.filesDir, "store-queue.json") + val until = System.nanoTime() + TimeUnit.SECONDS.toNanos(5) + var correctSavedRoot = false + while (!correctSavedRoot && System.nanoTime() < until) { + correctSavedRoot = runCatching { + val rows = JSONArray(saved.readText()) + (0 until rows.length()).any { i -> + val row = rows.getJSONObject(i) + row.getJSONObject("item").getString("id") == item.id && + row.getString("root") == target.absolutePath && row.getBoolean("paused") + } + }.getOrDefault(false) + if (!correctSavedRoot) Thread.sleep(10) + } + assertTrue("Paused saved queue must remember the existing card root", correctSavedRoot) + DownloadQueue.clear(app, item.key) + } finally { + if (previous != null) StoresState.register(previous) + SessionPrefs.setGameStorage(app, SessionPrefs.GAME_STORAGE_OFF, "") + StoresState.unfinished = emptyList() + } + } + + @Test fun preflightClearRemovesTheRowWithoutRunningItsRetry() { + val entry = DownloadEntry(Store.AMAZON, "clear-preflight", "Clear") + DownloadQueue.recordFailed(app, entry, "storage") { fail("Clear must not retry") } + DownloadQueue.clear(app, entry.key) + assertNull(DownloadQueue.entry(entry.key)) + } + + @Test fun heldPausedBackendRestoresAndResumesWithoutAPlaceholder() { + val entry = DownloadEntry(Store.GOG, "restored-paused", "Paused") + val ran = CountDownLatch(1) + DownloadQueue.holdPaused(entry.key) + DownloadQueue.enqueue(app, entry) { + object : DownloadQueue.DownloadJob { + override fun run(handle: DownloadQueue.JobHandle): String? { + ran.countDown() + throw IllegalStateException("restored") + } + override fun cancel(deleteFiles: Boolean) {} + } + } + assertEquals(DownloadState.PAUSED, DownloadQueue.entry(entry.key)?.state) + assertEquals(1L, ran.count) + DownloadQueue.resume(app, entry.key) + assertTrue(ran.await(5, TimeUnit.SECONDS)) + awaitFailure(entry.key, "restored") + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicAdaptivePlanTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicAdaptivePlanTest.kt new file mode 100644 index 000000000..fdf46d0df --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicAdaptivePlanTest.kt @@ -0,0 +1,107 @@ +package com.droiddeck.launcher.stores.epic + +import java.io.File +import org.junit.Assert.* +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33]) +class EpicAdaptivePlanTest { + @get:Rule val temp = TemporaryFolder() + private val mib = 1024L * 1024 + private val gib = 1024 * mib + + private fun file(name: String, size: Long) = EpicDownloadManager.FileInfo().apply { + filename = name + var left = size + while (left > 0) { + val part = minOf(left, Int.MAX_VALUE.toLong()) + parts.add(EpicDownloadManager.ChunkPart().apply { this.size = part.toInt() }) + left -= part + } + } + + @Test fun netPeakCreditsEachOldCopyAtItsSwapAndLargestReplacedIgnoresFreshFiles() { + val files = listOf(file("a", 10 * gib), file("b", 2 * gib), file("c", 4 * gib)) + val old = mapOf("a" to 9 * gib, "c" to 5 * gib) + // Running Σ(new − old): +1, +3, +2 GiB. + assertEquals(3 * gib, EpicDownloadManager.settledNetPeak(files, old)) + assertEquals(10 * gib, EpicDownloadManager.largestReplacedFile(files, old)) + assertEquals(16 * gib, EpicDownloadManager.settledNetPeak(files, emptyMap())) + assertEquals(0L, EpicDownloadManager.largestReplacedFile(files, emptyMap())) + assertEquals(0L, EpicDownloadManager.largestReplacedFile(files, mapOf("b" to 0L))) + } + + @Test fun cacheBudgetIsHalfTheHeadroomCappedAndDefaultsWhenSpaceIsUnknown() { + assertEquals(EpicDownloadManager.ADAPTIVE_CACHE_DEFAULT_BYTES, EpicDownloadManager.adaptiveCacheBudget(-1, 0)) + assertEquals(EpicDownloadManager.ADAPTIVE_CACHE_MAX_BYTES, EpicDownloadManager.adaptiveCacheBudget(100 * gib, 10 * gib)) + assertEquals(0L, EpicDownloadManager.adaptiveCacheBudget(gib, gib)) + val free = 1000 * mib + assertEquals((free - 400 * mib - EpicDownloadManager.ADAPTIVE_TRANSIENT_BYTES) / 2, EpicDownloadManager.adaptiveCacheBudget(free, 400 * mib)) + } + + @Test fun freshInstallsHaveNoOverlapLimit() { + val plan = EpicDownloadManager.planAdaptiveSpace(20 * gib, 0, 30 * gib, true, 10 * gib) + assertNull(plan.error) + assertEquals(-1L, plan.overlapBudget) + assertEquals(20 * gib, plan.gameNeed) + } + + @Test fun updateOverlapBudgetIsTheLeftoverAfterNetGrowthAndCache() { + // Card install, cache on internal: the card's leftover after the net growth is the budget. + val roomy = EpicDownloadManager.planAdaptiveSpace(gib, 4 * gib, 20 * gib, true, 10 * gib) + assertNull(roomy.error) + assertFalse(roomy.cacheBesideGame) + assertEquals(19 * gib, roomy.overlapBudget) + assertEquals(20 * gib, roomy.gameNeed) + // Tight: floored at the largest replaced file, one at a time. + val tight = EpicDownloadManager.planAdaptiveSpace(gib, 4 * gib, 5 * gib, true, 10 * gib) + assertNull(tight.error) + assertEquals(4 * gib, tight.overlapBudget) + // Not even one replaced file fits: the shortfall is named. + val short = EpicDownloadManager.planAdaptiveSpace(gib, 4 * gib, 4 * gib, true, 10 * gib) + assertTrue(short.error!!.contains("game's storage")) + assertTrue(short.error!!.contains("short by 1.0 GB")) + // Unknown free space: one replaced file at a time. + assertEquals(4 * gib, EpicDownloadManager.planAdaptiveSpace(gib, 4 * gib, -1, true, -1).overlapBudget) + } + + @Test fun cacheBesideTheGameComesOutOfTheOverlapBudget() { + val plan = EpicDownloadManager.planAdaptiveSpace(gib, 2 * gib, 10 * gib, false, -1) + assertNull(plan.error) + assertTrue(plan.cacheBesideGame) + assertTrue(plan.cacheBudget > 0) + assertEquals(10 * gib - gib - plan.cacheBudget - EpicDownloadManager.ADAPTIVE_TRANSIENT_BYTES, plan.overlapBudget) + assertEquals(10 * gib, plan.gameNeed) + } + + @Test fun fullInternalStorageMovesTheCacheBesideTheGameInsteadOfFailing() { + val plan = EpicDownloadManager.planAdaptiveSpace(20 * gib, 0, 30 * gib, true, 100 * mib) + assertNull(plan.error) + assertTrue(plan.cacheBesideGame) + assertEquals(0L, plan.scratchNeed) + assertTrue(plan.cacheBudget > 0) + assertEquals(20 * gib + plan.cacheBudget + EpicDownloadManager.ADAPTIVE_TRANSIENT_BYTES, plan.gameNeed) + } + + @Test fun ownedStagingIsRemovedOnlyWhenItHoldsJustItsMarker() { + val root = temp.newFolder("game") + val staging = EpicDownloadManager.recoverOwnedStaging(root, emptyList()) + File(staging, "keep.stage").writeText("x") + EpicDownloadManager.releaseOwnedStaging(root) + assertTrue(File(staging, "owner").exists()) + File(staging, "keep.stage").delete() + EpicDownloadManager.releaseOwnedStaging(root) + assertFalse(staging.exists()) + // A directory without the marker is not ours. + staging.mkdir() + File(staging, "owner").writeText("someone else") + EpicDownloadManager.releaseOwnedStaging(root) + assertTrue(staging.exists()) + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicBoundedInstallTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicBoundedInstallTest.kt new file mode 100644 index 000000000..37dd7d849 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicBoundedInstallTest.kt @@ -0,0 +1,646 @@ +package com.droiddeck.launcher.stores.epic + +import com.droiddeck.launcher.stores.Store +import com.droiddeck.launcher.stores.StoreGameSidecar +import com.droiddeck.launcher.stores.StoreInstalls +import java.io.File +import java.net.InetAddress +import java.net.ServerSocket +import java.net.SocketException +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.security.MessageDigest +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.net.Socket +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowStatFs + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33]) +class EpicBoundedInstallTest { + @get:Rule val temp = TemporaryFolder() + private lateinit var server: ServerSocket + private lateinit var worker: Thread + private lateinit var manifest: EpicDownloadManager.Manifest + private lateinit var root: File + private val requests = AtomicInteger() + private val reject = AtomicBoolean() + private val rejectGuid = AtomicInteger() + private val handlers = Executors.newCachedThreadPool() + private val sockets = ConcurrentHashMap.newKeySet<Socket>() + private var requestBarrier: CountDownLatch? = null + private var deliveryGate: CountDownLatch? = null + private var bytes = ByteArray(256 * 1024) { (it % 251).toByte() } + + @Before fun setUp() { + root = temp.newFolder("game") + ShadowStatFs.registerStats(root.path, 1_000_000_000, 1_000_000_000, 1_000_000_000) + server = ServerSocket(0, 8, InetAddress.getByName("127.0.0.1")) + worker = Thread { + while (!server.isClosed) { + val socket = try { server.accept() } catch (e: SocketException) { + if (server.isClosed) break else throw e + } + sockets.add(socket) + handlers.submit { + try { + socket.use { + it.soTimeout = 5000 + val reader = it.getInputStream().bufferedReader() + val request = reader.readLine() + while (!reader.readLine().isNullOrEmpty()) { } + requests.incrementAndGet() + requestBarrier?.let { barrier -> + barrier.countDown() + check(barrier.await(5, TimeUnit.SECONDS)) { "Requests did not overlap" } + } + val out = it.getOutputStream() + val rejectedGuid = rejectGuid.get() + if (reject.get() || (rejectedGuid != 0 && request.contains("_%08X".format(rejectedGuid)))) { + out.write("HTTP/1.1 503 Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n".toByteArray()) + } else { + val header = ByteBuffer.allocate(41).order(ByteOrder.LITTLE_ENDIAN) + .putInt(0xB1FE3AA2.toInt()).putInt(1).putInt(41).putInt(bytes.size) + .put(ByteArray(24)).put(0).array() + out.write("HTTP/1.1 200 OK\r\nContent-Length: ${header.size + bytes.size}\r\nConnection: close\r\n\r\n".toByteArray()) + out.write(header) + out.flush() + deliveryGate?.await(5, TimeUnit.SECONDS) + out.write(bytes) + } + out.flush() + } + } catch (_: java.io.IOException) { + // Cancellation disconnects an owned request while this fixture serves it. + } finally { sockets.remove(socket) } + } + } + }.apply { start() } + val chunk = EpicDownloadManager.ChunkInfo().apply { + guid = intArrayOf(1, 0, 0, 0) + windowSize = bytes.size + fileSize = bytes.size.toLong() + sha1 = sha(bytes) + } + manifest = EpicDownloadManager.Manifest().apply { + chunkDir = "chunks" + uniqueChunks = arrayListOf(chunk) + cdnUrls = listOf(EpicDownloadManager.CdnUrl("http://127.0.0.1:${server.localPort}", "", "")) + files = arrayListOf(gameFile("game.bin", 0, bytes.size / 2), gameFile("game.bin.part", bytes.size / 2, bytes.size / 2)) + } + } + + @After fun tearDown() { + deliveryGate?.countDown() + server.close() + worker.join(6000) + sockets.forEach { it.close() } + handlers.shutdownNow() + assertTrue(handlers.awaitTermination(6, TimeUnit.SECONDS)) + assertFalse(worker.isAlive) + } + + private fun sha(data: ByteArray) = MessageDigest.getInstance("SHA-1").digest(data) + + private fun gameFile(name: String, offset: Int, size: Int) = EpicDownloadManager.FileInfo().apply { + filename = name + sha1 = sha(bytes.copyOfRange(offset, offset + size)) + parts.add(EpicDownloadManager.ChunkPart().apply { + guid = intArrayOf(1, 0, 0, 0) + this.offset = offset + this.size = size + }) + } + + private fun install(cancel: AtomicBoolean = AtomicBoolean(), callback: EpicDownloadManager.Callback = + EpicDownloadManager.Callback { _, _ -> }, cache: String = "") = + EpicDownloadManager.installManifest(manifest, root.path, cache, null, cancel, callback) + + private fun oldInstall() { + File(root, "game.bin").writeBytes(byteArrayOf(9, 8, 7)) + File(root, "game.bin.part").writeBytes(byteArrayOf(6, 5, 4)) + StoreGameSidecar(Store.EPIC, "game", "Game", "game.bin", installVersion = "old").write(root) + } + + private fun assertOldInstallIntact() { + assertArrayEquals(byteArrayOf(9, 8, 7), File(root, "game.bin").readBytes()) + assertArrayEquals(byteArrayOf(6, 5, 4), File(root, "game.bin.part").readBytes()) + assertEquals("old", StoreGameSidecar.read(root)?.installVersion) + assertTrue(StoreGameSidecar.read(root)?.isInstalled == true) + assertTrue(root.walk().none { it.name.endsWith(".stage") }) + } + + @Test fun networkFailureAfterAcceptedPreflightPreservesInstalledFilesAndRecord() { + oldInstall() + reject.set(true) + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertTrue(requests.get() > 0) + assertOldInstallIntact() + } + + @Test fun hashFailureAfterAcceptedPreflightPreservesInstalledFilesAndRecord() { + oldInstall() + manifest.files.first().sha1 = ByteArray(20) { 1 } + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertOldInstallIntact() + } + + @Test fun cancellationWhileStagingPreservesInstalledFilesAndRecord() { + oldInstall() + val cancel = AtomicBoolean() + assertNull(install(cancel, EpicDownloadManager.Callback { message, _ -> + if (message.startsWith("Downloading chunks (")) cancel.set(true) + })) + assertOldInstallIntact() + } + + @Test fun laterFailureRetainsVerifiedSwapsAndMarksMixedUpdateResumable() { + oldInstall() + manifest.files.last().sha1 = ByteArray(20) { 1 } + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertArrayEquals(bytes.copyOfRange(0, bytes.size / 2), File(root, "game.bin").readBytes()) + assertArrayEquals(byteArrayOf(6, 5, 4), File(root, "game.bin.part").readBytes()) + val record = StoreGameSidecar.read(root)!! + assertEquals(StoreGameSidecar.STATE_UPDATING, record.state) + assertFalse(record.isInstalled) + assertEquals("", record.installVersion) + StoreInstalls.begin(root, Store.EPIC, "game", "Game", null, null) + assertEquals("game.bin", StoreGameSidecar.read(root)?.exe) + assertEquals(StoreGameSidecar.STATE_UPDATING, StoreGameSidecar.read(root)?.state) + } + + @Test fun laterNetworkFailurePreservesTheUnreplacedOldFile() { + oldInstall() + val secondChunk = EpicDownloadManager.ChunkInfo().apply { + guid = intArrayOf(2, 0, 0, 0) + windowSize = bytes.size + fileSize = bytes.size.toLong() + sha1 = sha(bytes) + } + manifest.uniqueChunks.add(secondChunk) + manifest.files.last().parts.single().guid = secondChunk.guid + // Prefetch may fail early, but replacement is still ordered by the writer. + rejectGuid.set(2) + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertArrayEquals(bytes.copyOfRange(0, bytes.size / 2), File(root, "game.bin").readBytes()) + assertArrayEquals(byteArrayOf(6, 5, 4), File(root, "game.bin.part").readBytes()) + assertEquals(StoreGameSidecar.STATE_UPDATING, StoreGameSidecar.read(root)?.state) + assertTrue(root.walk().none { it.name.endsWith(".stage") }) + } + + @Test fun cancellationAfterFirstSwapLeavesAResumableMixedUpdate() { + oldInstall() + val cancel = AtomicBoolean() + val callback = object : EpicDownloadManager.Callback { + override fun onProgress(message: String, pct: Int) {} + override fun onStage(stage: String, done: Long, total: Long, items: Int, itemsTotal: Int) { + if (stage == "stream" && items == 1) cancel.set(true) + } + } + assertNull(install(cancel, callback)) + assertArrayEquals(bytes.copyOfRange(0, bytes.size / 2), File(root, "game.bin").readBytes()) + assertArrayEquals(byteArrayOf(6, 5, 4), File(root, "game.bin.part").readBytes()) + assertEquals(StoreGameSidecar.STATE_UPDATING, StoreGameSidecar.read(root)?.state) + assertNotNull(install()) + assertArrayEquals(bytes.copyOfRange(bytes.size / 2, bytes.size), File(root, "game.bin.part").readBytes()) + // Registration, not an individual file swap, restores the installed state. + assertFalse(StoreGameSidecar.read(root)!!.isInstalled) + } + + @Test fun atomicRenameFailurePreservesDestinationAndCleansStagingWithoutDeleteFallback() { + val destination = File(root, "game.bin").apply { mkdir() } + File(destination, "old-content").writeText("keep") + val error = assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertTrue(error.message.orEmpty().contains("atomically replace")) + assertEquals("keep", File(destination, "old-content").readText()) + assertTrue(root.walk().none { it.name.endsWith(".stage") }) + } + + @Test fun safeStagingSpaceBoundaryRejectsWithoutReclaimingAllOldOutputs() { + // The first staged half-file must coexist with the shared chunk and BOTH old outputs. + val old = ByteArray(bytes.size / 2) { 9 } + File(root, "game.bin").writeBytes(old) + File(root, "game.bin.part").writeBytes(old) + val peak = bytes.size + old.size + ShadowStatFs.registerStats(root.path, 1024, peak / 4096 - 1, peak / 4096 - 1) + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertEquals(0, requests.get()) + assertArrayEquals(old, File(root, "game.bin").readBytes()) + assertArrayEquals(old, File(root, "game.bin.part").readBytes()) + ShadowStatFs.registerStats(root.path, 1024, peak / 4096, peak / 4096) + assertNotNull(install()) + } + + @Test fun zeroSpaceRejectsPendingFilesWhileUnknownSpaceIsNotTreatedAsZero() { + oldInstall() + ShadowStatFs.registerStats(root.path, 1024, 0, 0) + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertOldInstallIntact() + assertEquals(0, requests.get()) + assertTrue(com.droiddeck.launcher.stores.StoreInstallRoot.insufficientSpace(1, 0)) + assertFalse(com.droiddeck.launcher.stores.StoreInstallRoot.insufficientSpace(1, -1)) + } + + @Test fun zeroScratchSpaceRejectsBeforeFetchWithoutTouchingInstalledOutputs() { + oldInstall() + val cache = temp.newFolder("full-scratch") + ShadowStatFs.registerStats(cache.path, 1024, 0, 0) + val error = assertThrows(EpicDownloadManager.InstallException::class.java) { install(cache = cache.path) } + assertTrue(error.message.orEmpty().contains("Not enough internal space")) + assertOldInstallIntact() + assertEquals(0, requests.get()) + } + + @Test fun installsSharedChunksAndPreservesLegitimatePartFilename() { + assertNotNull(install()) + assertArrayEquals(bytes.copyOfRange(0, bytes.size / 2), File(root, "game.bin").readBytes()) + assertArrayEquals(bytes.copyOfRange(bytes.size / 2, bytes.size), File(root, "game.bin.part").readBytes()) + assertEquals(1, requests.get()) + assertFalse(File(root, ".chunks").exists()) + assertNotNull(install()) + assertEquals("Verified files must not be downloaded again", 1, requests.get()) + } + + private fun oversizedUpdate() { + val file = manifest.files[0] + val part = file.parts[0] + file.parts.clear() + repeat(100_000) { + file.parts.add(EpicDownloadManager.ChunkPart().apply { + guid = part.guid + size = Int.MAX_VALUE + }) + } + } + + @Test fun insufficientInstallSpacePreservesExistingFilesOnBothCacheLayouts() { + val oldGame = byteArrayOf(9, 8, 7) + val oldPart = byteArrayOf(6, 5, 4) + File(root, "game.bin").writeBytes(oldGame) + File(root, "game.bin.part").writeBytes(oldPart) + oversizedUpdate() + assertTrue(manifest.files[0].fileSize() > root.usableSpace) + + for (cache in listOf("", temp.newFolder("scratch-rejected").path)) { + if (cache.isNotEmpty()) ShadowStatFs.registerStats(cache, 1_000_000_000, 1_000_000_000, 1_000_000_000) + val error = assertThrows(EpicDownloadManager.InstallException::class.java) { install(cache = cache) } + assertTrue(error.message.orEmpty().contains("Not enough free space")) + assertArrayEquals(oldGame, File(root, "game.bin").readBytes()) + assertArrayEquals(oldPart, File(root, "game.bin.part").readBytes()) + } + assertEquals(0, requests.get()) + } + + @Test fun acceptedUpdateReplacesOutdatedFilesWithVerifiedOutputs() { + oldInstall() + assertNotNull(install()) + assertArrayEquals(bytes.copyOfRange(0, bytes.size / 2), File(root, "game.bin").readBytes()) + assertArrayEquals(bytes.copyOfRange(bytes.size / 2, bytes.size), File(root, "game.bin.part").readBytes()) + assertEquals(StoreGameSidecar.STATE_UPDATING, StoreGameSidecar.read(root)?.state) + assertTrue(root.walk().none { it.name.endsWith(".stage") }) + } + + @Test fun pauseKeepsCompletedFilesAndResumeReusesSharedCache() { + val cancel = AtomicBoolean() + val callback = object : EpicDownloadManager.Callback { + override fun onProgress(message: String, pct: Int) {} + override fun onStage(stage: String, done: Long, total: Long, items: Int, itemsTotal: Int) { + if (stage == "stream" && items == 1) cancel.set(true) + } + } + assertNull(install(cancel, callback)) + assertTrue(File(root, "game.bin").exists()) + assertFalse(File(root, "game.bin.part").exists()) + assertEquals(1, requests.get()) + assertNotNull(install()) + assertEquals(1, requests.get()) + } + + @Test fun cancellationDuringChunkDeliveryRemovesIncompleteOutput() { + val cancel = AtomicBoolean() + val callback = object : EpicDownloadManager.Callback { + override fun onProgress(message: String, pct: Int) { + if (message.startsWith("Downloading chunks (")) cancel.set(true) + } + } + assertNull(install(cancel, callback)) + assertFalse(File(root, "game.bin").exists()) + assertNotNull(install()) + assertArrayEquals(bytes.copyOfRange(0, bytes.size / 2), File(root, "game.bin").readBytes()) + } + + @Test fun fileHashFailureIsExplicitAndRetryRepairsIt() { + val expected = manifest.files[0].sha1 + manifest.files[0].sha1 = ByteArray(20) { 1 } + val error = assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertTrue(error.message.orEmpty().contains("SHA-1 mismatch")) + assertFalse(File(root, "game.bin").exists()) + manifest.files[0].sha1 = expected + assertNotNull(install()) + } + + @Test fun corruptCacheIsRefetchedAndScratchCacheIsRemovedOnSuccess() { + val cache = temp.newFolder("scratch") + ShadowStatFs.registerStats(cache.path, 1_000_000_000, 1_000_000_000, 1_000_000_000) + File(cache, manifest.uniqueChunks[0].guidStr()).writeBytes(ByteArray(bytes.size)) + assertNotNull(install(cache = cache.path)) + assertEquals(1, requests.get()) + assertFalse(cache.exists()) + } + + @Test fun networkFailureLeavesNoCompletedFileAndCanBeRetried() { + reject.set(true) + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertFalse(File(root, "game.bin").exists()) + reject.set(false) + assertNotNull(install()) + } + + @Test fun boundedProgressNeverReentersDownloadAfterEnteringInstall() { + val events = mutableListOf<String>() + val callback = object : EpicDownloadManager.Callback { + override fun onProgress(message: String, pct: Int) { + if (message.startsWith("Downloading chunks")) events.add("download") + if (message.startsWith("Writing")) events.add("install") + } + override fun onBytes(done: Long, total: Long, speedBps: Long) { events.add("download") } + override fun onStage(stage: String, done: Long, total: Long, items: Int, itemsTotal: Int) { + if (stage == "install") { + assertEquals(total, done) + assertEquals(itemsTotal, items) + events.add("install") + } + } + } + assertNotNull(install(callback = callback)) + val firstInstall = events.indexOf("install") + assertTrue(firstInstall > 0) + assertFalse(events.drop(firstInstall).contains("download")) + } + + @Test fun sharedChunksDoNotCompleteProgressBeforeTheRemainingFilesAreWritten() { + val samples = mutableListOf<Pair<Long, Long>>() + val callback = object : EpicDownloadManager.Callback { + override fun onProgress(message: String, pct: Int) {} + override fun onBytes(done: Long, total: Long, speedBps: Long) { samples.add(done to total) } + override fun onStage(stage: String, done: Long, total: Long, items: Int, itemsTotal: Int) { + if (stage == "stream" && items < itemsTotal) assertTrue(samples.last().first < samples.last().second) + } + } + assertNotNull(install(callback = callback)) + assertEquals(listOf(0L, bytes.size / 2L, bytes.size.toLong()), samples.map { it.first }) + assertTrue(samples.all { it.second == bytes.size.toLong() }) + } + + @Test fun oversizedSharedChunkStaysPinnedAcrossPrefetchedFilesWithoutChangingStages() { + bytes = ByteArray((EpicDownloadManager.MAX_RETAINED_CHUNK_CACHE_BYTES + 1024).toInt()) { (it % 251).toByte() } + manifest.uniqueChunks.single().apply { + windowSize = bytes.size + fileSize = bytes.size.toLong() + sha1 = sha(bytes) + } + manifest.files = (0..2).map { gameFile("part-$it.bin", it * 1024, 1024) } + val chunk = manifest.uniqueChunks.single() + val estimatedPeak = EpicDownloadManager.estimatePeakAdditionalBytes( + manifest.files, + mapOf(chunk.guidStr() to chunk), + emptyMap(), + true, + ) + assertEquals(EpicDownloadManager.MAX_RETAINED_CHUNK_CACHE_BYTES + 4 * 1024L, estimatedPeak) + val samples = mutableListOf<Long>() + val stages = mutableListOf<String>() + val callback = object : EpicDownloadManager.Callback { + override fun onProgress(message: String, pct: Int) {} + override fun onBytes(done: Long, total: Long, speedBps: Long) { + assertEquals(3072L, total) + assertFalse(stages.contains("install")) + samples.add(done) + } + override fun onStage(stage: String, done: Long, total: Long, items: Int, itemsTotal: Int) { + stages.add(stage) + if (stage == "stream" && items < itemsTotal) assertTrue(samples.last() < total) + } + } + assertNotNull(install(callback = callback)) + assertEquals(1, requests.get()) + assertEquals(listOf(0L, 1024L, 2048L, 3072L), samples) + assertEquals("install", stages.last()) + repeat(3) { assertArrayEquals(bytes.copyOfRange(it * 1024, (it + 1) * 1024), File(root, "part-$it.bin").readBytes()) } + } + + @Test fun hogwartsSizedSequentialPlanFitsAvailableSpaceWithoutFullChunkCache() { + val gib = 1024L * 1024 * 1024 + val file = EpicDownloadManager.FileInfo() + val chunks = LinkedHashMap<String, EpicDownloadManager.ChunkInfo>() + repeat(72) { index -> + val chunk = EpicDownloadManager.ChunkInfo().apply { + guid = intArrayOf(index + 1, 0, 0, 0) + windowSize = gib.toInt() + } + chunks[chunk.guidStr()] = chunk + file.parts.add(EpicDownloadManager.ChunkPart().apply { + guid = chunk.guid + size = chunk.windowSize + }) + } + val peak = EpicDownloadManager.estimatePeakAdditionalBytes(listOf(file), chunks, emptyMap(), true) + assertEquals(73 * gib, peak) + assertTrue(peak < 96 * gib) + } + + @Test fun plannersReclaimEachOldFileOnlyAfterItsOwnVerifiedSwap() { + val chunks = manifest.uniqueChunks.associateBy { it.guidStr() } + val half = bytes.size / 2L + val old = mapOf("game.bin" to half, "game.bin.part" to 100 * half) + assertEquals(bytes.size + half, EpicDownloadManager.estimatePeakAdditionalBytes(manifest.files, chunks, emptyMap(), true, old)) + assertEquals(half, EpicDownloadManager.estimateStagedFilePeak(manifest.files, old)) + assertEquals(2 * half, EpicDownloadManager.estimateStagedFilePeak(manifest.files, emptyMap())) + assertEquals(bytes.size.toLong(), EpicDownloadManager.estimatePeakAdditionalBytes(manifest.files, chunks, emptyMap(), false, old)) + } + + private fun distinctFiles(count: Int) { + val template = manifest.uniqueChunks.single() + manifest.uniqueChunks = (1..count).map { id -> + EpicDownloadManager.ChunkInfo().apply { + guid = intArrayOf(id, 0, 0, 0) + windowSize = template.windowSize + fileSize = template.fileSize + sha1 = template.sha1 + } + } + manifest.files = manifest.uniqueChunks.mapIndexed { index, chunk -> + gameFile("file-$index.bin", 0, bytes.size).apply { parts.single().guid = chunk.guid } + } + } + + @Test fun prefetchOverlapsEightSinglePartFilesButNeverFetchesTheWholeManifest() { + distinctFiles(20) + requestBarrier = CountDownLatch(8) + val cancel = AtomicBoolean() + var sawWindow = false + assertNull(install(cancel, EpicDownloadManager.Callback { message, _ -> + if (message.startsWith("Downloading chunks (")) { + assertEquals(8, requests.get()) + val cacheBytes = File(root, ".chunks").listFiles()!!.sumOf { it.length() } + assertTrue(cacheBytes <= 8L * bytes.size) + sawWindow = true + cancel.set(true) + } + })) + assertTrue("An eight-request barrier cannot pass with a sequential downloader", sawWindow) + assertEquals(8, requests.get()) + assertTrue(root.walk().none { it.name.endsWith(".stage") || it.name.endsWith(".part") }) + } + + @Test fun parallelWindowSpaceThresholdIsCheckedBeforeStartingAnyRequest() { + distinctFiles(20) + val cache = temp.newFolder("parallel-scratch") + val peak = EpicDownloadManager.estimatePeakAdditionalBytes( + manifest.files, manifest.uniqueChunks.associateBy { it.guidStr() }, emptyMap(), false, + ) + assertEquals(8L * bytes.size, peak) + ShadowStatFs.registerStats(cache.path, 1024, peak.toInt() / 4096 - 1, peak.toInt() / 4096 - 1) + assertThrows(EpicDownloadManager.InstallException::class.java) { install(cache = cache.path) } + assertEquals(0, requests.get()) + ShadowStatFs.registerStats(cache.path, 1024, peak.toInt() / 4096, peak.toInt() / 4096) + requestBarrier = CountDownLatch(8) + assertNotNull(install(cache = cache.path)) + assertEquals(20, requests.get()) + manifest.files.forEach { assertArrayEquals(bytes, File(root, it.filename).readBytes()) } + } + + @Test fun cancellationDuringParallelNetworkReadsJoinsWorkersBeforeReturning() { + distinctFiles(20) + requestBarrier = CountDownLatch(8) + deliveryGate = CountDownLatch(1) + val cancel = AtomicBoolean() + val outcome = java.util.concurrent.atomic.AtomicReference<Any?>() + val installer = Thread { + try { outcome.set(install(cancel)) } catch (e: Throwable) { outcome.set(e) } + }.apply { start() } + try { + assertTrue(requestBarrier!!.await(5, TimeUnit.SECONDS)) + cancel.set(true) + installer.join(10_000) + assertFalse("Installer must disconnect and join all download workers", installer.isAlive) + assertNull(outcome.get()) + val cache = File(root, ".chunks") + val snapshot = cache.listFiles()?.associate { it.name to it.length() }.orEmpty() + deliveryGate!!.countDown() + Thread.sleep(150) + assertEquals(snapshot, cache.listFiles()?.associate { it.name to it.length() }.orEmpty()) + assertTrue(snapshot.keys.none { it.endsWith(".part") }) + assertTrue(root.walk().none { it.name.endsWith(".stage") }) + assertEquals(8, requests.get()) + } finally { + cancel.set(true) + deliveryGate!!.countDown() + installer.join(10_000) + } + } + + @Test fun failedParallelFetchJoinsOtherNetworkReadersBeforeReturning() { + distinctFiles(20) + requestBarrier = CountDownLatch(8) + deliveryGate = CountDownLatch(1) + rejectGuid.set(1) + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertEquals(8, requests.get()) + val cache = File(root, ".chunks") + val snapshot = cache.listFiles()?.associate { it.name to it.length() }.orEmpty() + deliveryGate!!.countDown() + Thread.sleep(150) + assertEquals(snapshot, cache.listFiles()?.associate { it.name to it.length() }.orEmpty()) + assertTrue(snapshot.keys.none { it.endsWith(".part") }) + assertTrue(root.walk().none { it.name.endsWith(".stage") }) + } + + @Test fun ownedOrphanStagingIsRecoveredBeforeTheSpaceProbeWithoutTouchingManifestFiles() { + val staging = EpicDownloadManager.recoverOwnedStaging(root, manifest.files) + File(staging, "native-123-0.stage").writeBytes(ByteArray(bytes.size)) + val legitimate = File(root, ".droiddeck-epic-old.stage").apply { writeText("keep") } + oldInstall() + val peak = 2 * bytes.size + ShadowStatFs.registerStats(root.path, 1024, peak / 4096, peak / 4096) + assertNotNull(install()) + assertEquals("keep", legitimate.readText()) + // The orphan was recovered, and a finished install removes the then-empty owned namespace. + assertFalse(staging.exists()) + } + + @Test fun stagingRecoveryRefusesUnownedAndManifestOwnedNamespaces() { + val staging = File(root, EpicDownloadManager.STAGING_DIRECTORY).apply { mkdir() } + val untouched = File(staging, "legitimate.stage").apply { writeText("keep") } + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertEquals("keep", untouched.readText()) + File(staging, "owner").writeText("DroidDeck Epic staging v1\n") + manifest.files.first().filename = "${EpicDownloadManager.STAGING_DIRECTORY}/legitimate.stage" + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertEquals("keep", untouched.readText()) + assertEquals(0, requests.get()) + } + + @Test fun loadedNativeAbiFailureIsExplicitAndNeverFallsBackToJavaDownloads() { + val field = com.droiddeck.launcher.stores.StoresNative::class.java.getDeclaredField("loaded") + field.isAccessible = true + field.set(null, true) + try { + oldInstall() + val error = assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertTrue(error.message.orEmpty().contains("Native Epic install failed")) + assertEquals(0, requests.get()) + assertOldInstallIntact() + } finally { + com.droiddeck.launcher.stores.StoresNative.forget() + } + } + + @Test fun stagingRecoveryRejectsSymlinkNamespaceOwnerAndEntriesWithoutDeletingOutsideData() { + val outside = temp.newFolder("outside-staging") + val externalStage = File(outside, "keep.stage").apply { writeText("keep") } + val externalOwner = File(outside, "owner").apply { writeText("DroidDeck Epic staging v1\n") } + val staging = File(root, EpicDownloadManager.STAGING_DIRECTORY) + try { + java.nio.file.Files.createSymbolicLink(staging.toPath(), outside.toPath()) + } catch (e: java.nio.file.FileSystemException) { + org.junit.Assume.assumeNoException("Host does not permit symbolic links", e) + } + try { + val error = assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertTrue(error.message.orEmpty().contains("Unsafe Epic staging")) + assertEquals("keep", externalStage.readText()) + java.nio.file.Files.delete(staging.toPath()) + staging.mkdir() + java.nio.file.Files.createSymbolicLink(File(staging, "owner").toPath(), externalOwner.toPath()) + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + java.nio.file.Files.delete(File(staging, "owner").toPath()) + File(staging, "owner").writeText("DroidDeck Epic staging v1\n") + val localStage = File(staging, "local.stage").apply { writeText("local") } + java.nio.file.Files.createSymbolicLink(File(staging, "linked.stage").toPath(), externalStage.toPath()) + assertThrows(EpicDownloadManager.InstallException::class.java) { install() } + assertEquals("local", localStage.readText()) + assertEquals("keep", externalStage.readText()) + assertEquals(0, requests.get()) + } finally { + if (java.nio.file.Files.isSymbolicLink(staging.toPath())) java.nio.file.Files.delete(staging.toPath()) + for (name in listOf("owner", "linked.stage")) { + val entry = File(staging, name).toPath() + if (java.nio.file.Files.isSymbolicLink(entry)) java.nio.file.Files.delete(entry) + } + } + } +} 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..14bc3d364 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 @@ -1,17 +1,152 @@ package com.droiddeck.launcher.stores.epic import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue import org.junit.Test 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 { + @Test + fun boundedInstallPeakReservesParallelIncomingChunksBeforeWriting() { + val first = chunk(1) + val second = chunk(2) + val file = file(part(1, 0, 100), part(2, 0, 100)) + val chunks = mapOf(first.guidStr() to first, second.guidStr() to second) + + assertEquals(300L, EpicDownloadManager.estimatePeakAdditionalBytes(listOf(file), chunks, emptyMap(), true)) + assertEquals(200L, EpicDownloadManager.estimatePeakAdditionalBytes(listOf(file), chunks, emptyMap(), false)) + } + + @Test + fun sharedCachedChunkIsReclaimedOnlyAfterItsLastPart() { + val shared = chunk(1) + val files = listOf(file(part(1, 0, 60)), file(part(1, 60, 40))) + + assertEquals( + 100L, + EpicDownloadManager.estimatePeakAdditionalBytes( + files, mapOf(shared.guidStr() to shared), mapOf(shared.guidStr() to 100L), true, + ), + ) + assertEquals( + 200L, + EpicDownloadManager.estimatePeakAdditionalBytes( + files, mapOf(shared.guidStr() to shared), emptyMap(), true, + ), + ) + } + + @Test + fun sharedChunksWithinTheWindowStayPinnedWithinTheCacheCeiling() { + val first = chunk(1).apply { windowSize = 40 * 1024 * 1024 } + val second = chunk(2).apply { windowSize = 40 * 1024 * 1024 } + val files = listOf( + file(part(1, 0, 40 * 1024 * 1024)), + file(part(2, 0, 40 * 1024 * 1024)), + file(part(1, 0, 40 * 1024 * 1024)), + file(part(2, 0, 40 * 1024 * 1024)), + ) + val chunks = mapOf(first.guidStr() to first, second.guidStr() to second) + + val gamePeak = EpicDownloadManager.estimatePeakAdditionalBytes(files, chunks, emptyMap(), true) + val cachePeak = EpicDownloadManager.estimatePeakAdditionalBytes(files, chunks, emptyMap(), false) + + assertEquals(200L * 1024 * 1024, gamePeak) + assertTrue(cachePeak <= EpicDownloadManager.MAX_RETAINED_CHUNK_CACHE_BYTES + 40L * 1024 * 1024) + } + + @Test fun scheduleDeduplicatesWindowAndEvictsOnlyUnpinnedReusableChunks() { + val size = 40L * 1024 * 1024 + val chunks = (1..12).map { chunk(it).apply { windowSize = size.toInt() } } + .associateBy { it.guidStr() } + val files = (1..12).map { file(part(it, 0, 1)) } + (1..12).map { file(part(it, 0, 1)) } + val schedule = EpicDownloadManager.PrefetchSchedule(files, chunks, emptyMap()) + assertEquals(chunks.keys.take(8), schedule.fill()) + assertEquals(emptyList<String>(), schedule.fill()) + assertEquals(8 * size, schedule.bytes()) + assertEquals(emptyList<String>(), schedule.consume()) + assertEquals(listOf(chunks.keys.elementAt(8)), schedule.fill()) + assertEquals(9 * size, schedule.bytes()) + assertEquals(listOf(chunks.keys.first()), schedule.consume()) + var refetchedFirst = false + repeat(22) { + val fetched = schedule.fill() + if (chunks.keys.first() in fetched) refetchedFirst = true + assertTrue(schedule.bytes() <= EpicDownloadManager.MAX_RETAINED_CHUNK_CACHE_BYTES + 8 * size) + val released = schedule.consume() + assertTrue(released.none { it in schedule.window() }) + } + assertTrue(refetchedFirst) + assertEquals(0L, schedule.bytes()) + assertEquals(9 * size, EpicDownloadManager.estimatePeakAdditionalBytes(files, chunks, emptyMap(), false)) + } + + @Test fun cachedAndOldOutputBytesAreCreditedOnlyWhenTheScheduleReleasesThem() { + val chunks = (1..8).map { chunk(it) }.associateBy { it.guidStr() } + val files = (1..8).map { id -> file(part(id, 0, 100)).apply { filename = "file-$id" } } + val existing = chunks.keys.associateWith { 100L } + val old = files.associate { it.filename to 100L } + assertEquals(800L, EpicDownloadManager.estimatePeakAdditionalBytes(files, chunks, emptyMap(), false)) + assertEquals(0L, EpicDownloadManager.estimatePeakAdditionalBytes(files, chunks, existing, false)) + assertEquals(900L, EpicDownloadManager.estimatePeakAdditionalBytes(files, chunks, emptyMap(), true, old)) + assertEquals(100L, EpicDownloadManager.estimatePeakAdditionalBytes(files, chunks, existing, true, old)) + } + + @Test + fun resumeCacheDropsCorruptAndUnneededEntriesButKeepsVerifiedChunks() { + val dir = java.io.File(System.getProperty("user.dir"), "build/epic-cache-test-${System.nanoTime()}") + assertTrue(dir.mkdirs()) + try { + val bytes = ByteArray(100) { (it % 251).toByte() } + val expectedSha = java.security.MessageDigest.getInstance("SHA-1").digest(bytes) + val valid = chunk(1).apply { sha1 = expectedSha } + val corrupt = chunk(2).apply { sha1 = expectedSha } + java.io.File(dir, valid.guidStr()).writeBytes(bytes) + java.io.File(dir, corrupt.guidStr()).writeBytes(ByteArray(100) { 1 }) + java.io.File(dir, "orphan").writeBytes(bytes) + + val retained = EpicDownloadManager.prepareBoundedCache(dir, listOf(valid, corrupt)) + + assertEquals(mapOf(valid.guidStr() to 100L), retained) + assertTrue(java.io.File(dir, valid.guidStr()).exists()) + assertFalse(java.io.File(dir, corrupt.guidStr()).exists()) + assertFalse(java.io.File(dir, "orphan").exists()) + } finally { + EpicDownloadManager.deleteDir(dir) + } + } + + @Test + fun referenceCountsIncludeRepeatedPartsSoCancelledFilesCanBeReplanned() { + val pending = listOf(file(part(1, 0, 20), part(1, 20, 20)), file(part(1, 40, 60))) + + assertEquals(3, EpicDownloadManager.chunkReferences(pending)["00000001000000000000000000000000"]) + } + + private fun chunk(id: Int) = EpicDownloadManager.ChunkInfo().apply { + guid = intArrayOf(id, 0, 0, 0) + windowSize = 100 + fileSize = 100 + } + + private fun part(id: Int, offset: Int, size: Int) = EpicDownloadManager.ChunkPart().apply { + guid = intArrayOf(id, 0, 0, 0) + this.offset = offset + this.size = size + } + + private fun file(vararg parts: EpicDownloadManager.ChunkPart) = EpicDownloadManager.FileInfo().apply { + filename = "test.bin" + this.parts.addAll(parts) + } + @Test fun blobNumbersAreDecimalBytesLeastSignificantFirst() { assertEquals(1L, EpicDownloadManager.blobToNum("001000000000")) 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/stores/gog/GogResumeSpaceTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/gog/GogResumeSpaceTest.kt new file mode 100644 index 000000000..4934b5898 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/stores/gog/GogResumeSpaceTest.kt @@ -0,0 +1,43 @@ +package com.droiddeck.launcher.stores.gog + +import java.io.File +import java.security.MessageDigest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowStatFs +import com.droiddeck.launcher.stores.StoreInstallRoot + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33]) +class GogResumeSpaceTest { + @Test fun remainingWorkRejectsMeasuredZeroButNotUnknownSpace() { + val root = File(RuntimeEnvironment.getApplication().filesDir, "gog-zero").apply { mkdirs() } + ShadowStatFs.registerStats(root.path, 1024, 0, 0) + val files = listOf(GogDownloadManager.DepotFile("missing").apply { totalSize = 4 }) + val needed = GogDownloadManager.remainingInstallBytes(files, root) + assertEquals(4L, needed) + assertTrue(StoreInstallRoot.insufficientSpace(needed, StoreInstallRoot.availableBytes(root))) + assertFalse(StoreInstallRoot.insufficientSpace(needed, StoreInstallRoot.SPACE_UNKNOWN)) + } + + @Test fun remainingSpaceExcludesVerifiedFilesButIncludesCorruptAndPartialFiles() { + val root = File(RuntimeEnvironment.getApplication().filesDir, "gog-resume").apply { mkdirs() } + val data = byteArrayOf(1, 2, 3, 4) + val hash = MessageDigest.getInstance("MD5").digest(data).joinToString("") { "%02x".format(it) } + val files = listOf("good", "corrupt", "partial", "missing").map { + GogDownloadManager.DepotFile(it).apply { totalSize = 4; md5 = hash } + } + File(root, "good").writeBytes(data) + File(root, "corrupt").writeBytes(ByteArray(4)) + File(root, "partial").writeBytes(byteArrayOf(1)) + assertEquals(12L, GogDownloadManager.remainingInstallBytes(files, root)) + for (file in files) File(root, file.relativePath).writeBytes(data) + assertEquals(0L, GogDownloadManager.remainingInstallBytes(files, root)) + } +} 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 986734278..4cb392b0a 100644 --- a/docs/agent-control.md +++ b/docs/agent-control.md @@ -26,7 +26,7 @@ Player controls and observations work over authorized ADB on any build: `state`, `resume`, `wait`, `launch`, `quit`, `focus`, `input`, `ui`, `logs`, `screenshot`, `record`, `displays`, `wake`, and `access`. -**Setup > Session > Debugging tools** is off by default in every build, including debug builds. +**Setup > Diagnostics > Debugging tools** is off by default in every build, including debug builds. It enables `run`, `guest`, `cdp`, all `env` operations, `override`, and `prefs`. The toggle switches directly without a confirmation dialog. The bridge refuses to change it through `ui` or preference writes. The existing preference key, `setting-agent-commands` test tag, JSON command names, and @@ -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://..., --steamvr, --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..c3c7223c2 100644 --- a/docs/development/stores.md +++ b/docs/development/stores.md @@ -24,26 +24,175 @@ scanned (`StoreInstallRoot.roots`): internal, every card, and the Steam library' installs an earlier build put there. `<Store>` is `GOG`, `Epic` or `Amazon`; `<title>` is the title with unsafe characters dropped, at most 60 characters (`StoreInstallRoot.folderName`), chosen once; a rerun (repair, update) lands on the folder whose sidecar carries the game's id, wherever it is. -An Epic install on a card keeps its in-flight chunks in `cacheDir/stores/epic/<id>/`: the -`chunkCacheDir` of both native calls (`EpicNative.run` fetches into it, `EpicNative.assemble` -writes the files from it and drops each chunk after its last use, then the folder), and of the -manager's own loops when the engine is not there; `""` keeps the cache beside the game, as an -internal install has it. The scratch folder is removed on cancel-with-delete and on uninstall. -The cache holds whole ~1 MiB chunk windows, shared with files this device does not install, so it -is often larger than the game (Metalstorm: 9.1 GB of chunks for 4.4 GB of files); the free-space -check, made after the delta pass, counts the missing chunks and the missing files on their own -volumes. A successful run removes the cache. +An Epic install on a card keeps its in-flight chunks in `cacheDir/stores/epic/<id>/`; +`""` keeps the cache beside the game, as an internal install has it. The scratch folder is +removed on cancel-with-delete and on uninstall. + +Preflight resolves an existing game's folder before selecting its target root, so a resume or +repair on a card checks and remembers that volume rather than internal storage. Fresh GOG and +Amazon installs can reject insufficient catalog-size space immediately; existing installs defer +to manifest checks of the files still needing work (GOG MD5, Amazon SHA-256 where supplied). +Free-space display probes the nearest existing ancestor without creating the target folder; if +the probe cannot determine space, it reports **Free space unknown** rather than blocking an install. +`StoreInstallRoot` uses `-1` for unknown and preserves a measured `StatFs` zero as **0 B free**. +Fresh GOG/Amazon preflight and all three Java backend space checks reject known zero when bytes +remain. The read-only probe walks existing ancestors without creating directories; a failed +`StatFs` probe can use a positive `File.usableSpace` fallback, but that API's ambiguous zero is +unknown, not a measurement of a full disk. +Preflight failures remain failed Downloads rows with Clear and Retry. Retry reruns preflight +outside the worker lifecycle and then enqueues the real backend; it never occupies a queue slot +with a placeholder job. Failures are kept in the Downloads UI, notification and logs rather than +duplicated as transient failure toasts. Active queue persistence and ordinary backend pause/retry +are unchanged. + +Epic installs have four writers, all with the same per-file safety rules (below). The hidden +`storesEpicMode` string in the `session` preferences picks one: `adaptive` (default), `lookahead`, +`batch` or `java`. There is no UI for it; set it on a test device to compare the modes on the same +game. Every mode logs an `epic: summary mode=<mode>` line (files, bytes written, elapsed, average +MB/s). When `libdroiddeckstores.so` does not load, every mode runs the Java writer. Once it is +loaded, an engine failure (refused plan, ABI mismatch, network, hash or I/O failure, unsupported +atomic swap) fails the install; it never silently reruns as Java downloads. + +**Adaptive (default)** - `EpicNative.stream`, `epic/stream.rs`. Measured on device: about +83 MB/s on a 71 GB install. The chunks go through the shared fetch core in the order their files +need them, with its full adaptivity (window ramp, per-host caps, in-flight byte budget, retries, +host rotation, stall handoff). A chunk only one file part uses is verified and written straight +into that file's staging file at the part's offset, in whatever order it arrives; it is never +cached, so each byte is written once. A chunk several parts use is written into every part whose +file the download has reached and kept, decompressed, in the cache until its last part is written. +The cache has a byte budget; the plan simulates the install in file order and, when the budget +would be exceeded, drops the cached chunk needed furthest ahead and downloads it again later. +Files are opened only once the download reaches them, so the set of half-written files stays +within the fetch window. All writes happen in the fetch core's process pool, whose completion +frees the in-flight byte budget: a slow card throttles the download instead of growing memory. +Each file's SHA-1 is computed as its parts are written (parts that arrived out of order are read +back once the bytes before them are in). Stage files go into the owned staging namespace below. +The run logs `stream-plan` (direct vs shared chunks, planned refetches, budget) and +`stream-summary mode=adaptive` (peak cache, peak staging, refetches, bytes written from the +download vs from the cache). + +On an update, a staged file that replaces an existing one sits beside its old copy until its swap, +so the engine paces replacements with an overlap budget: files are admitted in order, a replaced +file only while the planned sizes of admitted, not-yet-swapped replaced files fit the budget (one +is always admitted, so the run progresses), and a chunk is fetched only once its file is admitted. +Fresh files are never held back, and admission only delays files, never reorders them, so the +cache plan stays exact. The run logs `overlap_budget=` in `stream-plan` and +`peak_replace_overlap_bytes=` in `stream-summary`. With room to spare the budget is large and +updates run as wide as fresh installs; on a nearly full volume, replaced files are staged one +after another and the fetch pauses at each replaced-file boundary until the previous one is +swapped in. + +Its space plan (`EpicDownloadManager.planAdaptiveSpace`, unit-tested in `EpicAdaptivePlanTest`) +checks each volume for what lands on it, with the lookahead planner's rule (the need must not +exceed the free reading). The game volume: the net growth at its worst point (`settledNetPeak`, +the running Σ(new − old) with each old copy released at its swap), plus the overlap budget, plus +the cache when it is beside the game. The overlap budget is whatever the game volume has left +after the net growth and the cache, never less than the largest replaced file; when even that +does not fit, the plan hands over to the lookahead engine. The cache volume: the budget plus 64 MiB of +chunks in transit. The cache budget is half the cache volume's free space after those needs, at most +1 GiB (256 MiB when free space is unknown; 0 is allowed and means every reuse is a refetch). On a +card install the cache stays on internal scratch unless internal storage cannot hold at least +64 MiB of it; then the cache goes beside the game (still released chunk by chunk) instead of +failing. A plan that does not fit logs the short storage and the shortfall and hands over to the +lookahead engine, which narrows its window to the space there is (or fails its own preflight with +an explicit error). Running out of space in the middle of an adaptive run is not destructive: a +failed write fails the run, unfinished staging files are removed, the old files stay. + +**Lookahead** - `EpicNative.installAdaptive`, `epic/adaptive.rs`; also the adaptive engine's +fallback when its space plan does not fit. The Java delta pass sends a versioned snapshot of +pending files, chunk metadata, verified cache lengths, old output lengths and both measured +free-space probes to Rust. The native planner and execution share one ordered moving-window +policy. Before any chunk request, Rust chooses the largest safe lookahead (128, 64, 32, 16, 8, 4, +2 or 1 part references) and reusable-cache budget (64 MiB, 16 MiB or zero) whose simulated +staged-file and cache peaks fit. Native device IDs also detect an explicitly named cache on the +game volume, so two separate paths cannot incorrectly turn one filesystem's requirement into two +independent checks. + +| Storage situation | Lookahead policy | +| --- | --- | +| Plenty of room | Up to 128 references ahead, up to 32 network requests, 64 MiB reusable data | +| Tight | Smaller lookahead and/or reusable budget; parallel fetching remains where space permits | +| Barely fits | One-reference lookahead, one request, no reusable retention if needed | +| Does not fit even the minimal schedule | Explicit preflight error before requests or replacement | +| Free space unknown | Conservative bounded lookahead (at most eight), not an invented zero | + +The lookahead fetch coordinator reuses `fetch_core`'s host scheduling, retries, adaptive network +window and decode pool. Each chunk is cached, then its parts are copied into staged files by a +writer that runs concurrently, waiting only for its next required chunk, through the native +assembler's streaming copy primitive. GUIDs in the lookahead are reserved once before submission, +at their full decoded size. Chunks are released at last use on **both** cache layouts; distant +reusable data is oldest-first bounded and may be refetched. At most the selected reusable budget +plus distinct lookahead decoded sizes is resident/reserved. New request admission re-probes both +volumes and can shrink unsent lookahead or retention if headroom disappears; already in-flight +reservations remain protected. If even existing reservations cannot fit, the run fails and joins +its workers rather than waiting indefinitely or deleting old game files. An install to SD retains +its internal scratch cache, so assembled output is the only game data written to the card. + +**Java** - without the native library, or with `storesEpicMode=java`. Eight Java workers prefetch +the next eight chunk-part references, across file boundaries, while one writer assembles and +verifies files in manifest order. Shared GUIDs within that window are fetched only once. Chunks +are deleted after their last use; other reusable chunks are retained up to 64 MiB. At most eight +additional decoded chunks (complete or in flight) coexist with that retained cache. The size +bound is 64 MiB plus the decoded sizes of the window's distinct GUIDs, not a fixed eight-MiB +allowance: manifests can supply larger chunk windows. Evicted distant shared chunks may be +downloaded again. No worker downloads outside the moving window; cancellation and failure +disconnect and join all workers before returning or removing the cache. Its summary line is +`mode=java-bounded`. + +**Batch** - the earlier native design, kept for A/B benchmarks: every chunk fetched into the +cache in parallel, then the files assembled from it (`EpicNative.run` + `EpicNative.assemble`). +It needs the cache and the files at once (about twice the game) and checks exactly that before +starting; it writes files in place, so an existing install is marked `updating` before assembly. +`epic/assemble.rs::build_assemble_plan` enables `scratch_cache` only for a nonempty cache path: +`run_assemble` then removes chunks at their last use; the default beside-game cache is removed at +the end (`default_cache_assembles_like_the_kotlin_loop_and_removes_the_cache_at_the_end` and the +scratch-cache assembly tests cover that distinction). + +The Epic download bar, measured rate and ETA (adaptive, lookahead and Java) count pending file bytes written, not unique +compressed chunks fetched. Shared chunks therefore cannot complete the bar before later files +are written, and an evicted chunk's refetch is reflected in the time spent doing that work. +Streaming stays in the Download stage until all files are written and verified, then enters +Install for registration; it does not bounce between stages for each file. + +For the lookahead and Java writers, the space check follows the manifest delta pass and uses the +same pure moving-window policy as execution, reserving all incoming decoded chunk sizes before writing parts and simulating +last-use releases and unpinned oldest-first evictions. Existing verified cache bytes count against +current free space and are credited only at their scheduled release. This avoids adding the entire +cache to the entire game without undercounting concurrent downloads. Each file is written to a +unique file in an owned staging namespace on its output volume (never the literal `<filename>.part`, which can +be a legitimate manifest file), size/hash verified, synced and atomically renamed over its output. +The old file remains until that swap succeeds. The planner credits its bytes only after that +file's complete staging write, not by subtracting all old outputs up front. Both volume checks +must accept this safe staging peak; insufficient space is rejected explicitly, with no destructive +delete-first fallback. An unsupported/failed atomic rename fails the install and retains the old +destination. Completed files and +verified cached chunks are accounted for on resume. On a card, game and cache requirements +are checked separately. File SHA-1 hashes are checked where supplied by the manifest; failed +or cancelled staging files are removed without deleting old outputs. A successful run removes the +cache. A resume within a large incomplete file restarts that file, not its individual parts. +All three staged writers (adaptive, lookahead, Java) use `<installDir>/.droiddeck-epic-staging/`, whose `owner` marker identifies this +namespace. A successful install removes it when it holds nothing but the marker (same no-follow +and canonical-path checks); the next run recreates it. Before the delta pass and free-space probes, the manager removes orphan `.stage` +entries only from this owned namespace. A manifest naming that directory or any descendant +fails without deletion; an unowned directory or unexpected entry also fails conservatively. +The namespace must be a real directory at the expected canonical child of the install root. +Ownership markers and staging entries must be no-follow regular files; symlink directories, +markers and entries are rejected before recovery deletes anything. +Ordinary game files with `.stage` or `.part` suffixes elsewhere are never recovery targets. +Abrupt process termination is therefore recoverable on the next run without inferring ownership +from a loose filename prefix. Cancellation/failure waits for all native/Java fetch writers before +returning; completed verified cache chunks remain usable on resume. ## The sidecar ```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", @@ -59,7 +208,18 @@ store root whose sidecar says `installing`, or that has no sidecar at all, is an install: `AddedGames.scan` leaves it out (it is neither a game nor a Custom folder), and the Stores card and page offer **Resume install**, which reuses that folder - and an Epic `.chunks` beside it - instead of starting a fresh one (`StoreInstallRoot.existingFolder`). A finished install being -repaired keeps its finished sidecar. The launcher and the finished sidecar are written before the +repaired keeps its finished sidecar until Epic reaches the first verified file swap. Immediately +before that swap Epic atomically changes the record to `"state": "updating"`, preserves launch +choices and identifiers, and clears `installVersion`. Network/hash failures or cancellation before +that point leave the old installed record and files intact. A later failure can leave a mixture of +verified new files and untouched old files: replacement is atomic **per file**, not a transactional +whole-game update or rollback. The updating folder is excluded from installed scans and offered +for resume. Cancellation with deletion clears caches but retains that folder and its verified +swaps; retry preserves the updating record's metadata. If marking or swapping fails, no destructive +fallback is attempted (the conservative updating state can remain even when a swap fails). +Completion writes the installed record with the new version only after all selected files succeed. +An already registered live Steam shortcut is not revoked at each file swap; do not launch an +in-progress update. The launcher and the finished sidecar are written before the art and the Steam registration; a failure writing them fails the download with its message. `StoreGameSidecar.parse` refuses anything whose `exe` or `launcher` would point outside the folder. diff --git a/docs/quest.md b/docs/quest.md index af5871d0e..307d5f84b 100644 --- a/docs/quest.md +++ b/docs/quest.md @@ -1,7 +1,23 @@ -# Experimental Quest 3/3S support +# Experimental Quest 2/3/3S support Quest 3/3S runs DroidDeck as a 2D panel. This does not add immersive VR or -OpenXR support. Quest 2 is outside the bundled Adreno 7xx driver path. +OpenXR support. Quest 2 has a separate experimental Adreno 6xx driver path. + +Quest 2 selects `turnip26.3.0-quest2`, a pinned Banners-Turnip A6xx/A7xx build +with the same GPUOBJ adapter. It does not select the Quest 3 A7xx binary. +The first Quest 2 launch applies manual driver selection and the SurfaceControl +workaround, even if `questDefaultsV1` exists from an earlier install. Later user +choices are retained. The separate `turnip26.3.0-a6xx` bundle provides a plain +Adreno 6xx offline fallback for other devices; their driver Auto behavior remains. +Neither family-wide hardware coverage nor Quest 2 operation has been verified. + +The A6xx input URL and SHA-256 are pinned in `tools/quest/build-driver.py`. +Both A6xx bundles are prepared by the existing local/CI wrapper build step and +covered by runtime-input attestation. Quest 3/3S retains its original driver. +Early installation and compositor breadcrumbs are written to `driver-setup.txt` +in private files and, when available, the app's external files directory. They +do not require a Steam session. The existing Quest Java crash file now also +applies to Quest 2; native crashes still require Android logcat. Install the standard DroidDeck APK on Quest 3/3S. Device detection on the first launch selects manual drivers, the bundled 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 ed39908e3..a28eaa27b 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,16 +26,12 @@ 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)); @@ -47,7 +41,6 @@ int main(void) { q.mono = 0; cardboard_push_constants(&q, pose, 3840, 1080, 1920, 1080, pc); CHECK(near(pc[3], 0.032f) && near(pc[19], 960.0f / 1080.0f) && near(pc[20], 0.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 19e345711..9553d2561 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": @@ -300,7 +298,7 @@ def start_session(adb: str, serial: str, request: dict) -> dict: def require_commands(adb: str, serial: str) -> dict: access = checked(provider_call(adb, serial, "access"), "ACCESS_FAILED") if access.get("commands") is not True: - raise ControlError("Debugging tools are off: turn on Setup > Session > Debugging tools on the device", + raise ControlError("Debugging tools are off: turn on Setup > Diagnostics > Debugging tools on the device", "AGENT_COMMANDS_DISABLED") return access @@ -343,8 +341,6 @@ def execute(args: argparse.Namespace, adb: str, serial: str) -> dict: if args.steamvr: request["steamVr"] = True 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"}: @@ -528,7 +524,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") @@ -570,7 +565,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") @@ -753,7 +747,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/PATCHES.md b/tools/gamescope/PATCHES.md index acb345515..3a36c3ae0 100644 --- a/tools/gamescope/PATCHES.md +++ b/tools/gamescope/PATCHES.md @@ -44,6 +44,13 @@ library list, before anything is published. read the window's attributes first, it kept the window as override-redirect and left it out of `GAMESCOPE_FOCUSABLE_WINDOWS`, so the client kept its loading screen over the running game (Skyrim SE, A Plague Tale: Innocence, on the games' own Xwayland). MapNotify now refreshes the flag. +- `0115-limiter-paces-per-window.patch` - this app: on a fixed-refresh output the frame limiter + (Steam's Frame Limit) let a capped window's frames out only on every Nth refresh of a global grid, + so a frame finished just after its slot waited a whole extra slot. At 30 on 60 Hz one picture was + held for three or four refreshes - "small stops and ladders" with frame generation - where a game's + own limiter slips such a frame by one refresh. The fixed-refresh branch now paces each window by + time, as the VRR branch already did: its next frame goes out on the first refresh a capped interval + after its last one, with half a refresh of tolerance so an on-time window keeps every Nth refresh. Sixteen more of Armada's patches are DRM/lease/HDR-on-KMS work for a native display, which this app's Wayland-hosted gamescope never reaches, or need a newer gamescope than the runtime has. 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/gamescope/patches/0115-limiter-paces-per-window.patch b/tools/gamescope/patches/0115-limiter-paces-per-window.patch new file mode 100644 index 000000000..7ead84e35 --- /dev/null +++ b/tools/gamescope/patches/0115-limiter-paces-per-window.patch @@ -0,0 +1,27 @@ +diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp +index 5b0e300..b9b07ca 100644 +--- a/src/steamcompmgr.cpp ++++ b/src/steamcompmgr.cpp +@@ -6875,10 +6875,19 @@ static bool steamcompmgr_should_vblank_window( bool bShouldLimitFPS, uint64_t vb + { + if ( g_nSteamCompMgrTargetFPS && bShouldLimitFPS && nRefreshHz > nTargetFPS ) + { +- int nVblankDivisor = nRefreshHz / nTargetFPS; ++ if ( w && now ) ++ { ++ uint64_t schedule = w->last_commit_first_latch_time + g_SteamCompMgrLimitedAppRefreshCycle; ++ if ( now + g_SteamCompMgrAppRefreshCycle / 2 < schedule ) ++ bSendCallback = false; ++ } ++ else ++ { ++ int nVblankDivisor = nRefreshHz / nTargetFPS; + +- if ( vblank_idx % nVblankDivisor != 0 ) +- bSendCallback = false; ++ if ( vblank_idx % nVblankDivisor != 0 ) ++ bSendCallback = false; ++ } + } + } + diff --git a/tools/gamescope/release.env b/tools/gamescope/release.env index 9f895d08d..631c6b47d 100644 --- a/tools/gamescope/release.env +++ b/tools/gamescope/release.env @@ -1,3 +1,3 @@ GAMESCOPE_REPO=Droid-Deck/DroidDeck-Components -GAMESCOPE_TAG=gamescope-3.16.29-p9 -GAMESCOPE_SHA256=7123a37bdbcd0d76d4bd1d7490e5d2017221e036395fdd48c1e92458594ca769 +GAMESCOPE_TAG=gamescope-3.16.29-p10 +GAMESCOPE_SHA256=e8e2433ae618458135ec271a30af7e80b282ff41f9931286e9bbad614838eca0 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 bfa637a7f..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,15 +579,17 @@ class Installer: def put(self, source, rel): target = self.out / rel + if target.exists() and Path(source).samefile(target): + return False target.parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(source, target) + return True def install_files(self, unpacked): assemblies = self.assemblies() 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: @@ -664,16 +644,12 @@ class Installer: folder = self.directory(row["DestFolder"]) if row.get("DestFolder") else self.files[row["File_"]].rsplit("/", 1)[0] name = long_name(row.get("DestName", "")) or self.files[row["File_"]].rsplit("/", 1)[1] source = self.out / self.files[row["File_"]] - if folder and source.is_file() and "/" not in name: - self.put(source, folder + "/" + name) + if folder and source.is_file() and "/" not in name and self.put(source, folder + "/" + name): self.counts["files"] += 1 if missing: 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\\"):] @@ -782,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: @@ -796,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.""" @@ -974,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 ba25dcc2a..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,57 @@ case $mode in done ) & 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_ZRAM:-0}" = 1 ]; then + printf 0 > /tmp/.bl-zram + step "zRAM compression: on" + ( + bl_zram_scan() { + _bl_game=0 _bl_pids=() + for _d in /proc/[0-9]*; do + IFS= read -r -d '' _c 2>/dev/null < "$_d/cmdline" || continue + case ${_c##*/} in + reaper) _bl_game=1 ;; + steam|steamwebhelper) _bl_pids+=("${_d#/proc/}") ;; + esac + done + } + bl_zram_usage() { + local _p _k _v _u _rss=0 _swap=0 + for _p in "${_bl_pids[@]}"; do + while read -r _k _v _u; do + case $_k in VmRSS:) _rss=$((_rss + _v)) ;; VmSwap:) _swap=$((_swap + _v)) ;; esac + done 2>/dev/null < "/proc/$_p/status" + done + echo "${#_bl_pids[@]} processes, resident $((_rss / 1024)) MB, in zram $((_swap / 1024)) MB" + } + bl_zram_send() { + step "zRAM: $2 ($(bl_zram_usage))" + printf '%s' "$1" > /tmp/.bl-zram + [ "${#_bl_pids[@]}" = 0 ] || kill -URG "${_bl_pids[@]}" 2>/dev/null + nap 5 + bl_zram_scan + step "zRAM: done ($(bl_zram_usage))" + } + _bl_since=-1 _bl_packed=-1 + while kill -0 $$ 2>/dev/null; do + bl_zram_scan + if [ "$_bl_game" = 1 ]; then + [ "$_bl_since" -ge 0 ] || _bl_since=$SECONDS + if [ $((SECONDS - _bl_since)) -ge 20 ] && [ "${#_bl_pids[@]}" -gt 0 ] && + { [ "$_bl_packed" -lt 0 ] || [ $((SECONDS - _bl_packed)) -ge 300 ]; }; then + bl_zram_send 1 "compressing the client" + _bl_packed=$SECONDS + fi + elif [ "$_bl_since" -ge 0 ]; then + [ "$_bl_packed" -lt 0 ] || bl_zram_send 0 "game closed, reading the client back" + _bl_since=-1 _bl_packed=-1 + fi + nap 5 + done + ) & + else + rm -f /tmp/.bl-zram + fi 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 & @@ -504,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 @@ -540,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" @@ -579,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 @@ -609,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 @@ -629,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#*=} @@ -660,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 @@ -708,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" @@ -721,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=() @@ -749,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 ;; @@ -763,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' @@ -798,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=$! @@ -826,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) @@ -875,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" \ @@ -926,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 @@ -945,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 @@ -966,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 @@ -1045,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 @@ -1086,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 -- @@ -1099,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 @@ -1111,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 8e2b8ae1f..080fcfeaf 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 @@ -109,13 +93,10 @@ JSON } 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 @@ -124,8 +105,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 new file mode 100644 index 000000000..565e0f7dd --- /dev/null +++ b/tools/linuxfs/preload/zram.c @@ -0,0 +1,171 @@ +#define _GNU_SOURCE +#include <dlfcn.h> +#include <errno.h> +#include <fcntl.h> +#include <pthread.h> +#include <signal.h> +#include <stdint.h> +#include <string.h> +#include <sys/mman.h> +#include <sys/syscall.h> +#include <sys/uio.h> +#include <unistd.h> + + +#ifndef MADV_PAGEOUT +#define MADV_PAGEOUT 21 +#endif +#ifndef ZR_CTL +#define ZR_CTL "/tmp/.bl-zram" +#endif +#define ZR_SWAPPED (1ULL << 62) +#define ZR_BATCH 512 + +static long (*zr_sys)(long, ...); +static unsigned long zr_page; +static int zr_busy; +static int zr_pending; +static char zr_maps[8192]; +static uint64_t zr_entries[ZR_BATCH]; +static struct iovec zr_iov[ZR_BATCH]; +static char zr_sink[ZR_BATCH]; + +static int zr_open(const char *path) { + return (int)zr_sys(SYS_openat, AT_FDCWD, path, O_RDONLY | O_CLOEXEC, 0); +} + +static int zr_mode(void) { + char c = 0; + int fd = zr_open(ZR_CTL); + if (fd < 0) return -1; + long n = zr_sys(SYS_read, fd, &c, 1); + zr_sys(SYS_close, fd); + return n == 1 && (c == '0' || c == '1') ? c - '0' : -1; +} + +static unsigned long zr_hex(const char **p) { + unsigned long v = 0; + for (;; (*p)++) { + char c = **p; + if (c >= '0' && c <= '9') v = v * 16 + (unsigned long)(c - '0'); + else if (c >= 'a' && c <= 'f') v = v * 16 + (unsigned long)(c - 'a' + 10); + else return v; + } +} + +static int zr_eligible(const char *line, int compress, unsigned long *start, unsigned long *end) { + const char *p = line; + *start = zr_hex(&p); + if (*p++ != '-') return 0; + *end = zr_hex(&p); + if (*p++ != ' ' || p[0] != 'r' || p[3] != 'p') return 0; + if (!compress) return 1; + if (p[1] != 'w') return 0; + for (int field = 0; field < 4; field++) { + while (*p && *p != ' ') p++; + while (*p == ' ') p++; + } + return *p == 0 || strncmp(p, "[heap]", 6) == 0 || strncmp(p, "[anon:", 6) == 0; +} + +static void zr_read_back(int pagemap, unsigned long start, unsigned long end) { + pid_t self = (pid_t)zr_sys(SYS_getpid); + struct iovec local = {zr_sink, 0}; + for (unsigned long addr = start; addr < end;) { + unsigned long count = (end - addr) / zr_page; + if (count > ZR_BATCH) count = ZR_BATCH; + if (zr_sys(SYS_lseek, pagemap, (long)(addr / zr_page * sizeof(uint64_t)), SEEK_SET) < 0) return; + long got = zr_sys(SYS_read, pagemap, zr_entries, count * sizeof(uint64_t)); + if (got < (long)sizeof(uint64_t)) return; + unsigned long n = 0; + for (unsigned long i = 0; i < (unsigned long)got / sizeof(uint64_t); i++) { + if (!(zr_entries[i] & ZR_SWAPPED)) continue; + zr_iov[n].iov_base = (void *)(addr + i * zr_page); + zr_iov[n++].iov_len = 1; + } + for (unsigned long done = 0; done < n;) { + local.iov_len = n - done; + long r = zr_sys(SYS_process_vm_readv, self, &local, 1UL, zr_iov + done, n - done, 0UL); + done += r > 0 ? (unsigned long)r : 1; + } + addr += (unsigned long)got / sizeof(uint64_t) * zr_page; + } +} + +static void zr_walk(int compress) { + int maps = zr_open("/proc/self/maps"); + if (maps < 0) return; + int pagemap = compress ? -1 : zr_open("/proc/self/pagemap"); + if (!compress && pagemap < 0) { + zr_sys(SYS_close, maps); + return; + } + size_t have = 0; + for (;;) { + long n = zr_sys(SYS_read, maps, zr_maps + have, sizeof(zr_maps) - 1 - have); + if (n < 0 && errno == EINTR) continue; + if (n <= 0) break; + have += (size_t)n; + zr_maps[have] = 0; + char *line = zr_maps; + char *nl; + while ((nl = memchr(line, '\n', have - (size_t)(line - zr_maps))) != NULL) { + *nl = 0; + unsigned long s, e; + if (zr_eligible(line, compress, &s, &e) && e > s) { + if (compress) zr_sys(SYS_madvise, s, e - s, MADV_PAGEOUT); + else zr_read_back(pagemap, s, e); + } + line = nl + 1; + } + have -= (size_t)(line - zr_maps); + memmove(zr_maps, line, have); + if (have == sizeof(zr_maps) - 1) have = 0; + } + if (pagemap >= 0) zr_sys(SYS_close, pagemap); + zr_sys(SYS_close, maps); +} + +static void zr_handler(int sig, siginfo_t *info, void *context) { + (void)sig; + (void)context; + if (info == NULL || info->si_code != SI_USER) return; + int saved = errno; + __atomic_store_n(&zr_pending, 1, __ATOMIC_SEQ_CST); + while (__atomic_load_n(&zr_pending, __ATOMIC_SEQ_CST) + && !__atomic_exchange_n(&zr_busy, 1, __ATOMIC_SEQ_CST)) { + __atomic_store_n(&zr_pending, 0, __ATOMIC_SEQ_CST); + int mode = zr_mode(); + if (mode >= 0) zr_walk(mode); + __atomic_store_n(&zr_busy, 0, __ATOMIC_SEQ_CST); + } + errno = saved; +} + +static void zr_child(void) { + __atomic_store_n(&zr_busy, 0, __ATOMIC_SEQ_CST); + __atomic_store_n(&zr_pending, 0, __ATOMIC_SEQ_CST); +} + +__attribute__((constructor)) static void zr_init(void) { + const char *name = program_invocation_short_name; + if (strcmp(name, "steam") != 0 && strcmp(name, "steamwebhelper") != 0) return; + void *libc = dlopen("libc.so.6", RTLD_LAZY | RTLD_NOLOAD); + if (libc == NULL) return; + zr_sys = (long (*)(long, ...))dlsym(libc, "syscall"); + if (zr_sys == NULL) return; + int fd = zr_open(ZR_CTL); + if (fd < 0) return; + zr_sys(SYS_close, fd); + long page = sysconf(_SC_PAGESIZE); + zr_page = page > 0 ? (unsigned long)page : 4096UL; + struct sigaction old; + if (sigaction(SIGURG, NULL, &old) != 0) return; + if ((old.sa_flags & SA_SIGINFO) || old.sa_handler != SIG_DFL) return; + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = zr_handler; + sa.sa_flags = SA_SIGINFO | SA_RESTART; + sigemptyset(&sa.sa_mask); + if (sigaction(SIGURG, &sa, NULL) == 0) pthread_atfork(NULL, NULL, zr_child); +} 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/PATCHES.md b/tools/proot/PATCHES.md index d893f23c6..138181ae8 100644 --- a/tools/proot/PATCHES.md +++ b/tools/proot/PATCHES.md @@ -78,9 +78,17 @@ Added by DroidDeck: exit.c act on (`TCSETSF`, the four termios2 requests, `FICLONE`) and allows the rest at once, and the ioctl block is emitted first (after WinNative 53836ca9, which traces only the termios2 ones). The `faccessat2` exit stop (termux 5ba8b95, for glibc's ENOSYS fallback on kernels before 5.8) and - the `statx` one (emulation on kernels before 4.11) are dropped when the running kernel is newer. + the `statx` one (emulation on kernels before 4.11) are dropped on kernels 5.8 and newer; patch + `0015` keeps it on 4.11–5.7 to return the mount ID added in 5.8. Host build: `ioctl` 31.6 -> 0.5 us; `stat`/`statx`/`faccessat2` about 44 -> 28 us (one stop). -- `0015-robust-list-from-seccomp.patch` - Android's app policy refuses `set_robust_list` and +- `0015-statx-mnt-id-fdinfo.patch` - kernels before 5.8 can complete `statx` without the mount ID + that libglnx requires while chasing an open descriptor. PRoot keeps the exit stop on those + kernels and fills only a missing ordinary `STATX_MNT_ID` for `AT_EMPTY_PATH` after pinning the + tracee descriptor, matching its device and inode to the successful statx result, and reading + `mnt_id` from the pin's fdinfo. A later close/reuse of the tracee fd cannot redirect that read; + a concurrent reuse for the same inode through a different bind mount is indistinguishable at + the exit stop. It does not synthesize `STATX_MNT_ID_UNIQUE`. +- `0016-robust-list-from-seccomp.patch` - Android's app policy refuses `set_robust_list` and `get_robust_list`. proot still answers the first with `ENOSYS` but keeps the head each thread offered (cleared on `execve`, none in a new task), and answers the second for the calling thread with it. SteamVR's `vrclient.so` asks for the head through libc's `syscall()`, which 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/build.sh b/tools/proot/build.sh index a899370fd..718190552 100755 --- a/tools/proot/build.sh +++ b/tools/proot/build.sh @@ -30,6 +30,7 @@ fetch() { } fetch proot.tar.gz "https://github.com/termux/proot/archive/${PROOT_COMMIT}.tar.gz" "$PROOT_SHA256" fetch talloc.tar.gz "https://www.samba.org/ftp/talloc/talloc-${TALLOC_VERSION}.tar.gz" "$TALLOC_SHA256" +cp "$HERE/statx_mnt_id.h" "$WORK/proot/src/tracee/statx_mnt_id.h" for patch in "$HERE"/patches/*.patch; do patch -d "$WORK/proot" -p1 --forward --quiet < "$patch" done 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/proot/patches/0015-statx-mnt-id-fdinfo.patch b/tools/proot/patches/0015-statx-mnt-id-fdinfo.patch new file mode 100644 index 000000000..a0821997a --- /dev/null +++ b/tools/proot/patches/0015-statx-mnt-id-fdinfo.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: DroidDeck <droiddeck@users.noreply.github.com> +Date: Tue, 13 Oct 2026 00:00:00 +0000 +Subject: [PATCH] statx: supply mount IDs on kernels before 5.8 + +On kernels before 5.8, statx can succeed without STATX_MNT_ID. Recover it +from the already-open descriptor after verifying its identity. Keep the +compatibility exit stop only on kernels that need the missing field. +--- +diff --git a/src/syscall/seccomp.c b/src/syscall/seccomp.c +index ee7b8acd68660f38fd52f56e951ffb94e9061d68..ff3a79b60e9092a296bb444c919bb3861338727f 100644 +--- a/src/syscall/seccomp.c ++++ b/src/syscall/seccomp.c +@@ -764 +764 @@ static void drop_unneeded_exit_stops(void) +- if (major > 4 || (major == 4 && minor >= 11)) ++ if (major > 5 || (major == 5 && minor >= 8)) +diff --git a/src/tracee/statx.c b/src/tracee/statx.c +index ca972f892e84461cb694dd13a37bfbc67527b320..824771ccf5934e1f870fc36a29fb22fdcad0a856 100644 +--- a/src/tracee/statx.c ++++ b/src/tracee/statx.c +@@ -4,0 +5 @@ ++#include "tracee/statx_mnt_id.h" +@@ -125,0 +127,5 @@ int handle_statx_syscall(Tracee *tracee, bool from_sigsys) { ++ if (droiddeck_proot_statx_fill_mnt_id(tracee->pid, (int) dirfd, ++ (int) flags, guest_path, (unsigned int) mask, ++ &state.statx_buf)) ++ state.updated_stats = true; ++ diff --git a/tools/proot/patches/0015-robust-list-from-seccomp.patch b/tools/proot/patches/0016-robust-list-from-seccomp.patch similarity index 100% rename from tools/proot/patches/0015-robust-list-from-seccomp.patch rename to tools/proot/patches/0016-robust-list-from-seccomp.patch diff --git a/tools/proot/statx_mnt_id.h b/tools/proot/statx_mnt_id.h new file mode 100644 index 000000000..deb1789b8 --- /dev/null +++ b/tools/proot/statx_mnt_id.h @@ -0,0 +1,95 @@ +#ifndef DROIDDECK_PROOT_STATX_MNT_ID_H +#define DROIDDECK_PROOT_STATX_MNT_ID_H + +#include <errno.h> +#include <fcntl.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/sysmacros.h> +#include <sys/types.h> +#include <unistd.h> + +static inline bool droiddeck_proot_read_fdinfo(int fd, uint64_t *mnt_id, uint64_t *ino) { + if (fd < 0 || !mnt_id || !ino) return false; + + char fdinfo_path[64]; + int n = snprintf(fdinfo_path, sizeof fdinfo_path, "/proc/self/fdinfo/%d", fd); + if (n < 0 || (size_t)n >= sizeof fdinfo_path) return false; + + FILE *fdinfo = fopen(fdinfo_path, "r"); + if (!fdinfo) return false; + + bool have_mnt_id = false, have_ino = false; + uint64_t parsed_mnt_id = 0, parsed_ino = 0; + char line[256]; + while (fgets(line, sizeof line, fdinfo)) { + const char *value = NULL; + uint64_t *field = NULL; + if (strncmp(line, "mnt_id:", 7) == 0) { + value = line + 7; + field = &parsed_mnt_id; + have_mnt_id = true; + } else if (strncmp(line, "ino:", 4) == 0) { + value = line + 4; + field = &parsed_ino; + have_ino = true; + } + if (!value) continue; + + errno = 0; + char *end = NULL; + unsigned long long parsed = strtoull(value, &end, 10); + if (errno || end == value || (*end && *end != '\n' && *end != '\r')) { + if (field == &parsed_mnt_id) have_mnt_id = false; + else have_ino = false; + continue; + } + *field = (uint64_t)parsed; + } + bool read_error = ferror(fdinfo); + fclose(fdinfo); + + if (read_error || !have_mnt_id || !have_ino || parsed_mnt_id == 0) return false; + *mnt_id = parsed_mnt_id; + *ino = parsed_ino; + return true; +} + +static inline bool droiddeck_proot_statx_fill_mnt_id(pid_t pid, int fd, + int flags, const char *path, + unsigned int mask, + struct statx *buf) { + if (fd < 0 || !(flags & AT_EMPTY_PATH) || !path || path[0] != '\0' + || !(mask & STATX_MNT_ID) || (buf->stx_mask & STATX_MNT_ID) + || !(buf->stx_mask & STATX_INO)) + return false; + + char tracee_fd_path[64]; + int n = snprintf(tracee_fd_path, sizeof tracee_fd_path, "/proc/%ld/fd/%d", (long)pid, fd); + if (n < 0 || (size_t)n >= sizeof tracee_fd_path) return false; + + /* Another thread sharing the tracee's fd table can close/reuse fd after statx returns. Pin the + * current referent, then require both device and inode to match statx before trusting its mount. + * Read fdinfo from our pin so a later tracee fd reuse cannot change which mount ID is observed. + * A same-inode bind-mount alias has the same device/inode pair; at this exit stop we cannot + * distinguish that rare concurrent replacement from the mount used by the completed syscall. */ + int pinned = open(tracee_fd_path, O_PATH | O_CLOEXEC); + if (pinned < 0) return false; + struct stat st; + bool same = fstat(pinned, &st) == 0 && st.st_ino == buf->stx_ino && + major(st.st_dev) == buf->stx_dev_major && minor(st.st_dev) == buf->stx_dev_minor; + uint64_t mnt_id = 0, ino = 0; + bool have_fdinfo = same && droiddeck_proot_read_fdinfo(pinned, &mnt_id, &ino); + close(pinned); + if (!have_fdinfo || ino != st.st_ino) return false; + + buf->stx_mnt_id = mnt_id; + buf->stx_mask |= STATX_MNT_ID; + return true; +} + +#endif 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 b421f9974..e72b2991e 100644 --- a/tools/quest/build-driver.py +++ b/tools/quest/build-driver.py @@ -7,9 +7,81 @@ import subprocess import sys import tarfile import tempfile +import hashlib +import urllib.request +import zipfile ROOT = Path(__file__).resolve().parents[2] OUTPUT = ROOT / 'app/build/prepared-assets/graphics_driver/adrenotools-turnip25.1.0-quest1.tzst' +A6XX_TAG = 'v26.3.0-20261012-r2' +A6XX_SHA256 = 'bb1b2c51e6446cf428d70255d037be169ac68108c00af33aa1ba7ece711f8336' +A6XX_URL = f'https://github.com/The412Banner/Banners-Turnip/releases/download/{A6XX_TAG}/Turnip-{A6XX_TAG}.zip' + + +def pack(entries, output): + stream = io.BytesIO() + with tarfile.open(fileobj=stream, mode='w', format=tarfile.USTAR_FORMAT) as archive: + for name, content in entries: + entry = tarfile.TarInfo(name) + entry.size = len(content) + entry.mode = 0o644 + archive.addfile(entry, io.BytesIO(content)) + compressed = subprocess.run(['zstd', '-q', '-19', '-c'], input=stream.getvalue(), + capture_output=True, check=True).stdout + output.parent.mkdir(parents=True, exist_ok=True) + output.write_bytes(compressed) + print(output) + + +def build_a6xx(ndk, output_dir=OUTPUT.parent, archive_path=None): + """Pin a real A6xx driver, with a separate Quest adapter around the same bytes.""" + cached = archive_path or ROOT / 'app/build/quest-inputs' / f'Turnip-{A6XX_TAG}.zip' + if not cached.exists(): + cached.parent.mkdir(parents=True, exist_ok=True) + data = urllib.request.urlopen(A6XX_URL, timeout=120).read() + if hashlib.sha256(data).hexdigest() != A6XX_SHA256: + raise ValueError('A6xx archive checksum mismatch') + cached.write_bytes(data) + data = cached.read_bytes() + if hashlib.sha256(data).hexdigest() != A6XX_SHA256: + raise ValueError('A6xx archive checksum mismatch') + with zipfile.ZipFile(io.BytesIO(data)) as archive: + driver = archive.read('libvulkan_freedreno.so') + original_meta = json.loads(archive.read('meta.json')) + if original_meta.get('libraryName') != 'libvulkan_freedreno.so': + raise ValueError('Unexpected A6xx driver library') + hosts = list((Path(ndk) / 'toolchains/llvm/prebuilt').glob('*')) + if len(hosts) != 1: + raise ValueError('Expected one NDK host toolchain') + llvm = hosts[0] / 'bin' + provenance = (f'Banners-Turnip {A6XX_TAG}\n{A6XX_URL}\nSHA256 {A6XX_SHA256}\n' + 'Mesa source: a0ff50acf0e1d152f54949490fa41558d2e0f7a4\n' + 'Quest adapter: DroidDeck tools/quest (GPL-3.0-or-later)\n').encode() + for quest in (False, True): + meta = dict(original_meta) + meta['name'] = 'Turnip Adreno 6xx' + (' - Quest 2 GPUOBJ experimental' if quest else ' - bundled fallback') + meta['description'] += ' Bundled for Adreno 6xx; Quest 2 hardware validation pending.' + entries = [('libvulkan_freedreno.so', driver), ('SOURCE.txt', provenance)] + if quest: + with tempfile.TemporaryDirectory(prefix='quest-a6xx-') as folder: + work = Path(folder) + original = work / 'libvulkan_freedreno.so' + original.write_bytes(driver) + wrapper = work / 'libquest_turnip.so' + subprocess.run([ + str(llvm / 'aarch64-linux-android28-clang'), '-shared', '-fPIC', '-O2', + '-Wall', '-Wextra', '-Werror', '-fvisibility=hidden', + '-Wl,-z,max-page-size=16384', '-Wl,-soname,libquest_turnip.so', + '-Wl,--no-as-needed', str(original), '-Wl,--as-needed', + str(ROOT / 'tools/quest/android_wrapper.c'), str(ROOT / 'tools/quest/kgsl_compat.c'), + '-ldl', '-llog', '-o', str(wrapper), + ], check=True) + subprocess.run([str(llvm / 'llvm-strip'), '--strip-unneeded', str(wrapper)], check=True) + entries.append(('libquest_turnip.so', wrapper.read_bytes())) + meta['libraryName'] = 'libquest_turnip.so' + entries.append(('meta.json', (json.dumps(meta, indent=2) + '\n').encode())) + name = 'turnip26.3.0-quest2' if quest else 'turnip26.3.0-a6xx' + pack(entries, output_dir / f'adrenotools-{name}.tzst') def build(ndk, output=OUTPUT): @@ -22,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' @@ -58,3 +129,4 @@ def build(ndk, output=OUTPUT): if __name__ == '__main__': build(sys.argv[1]) + build_a6xx(sys.argv[1]) 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/check_android_libraries.py b/tools/release/check_android_libraries.py index 765e226ac..8f79e8844 100644 --- a/tools/release/check_android_libraries.py +++ b/tools/release/check_android_libraries.py @@ -14,6 +14,7 @@ EXPORTS = ['Java_com_droiddeck_launcher_stores_StoresNative_nativeVersion', 'Java_com_droiddeck_launcher_stores_gog_GogNative_nativeStart', 'Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeStart', 'Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeAssemble', + 'Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeStream', 'Java_com_droiddeck_launcher_stores_amazon_AmazonNative_nativeStart'] diff --git a/tools/release/runtime_inputs.py b/tools/release/runtime_inputs.py index c9716edf8..6c7944ce1 100644 --- a/tools/release/runtime_inputs.py +++ b/tools/release/runtime_inputs.py @@ -53,11 +53,16 @@ def outputs(root): result['assets/pulseaudio.tzst'] = 'app/build/prepared-assets/pulseaudio.tzst' result['assets/graphics_driver/adrenotools-turnip25.1.0-quest1.tzst'] = \ 'app/build/prepared-assets/graphics_driver/adrenotools-turnip25.1.0-quest1.tzst' + for name in ['turnip26.3.0-a6xx', 'turnip26.3.0-quest2']: + result[f'assets/graphics_driver/adrenotools-{name}.tzst'] = \ + f'app/build/prepared-assets/graphics_driver/adrenotools-{name}.tzst' return result def required(): paths = ['assets/graphics_driver/adrenotools-turnip25.1.0-quest1.tzst'] + paths += [f'assets/graphics_driver/adrenotools-{name}.tzst' + for name in ['turnip26.3.0-a6xx', 'turnip26.3.0-quest2']] paths += ['assets/linuxfs/' + name for name in ['libfakeinput.so', 'libblsession.so', 'libblfastpath.so', 'libssbs.so', 'usr/local/bin/droiddeck-clipboard', 'usr/local/bin/gamescope', 'usr/local/lib/droiddeck/uruntime']] @@ -90,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(): @@ -107,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 22330ded0..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"]) @@ -193,7 +190,7 @@ class DroiddeckctlTest(FakeAdbTest): code, out = self.run_ctl("run", "/usr/bin/sh", "--", "-c", "true") self.assertEqual(7, code) self.assertEqual("AGENT_COMMANDS_DISABLED", out["error"]["code"]) - self.assertIn("Debugging tools", out["error"]["message"]) + self.assertIn("Setup > Diagnostics > Debugging tools", out["error"]["message"]) self.assertEqual(["access"], [c.get("method") for c in self.calls()]) def test_override_checks_names_before_pushing_any_file(self): @@ -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 16e8807f1..dda3eeea5 100644 --- a/tools/tests/test_msi_install.py +++ b/tools/tests/test_msi_install.py @@ -119,6 +119,32 @@ class LayoutTest(unittest.TestCase): inst.select() self.assertEqual(inst.included_components, {"Up", "Wide"}) + def test_a_duplicate_onto_the_file_itself_is_skipped(self): + with tempfile.TemporaryDirectory() as tmp: + src = Path(tmp) / "cab" / "dotnet.exe" + src.parent.mkdir() + src.write_bytes(b"MZ") + out = Path(tmp) / "out" + inst = installer({ + "Directory": rows("Directory Directory_Parent DefaultDir", + ("TARGETDIR", "", "SourceDir"), + ("ProgramFilesFolder", "TARGETDIR", "PFiles"), + ("DOTNETHOME", "ProgramFilesFolder", "dotnet"), + ("DOTNETHOME_X86", "ProgramFilesFolder", "dotnet")), + "Component": rows("Component Directory_ Attributes Condition KeyPath", + ("Host", "DOTNETHOME", "0", "", "DotnetExe")), + "Feature": rows("Feature Feature_Parent Level", ("Main", "", "1")), + "FeatureComponents": rows("Feature_ Component_", ("Main", "Host")), + "File": rows("File Component_ FileName", ("DotnetExe", "Host", "dotnet.exe")), + "DuplicateFile": rows("FileKey Component_ File_ DestName DestFolder", + ("Dup", "Host", "DotnetExe", "", "DOTNETHOME_X86")), + }, out=str(out)) + inst.select() + inst.install_files({"DotnetExe": src}) + placed = out / "drive_c/Program Files (x86)/dotnet/dotnet.exe" + self.assertEqual(placed.read_bytes(), b"MZ") + self.assertEqual(inst.counts["files"], 1) + def test_global_assembly_paths(self): with tempfile.TemporaryDirectory() as tmp: dll = Path(tmp) / "a.dll" @@ -240,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_proot_statx_mnt_id.py b/tools/tests/test_proot_statx_mnt_id.py new file mode 100644 index 000000000..e3dd59658 --- /dev/null +++ b/tools/tests/test_proot_statx_mnt_id.py @@ -0,0 +1,133 @@ +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path +import unittest + +PROOT = Path(__file__).resolve().parents[1] / "proot" + +PROGRAM = r""" +#define _GNU_SOURCE +#include <fcntl.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> +#include "statx_mnt_id.h" + +static int fdinfo_mnt_id(int fd, uint64_t *out) { + char path[64], line[256]; + snprintf(path, sizeof path, "/proc/self/fdinfo/%d", fd); + FILE *f = fopen(path, "r"); + if (!f) return 0; + int found = 0; + while (fgets(line, sizeof line, f)) { + unsigned long long id; + if (sscanf(line, "mnt_id:%llu", &id) == 1) { + *out = (uint64_t)id; + found = 1; + break; + } + } + fclose(f); + return found; +} + +int main(void) { + char name[] = "/tmp/droiddeck-statx-mnt-id-XXXXXX"; + int fd = mkstemp(name); + if (fd < 0) return 1; + + struct stat st; + if (fstat(fd, &st) < 0) return 2; + uint64_t expected; + if (!fdinfo_mnt_id(fd, &expected) || expected == 0) return 3; + + /* Model a successful pre-5.8 statx: inode fields are present, MNT_ID is absent. */ + struct statx result = {0}; + result.stx_mask = STATX_TYPE | STATX_INO; + result.stx_mode = st.st_mode; + result.stx_ino = st.st_ino; + result.stx_dev_major = major(st.st_dev); + result.stx_dev_minor = minor(st.st_dev); + unsigned flags = AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW; + unsigned mask = STATX_TYPE | STATX_INO | STATX_MNT_ID; + if (!droiddeck_proot_statx_fill_mnt_id(getpid(), fd, flags, "", mask, &result)) return 4; + if (!(result.stx_mask & STATX_MNT_ID) || result.stx_mnt_id != expected) return 5; + + /* A reused fd can name the same inode number on another device. Inode alone is not identity. */ + result.stx_mask = STATX_TYPE | STATX_INO; + result.stx_dev_major = major(st.st_dev) ^ 1U; + result.stx_dev_minor = minor(st.st_dev); + if (droiddeck_proot_statx_fill_mnt_id(getpid(), fd, flags, "", mask, &result)) return 6; + if (result.stx_mask & STATX_MNT_ID) return 7; + + /* Only ordinary MNT_ID is reconstructed; never claim the newer UNIQUE bit. */ + result.stx_ino = st.st_ino; + result.stx_dev_major = major(st.st_dev); + result.stx_dev_minor = minor(st.st_dev); + if (droiddeck_proot_statx_fill_mnt_id(getpid(), fd, flags, "", 0x4000U, &result)) return 8; + if (result.stx_mask & STATX_MNT_ID) return 9; + + if (droiddeck_proot_statx_fill_mnt_id(getpid(), AT_FDCWD, flags, "", mask, &result)) return 10; + if (droiddeck_proot_statx_fill_mnt_id(getpid(), -1, flags, "", mask, &result)) return 11; + if (droiddeck_proot_statx_fill_mnt_id(getpid(), fd, AT_SYMLINK_NOFOLLOW, "", mask, &result)) return 12; + if (droiddeck_proot_statx_fill_mnt_id(getpid(), fd, flags, "file", mask, &result)) return 13; + int stale = dup(fd); + if (stale < 0 || close(stale) < 0) return 14; + if (droiddeck_proot_statx_fill_mnt_id(getpid(), stale, flags, "", mask, &result)) return 15; + + /* Pin the original referent, then reuse its tracee fd number. The pin's fdinfo stays stable, + * while recovery against the reused number must reject the different inode/device. */ + char replacement_name[] = "/tmp/droiddeck-statx-mnt-id-replacement-XXXXXX"; + int replacement = mkstemp(replacement_name); + if (replacement < 0) return 16; + int pinned; + char path[64]; + snprintf(path, sizeof path, "/proc/%ld/fd/%d", (long)getpid(), fd); + pinned = open(path, O_PATH | O_CLOEXEC); + if (pinned < 0) return 17; + uint64_t pinned_mnt_id, pinned_ino; + if (!droiddeck_proot_read_fdinfo(pinned, &pinned_mnt_id, &pinned_ino) || + pinned_mnt_id != expected || pinned_ino != st.st_ino) return 18; + + struct stat replacement_st; + if (fstat(replacement, &replacement_st) < 0) return 19; + if (replacement_st.st_dev == st.st_dev && replacement_st.st_ino == st.st_ino) return 20; + unlink(name); + if (close(fd) < 0 || dup2(replacement, fd) != fd) return 21; + close(replacement); + unlink(replacement_name); + + result.stx_mask = STATX_TYPE | STATX_INO; + result.stx_ino = st.st_ino; + result.stx_dev_major = major(st.st_dev); + result.stx_dev_minor = minor(st.st_dev); + if (droiddeck_proot_statx_fill_mnt_id(getpid(), fd, flags, "", mask, &result)) return 22; + if (result.stx_mask & STATX_MNT_ID) return 23; + if (!droiddeck_proot_read_fdinfo(pinned, &pinned_mnt_id, &pinned_ino) || + pinned_mnt_id != expected || pinned_ino != st.st_ino) return 24; + + close(pinned); + close(fd); + return 0; +} +""" + + +@unittest.skipUnless(shutil.which("cc") and sys.platform.startswith("linux"), "requires Linux and a C compiler") +class ProotStatxMountIdTest(unittest.TestCase): + def test_missing_mount_id_is_recovered_only_for_the_same_open_inode(self): + with tempfile.TemporaryDirectory() as tmp: + source = Path(tmp) / "test.c" + program = Path(tmp) / "test" + source.write_text(PROGRAM) + subprocess.run(["cc", "-O2", "-Wall", "-Werror", "-I", str(PROOT), "-o", str(program), str(source)], check=True) + subprocess.run([str(program)], check=True) + + +if __name__ == "__main__": + unittest.main() 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 new file mode 100644 index 000000000..a9a437e3e --- /dev/null +++ b/tools/tests/test_zram_preload.py @@ -0,0 +1,174 @@ +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +PRELOAD = Path(__file__).resolve().parents[1] / "linuxfs/preload" + +PROGRAM = r""" +#define _GNU_SOURCE +#include <fcntl.h> +#include <pthread.h> +#include <signal.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/mman.h> +#include <unistd.h> + +static const char *ctl; +static size_t size, page; +static volatile int stop; +static unsigned char *shared; + +static long status(const char *key) { + FILE *f = fopen("/proc/self/status", "r"); + char line[256]; + long v = -1; + while (f && fgets(line, sizeof(line), f)) + if (!strncmp(line, key, strlen(key))) v = atol(line + strlen(key)); + if (f) fclose(f); + return v; +} + +static void request(char mode) { + int fd = open(ctl, O_WRONLY | O_TRUNC); + if (fd < 0 || write(fd, &mode, 1) != 1) exit(3); + close(fd); + kill(getpid(), SIGURG); +} + +static uint64_t sum(const unsigned char *p, size_t n) { + uint64_t s = 0; + for (size_t i = 0; i < n; i += 64) s = s * 31 + p[i]; + return s; +} + +static void *writer(void *arg) { + unsigned char *mine = shared + (size_t)(uintptr_t)arg * (size / 4); + for (unsigned char round = 1; !stop; round++) { + for (size_t i = 0; i < size / 4; i += page) { + if (round > 1 && mine[i] != (unsigned char)(round - 1)) { + printf("corrupt\n"); + exit(4); + } + mine[i] = round; + } + } + return NULL; +} + +int main(int argc, char **argv) { + ctl = argv[1]; + size = strtoul(argv[2], NULL, 0) << 20; + int cycles = atoi(argv[3]); + page = (size_t)sysconf(_SC_PAGESIZE); + unsigned char *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + for (size_t i = 0; i < size; i++) data[i] = (unsigned char)(i * 7 + i / 4096); + uint64_t before = sum(data, size); + shared = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + memset(shared, 0, size); + pthread_t t[2]; + for (uintptr_t i = 0; i < 2; i++) pthread_create(&t[i], NULL, writer, (void *)i); + struct sigaction sa; + sigaction(SIGURG, NULL, &sa); + printf("handler %d\n", (sa.sa_flags & SA_SIGINFO) ? 1 : 0); + if (argc > 4) { + 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); + madvise(ro, size, 21); + printf("elsewhere %ld\n", status("VmSwap:")); + request('0'); + printf("back %ld\n", status("VmSwap:")); + } + for (int c = 0; c < cycles; c++) { + request('1'); + printf("out %ld\n", status("VmSwap:")); + request('0'); + printf("back %ld\n", status("VmSwap:")); + } + stop = 1; + for (int i = 0; i < 2; i++) pthread_join(t[i], NULL); + printf("intact %d\n", sum(data, size) == before); + return 0; +} +""" + + +def swap_total_kb(): + for line in Path("/proc/meminfo").read_text().splitlines(): + if line.startswith("SwapTotal:"): + return int(line.split()[1]) + return 0 + + +@unittest.skipUnless(shutil.which("cc"), "needs a C compiler") +class ZramPreloadTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.dir = Path(tempfile.mkdtemp()) + cls.ctl = cls.dir / "ctl" + build = cls.dir / "build" + build.mkdir() + (build / "program.c").write_text(PROGRAM) + cls.lib = build / "libblsession.so" + subprocess.run(["cc", "-shared", "-fPIC", "-O2", "-Wall", "-pthread", "-DZR_CTL=\"%s\"" % cls.ctl, + "-o", str(cls.lib), *map(str, sorted(PRELOAD.glob("*.c"))), "-ldl"], check=True) + cls.steam = cls.dir / "steam" + subprocess.run(["cc", "-O2", "-pthread", "-o", str(cls.steam), str(build / "program.c")], check=True) + cls.game = cls.dir / "game" + shutil.copy(cls.steam, cls.game) + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.dir, True) + + def setUp(self): + self.ctl.write_text("0") + + def run_program(self, program=None, mb=48, cycles=1, *extra): + environment = dict(os.environ, LD_PRELOAD=str(self.lib)) + result = subprocess.run([str(program or self.steam), str(self.ctl), str(mb), str(cycles), *extra], env=environment, + capture_output=True, text=True, check=True, timeout=120) + out = {} + for line in result.stdout.splitlines(): + key, value = line.split() + out.setdefault(key, []).append(int(value)) + return out + + def test_only_the_client_takes_the_signal(self): + self.assertEqual([1], self.run_program()["handler"]) + self.assertEqual([0], self.run_program(self.game)["handler"]) + + def test_nothing_is_installed_without_the_control_file(self): + self.ctl.unlink() + try: + self.assertEqual([0], self.run_program(cycles=0)["handler"]) + finally: + self.ctl.write_text("0") + + @unittest.skipUnless(swap_total_kb() > 256 * 1024, "needs swap") + def test_pages_out_and_reads_back(self): + out = self.run_program(mb=48) + self.assertGreater(out["out"][0], 32 * 1024) + self.assertLess(out["back"][0], out["out"][0] // 4) + self.assertEqual([1], out["intact"]) + + @unittest.skipUnless(swap_total_kb() > 256 * 1024, "needs swap") + def test_reads_back_what_the_kernel_swapped_elsewhere(self): + out = self.run_program(None, 32, 0, "read-only") + self.assertGreater(out["elsewhere"][0], 16 * 1024) + self.assertLess(out["back"][0], out["elsewhere"][0] // 4) + + def test_memory_stays_intact_under_writers(self): + out = self.run_program(mb=32, cycles=6) + self.assertEqual([1], out["intact"]) + self.assertEqual(6, len(out["out"])) + + +if __name__ == "__main__": + unittest.main() 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 b9622945a..2ea58195b 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) { @@ -103,8 +89,6 @@ HmdQuaternion_t QuatFromMatrix(const float m[9]) { double VsyncPeriodSeconds(); -// 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 { @@ -117,7 +101,6 @@ public: } void WaitForPresent() override {} bool GetTimeSinceLastVsync(float *seconds, uint64_t *counter) override { - // A free-running vsync at the panel's rate. using namespace std::chrono; double t = duration<double>(steady_clock::now().time_since_epoch()).count(); double period = VsyncPeriodSeconds(); @@ -131,8 +114,6 @@ private: std::chrono::steady_clock::time_point last_; }; -// The vsync grid SteamVR paces against in driver-direct mode (Prop_DriverDirectModeSendsVsyncEvents): -// a free-running tick at the panel's rate, declared a little ahead of time (ALVR's announcer, alvr/server_openvr/src/vsync.rs). class Vsync { public: static constexpr std::chrono::microseconds kLead{2000}; @@ -182,10 +163,6 @@ private: double VsyncPeriodSeconds() { return Vsync::Interval().count() / 1e9; } -// 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 { @@ -200,8 +177,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, @@ -255,7 +232,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; @@ -284,7 +260,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::Interval() * (1 + (t ? t->nFramesToThrottle : 0)); if (until > std::chrono::steady_clock::now()) std::this_thread::sleep_until(until); @@ -314,12 +289,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, @@ -350,7 +322,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()) { @@ -373,10 +345,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) {} @@ -429,7 +397,6 @@ public: for (VRInputComponentHandle_t h : {trigger_, padX_, padY_}) in->UpdateScalarComponent(h, 0.0f, 0); } - // From the driver's pose thread: the head's orientation and the pad. static constexpr float kReachSpeed = 0.6f, kReachMin = -0.2f, kReachMax = 1.0f; static bool Grabbing(const PadState &pad, bool right) { @@ -465,7 +432,6 @@ public: p.qRotation.x = head.w * lx + head.x * lw + head.y * lz - head.z * ly; p.qRotation.y = head.w * ly - head.x * lz + head.y * lw + head.z * lx; p.qRotation.z = head.w * lz + head.x * ly - head.y * lx + head.z * lw; - // 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) + handX_, -0.25 + handY_, -0.35 - reach - pull_}; double m[3][3]; const double w = head.w, x = head.x, y = head.y, z = head.z; @@ -518,7 +484,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; @@ -539,8 +504,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"); @@ -552,9 +515,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); @@ -591,7 +551,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) { @@ -606,14 +566,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. const float scale = renderScale_; *w = (uint32_t)(height_ * scale * eyeAspect_); *h = (uint32_t)(height_ * scale); @@ -623,7 +581,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_; } @@ -671,7 +628,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))); @@ -788,7 +744,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 e34d0db7e..9fe7a6cf9 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; @@ -49,7 +48,6 @@ void Log(const char *fmt, ...) { fprintf(stderr, "[droiddeck] %s\n", buf); } -// ---------------------------------------------------------------- Xlib, loaded at runtime struct X11 { void *lib = nullptr; @@ -76,7 +74,6 @@ struct X11 { } }; -// ---------------------------------------------------------------- Vulkan, loaded at runtime #define VK_GLOBAL(X) X(CreateInstance) #define VK_INSTANCE(X) \ @@ -132,7 +129,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(); @@ -176,7 +173,6 @@ private: } } - // ------------------------------------------------------------ the presenting thread void Run() { const char *mode = getenv("DROIDDECK_VR_PRESENT"); @@ -217,9 +213,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] && tracked) { const auto start = std::chrono::steady_clock::now(); const int r = WaitDrawn(mark, start + kDrawnTimeout); @@ -317,10 +310,6 @@ private: } } - // ------------------------------------------------------------ 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; @@ -373,9 +362,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; if (!PanelSize(&width_, &height_)) width_ = height_ = 0; if (!width_ && cfgW_ > 0 && cfgH_ > 0) { width_ = (uint32_t)cfgW_; height_ = (uint32_t)cfgH_; } @@ -386,8 +372,6 @@ private: bool InitWindow() { 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); @@ -397,14 +381,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_); @@ -533,15 +514,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)}; @@ -611,8 +587,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, @@ -701,11 +675,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 f9c63943b..e88772fab 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; @@ -309,13 +260,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 clock at the panel's rate. Size and rate: - * DROIDDECK_VR_DISPLAY_MODE=WIDTHxHEIGHT@HZ (default 1920x1080, at the head pose file's rate, else 60). */ #include <fcntl.h> #include <stdatomic.h> @@ -491,8 +435,6 @@ static VkResult VKAPI_CALL GetRandROutputDisplayEXT(VkPhysicalDevice pd, void *d *d = FAKE_DISPLAY; return VK_SUCCESS; } -/* VK_EXT_display_control on the device: a clock at the panel's rate. 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}; @@ -516,14 +458,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}; @@ -537,13 +475,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; @@ -563,16 +500,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> @@ -586,15 +513,15 @@ 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; uint64_t seen, seen_us; }; 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; @@ -613,7 +540,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; @@ -625,7 +551,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++) { @@ -716,7 +641,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; @@ -731,7 +655,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); @@ -775,7 +699,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); @@ -791,7 +715,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); @@ -806,18 +729,9 @@ 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 #define SHARED_IDLE_US 100000 -/* 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); @@ -832,7 +746,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"); @@ -858,7 +771,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); @@ -882,7 +794,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; @@ -896,7 +807,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); @@ -1083,8 +993,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; @@ -1118,7 +1026,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; @@ -1127,7 +1034,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 = submit_tracked(next, q, count, copy, fence); @@ -1196,7 +1103,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; @@ -1233,7 +1139,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; @@ -1292,7 +1197,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 };