diff --git a/.gitattributes b/.gitattributes index 3cb68a29e..4c0a6c312 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,7 +3,6 @@ tools/mangoapp/** text=auto eol=lf tools/comet/LICENSE whitespace=-blank-at-eof tools/comet/droiddeck.patch whitespace=-blank-at-eol,-blank-at-eof -# These helpers are packaged verbatim and executed inside the Linux runtime. tools/linuxfs/overlay/** text eol=lf tools/linuxfs/desktop/** text eol=lf tools/mangoapp/mangoapp text eol=lf diff --git a/.github/trusted-test-builds.txt b/.github/trusted-test-builds.txt index 9b09f6132..f91528f39 100644 --- a/.github/trusted-test-builds.txt +++ b/.github/trusted-test-builds.txt @@ -1,4 +1,3 @@ -# GitHub PR openers trusted for automatic release-signed test builds. FX192 K11MCH1 kingokksa diff --git a/.github/workflows/build-fex.yml b/.github/workflows/build-fex.yml index 8ec3369a1..deb099100 100644 --- a/.github/workflows/build-fex.yml +++ b/.github/workflows/build-fex.yml @@ -1,8 +1,5 @@ name: Build FEX (x86-64 Steam client) -# Checks tools/fex: the same build DroidDeck-Components' build-fex.yml runs from this repository to publish FEX -# (its single `fex` release and fex.json, which the app follows). Nothing is published from here. - on: push: paths: @@ -31,7 +28,6 @@ jobs: with: persist-credentials: false - name: Build in Arch Linux ARM - # Inputs reach the shell as environment variables, never pasted into the script. env: FEX_EXTRA_CXXFLAGS: ${{ inputs.cxxflags }} FEX_THUNKS: ${{ inputs.thunks == 'true' && '1' || '0' }} diff --git a/.github/workflows/publish-packs.yml b/.github/workflows/publish-packs.yml index 7bf219ef7..de4adb3b2 100644 --- a/.github/workflows/publish-packs.yml +++ b/.github/workflows/publish-packs.yml @@ -1,11 +1,5 @@ name: Publish add-on packs -# The add-on packs (tools/packs/build.sh) a Build APK run made, published to DroidDeck-Components under -# content-addressed tags (pack--). The APK from that run pins each pack's -# SHA-256 and downloads it from that tag. A tag is created once and never replaced. -# Only main and release builds publish here; a PR test build attaches the packs main has not published -# to its pr-N release on DroidDeck-CI (pr-release-publish.yml), which is deleted when the PR closes. - on: workflow_call: inputs: diff --git a/README.md b/README.md index 9b3f3c443..646d70214 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ DroidDeck is [GPL-3.0](LICENSE). Steam and Proton belong to Valve Corporation. V - libblsession runs Wine's arm64 `SIGUSR2` context handler (`dlls/ntdll/unix/signal_arm64.c`, LGPL-2.1-or-later) in place on a frame laid out like [Linux](https://github.com/torvalds/linux)'s `arch/arm64/kernel/signal.c`, and serves `fill_vm_counters` in `dlls/ntdll/unix/process.c` the kernel's own `/proc//status`. - droiddeck-ntsync follows the [Linux](https://github.com/torvalds/linux) ntsync semantics in `Documentation/userspace-api/ntsync.rst` (GPL-2.0). - The proot fast path orders bindings like [PRoot](https://github.com/proot-me/proot)'s `path/binding.c` (GPL-2.0-or-later). +- DiRT 3 and DiRT Showdown keep their telemetry socket the way RoccoC's DiRTTelemetryErrorFix (MIT) does, based on the fix by Carsten Gottschling. - Games use [Mesa](https://gitlab.freedesktop.org/mesa/mesa) Turnip's per render pass memory choice. The Steam client keeps `TU_DEBUG=sysmem`. - Black Ops III and Dark Souls Remastered start without [umu-protonfixes](https://github.com/Open-Wine-Components/umu-protonfixes) (BSD-2-Clause), whose `311210.py` and `570940.py` fail under ARM64 Proton. - [ReSkate](https://github.com/Dingo-Shenanigans/ReSkate) (GPL-3.0) by Dingo Shenanigans uses the launch option from its README and D3D12 feature level 12_1. @@ -93,8 +94,10 @@ DroidDeck is [GPL-3.0](LICENSE). Steam and Proton belong to Valve Corporation. V - Battle.net is by [@kayakyakr](https://github.com/kayakyakr). It runs Blizzard's own client in Proton (downloaded from Blizzard, not included) and shows key art from the [Battle.net Shop](https://shop.battle.net). Without Steam, its games see the pad as an Xbox 360 controller via `PROTON_SPOOF_STEAMINPUT_VIDPID` in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/winebus.sys`. - Battle.net installs drive Blizzard's Agent the way [@barncastle](https://github.com/barncastle)'s [Battle.Net-Installer](https://github.com/barncastle/Battle.Net-Installer) does, with requests from [wowdev.wiki](https://wowdev.wiki/Agent). Read without taking code: [@mubaraknumann](https://github.com/mubaraknumann)'s [unifideck](https://github.com/mubaraknumann/unifideck), [@djmittens](https://github.com/djmittens)'s [neo-mittens](https://github.com/djmittens/neo-mittens) `bnetswitch`, [@BCD1210](https://github.com/BCD1210)'s [soju](https://github.com/BCD1210/soju) and [Lutris](https://github.com/lutris/lutris)'s `product.db` reader. - Xbox games load [@domsvoboda](https://github.com/domsvoboda)'s [xodus-nexus](https://github.com/domsvoboda/xodus-nexus) modules (LGPL-2.1-or-later), including `wintypes.dll`. Minecraft's GameInput setup was checked against [@Wyze3306](https://github.com/Wyze3306)'s [BedrockOnLinux](https://github.com/Wyze3306/BedrockOnLinux) (MIT). +- The Xbox Cloud Gaming player uses the WebRTC client of [@unknownskl](https://github.com/unknownskl)'s [Greenlight](https://github.com/unknownskl/greenlight) (MIT), and its offer tweaks and sharpening follow [@redphx](https://github.com/redphx)'s [Better xCloud](https://github.com/redphx/better-xcloud) (MIT). See `tools/xcloud-player/NOTICE.md`. - The itch.io butler pack ships butler's `7z.so`, itch.io's build of [7-Zip](https://www.7-zip.org) by Igor Pavlov (LGPL-2.1, unRAR restriction on RAR code), unmodified. Other archives use Apache Commons Compress and the runtime's bsdtar (libarchive). - The Store uses [Flatpak](https://github.com/flatpak/flatpak) (LGPL-2.1) through DroidDeck's own bubblewrap shim. Chromium and Electron apps start directly, since [zypak](https://github.com/refi64/zypak) by [@refi64](https://github.com/refi64) (BSD-3-Clause) always uses Flatpak's spawn portal. +- Folder sync runs [Syncthing](https://github.com/syncthing/syncthing) (MPL-2.0), built from [@researchxxl](https://github.com/researchxxl)'s [syncthing-android](https://github.com/researchxxl/syncthing-android) fork (MPL-2.0). @@ -103,6 +106,7 @@ DroidDeck is [GPL-3.0](LICENSE). Steam and Proton belong to Valve Corporation. V - Steam audio goes through [@The412Banner](https://github.com/The412Banner)'s [DirectAudio](https://github.com/The412Banner/directaudio) PulseAudio sink (LGPL-2.1-or-later), built from `tools/directaudio/sink`. - The DroidDeck HUD is [@The412Banner](https://github.com/The412Banner)'s [FusionHUD](https://github.com/The412Banner/FusionHUD) (GPL-3.0 with an attribution term). Its readings match Valve's [MangoHud](https://github.com/flightlessmango/MangoHud) build (MIT), with frame times from gamescope patch 0125. +- The HUD's CPU, GPU load, thermal and battery readers come from [@xXJSONDeruloXx](https://github.com/xXJSONDeruloXx)'s work. - LSFG frame generation comes from Camille LaVey and the [Eden](https://eden-emu.dev) project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported by [@maxjivi05](https://github.com/maxjivi05). It needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. - Remote Play decodes through Android's MediaCodec or the [FFmpeg](https://ffmpeg.org) (LGPL-2.1-or-later) in Steam's runtime. Qualcomm low latency settings follow [Moonlight](https://github.com/moonlight-stream/moonlight-android) (GPL-3.0). - [PipeWire](https://gitlab.freedesktop.org/pipewire/pipewire) (MIT) sockets use the settings in its `module-access.c`, and [WirePlumber](https://gitlab.freedesktop.org/pipewire/wireplumber) (MIT) keeps those clients at full permissions. @@ -125,6 +129,7 @@ DroidDeck is [GPL-3.0](LICENSE). Steam and Proton belong to Valve Corporation. V Smart glasses - Builds on WinNative's VITURE work. Protocol checked against [@wheaney](https://github.com/wheaney)'s [XRLinuxDriver](https://github.com/wheaney/XRLinuxDriver), [@badicsalex](https://github.com/badicsalex)'s [ar-drivers-rs](https://github.com/badicsalex/ar-drivers-rs) and [go-macos/iokit](https://pkg.go.dev/github.com/go-macos/iokit/viture). The optional VITURE SDK is not included. +- The VITURE protocol V2 frames adapt [@elasticjava](https://github.com/elasticjava)'s [viture-v2](https://github.com/elasticjava/viture-v2) (MIT) by Holger Bartnick. See `docs/third-party/viture-v2`. - RayNeo adapts [@informalTechCode](https://github.com/informalTechCode)'s [RayNeo-Air-Series-3DoF-Scaffold](https://github.com/informalTechCode/RayNeo-Air-Series-3DoF-Scaffold) (Apache-2.0) and references [@verncat](https://github.com/verncat)'s [RayNeo-Air-3S-Pro-OpenVR](https://github.com/verncat/RayNeo-Air-3S-Pro-OpenVR) (MIT) and [Monado](https://gitlab.freedesktop.org/monado/monado) (BSL-1.0). See [docs/third-party/rayneo](docs/third-party/rayneo/README.md). - XREAL 1S references XRLinuxDriver and [xrealOneDeviceKit](https://github.com/wheaney/xrealOneDeviceKit) by @wheaney, [@rohitsangwan01](https://github.com/rohitsangwan01)'s [xreal_one_driver](https://github.com/rohitsangwan01/xreal_one_driver) (MIT) and [@0xcaff](https://github.com/0xcaff)'s [xr-tools](https://github.com/0xcaff/xr-tools). - Rokid follows ar-drivers-rs, @badicsalex's [Void Computing write-ups](https://voidcomputing.hu/blog/worse-better-prettier/) and Monado's Rokid driver. diff --git a/app/build.gradle b/app/build.gradle index 7459cc637..91fd2ad94 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,7 +4,6 @@ plugins { apply plugin: 'kotlin-android' apply plugin: 'org.jetbrains.kotlin.plugin.compose' -// Machine-specific tooling and optional CI-prepared inputs stay out of version control. def localBuildOptions = new Properties() def localBuildOptionsFile = rootProject.layout.projectDirectory.file('local.properties') if (localBuildOptionsFile.asFile.exists()) { @@ -98,8 +97,6 @@ def addonPacks = ['proot', 'directaudio', 'tracking', 'xbox', 'comet', 'butler'] def pack = file("build/packs/${name}.tar.zst") if (!pack.isFile()) { if (ciBuild) throw new GradleException("Add-on pack ${pack} is missing: run tools/packs/build.sh app/build/packs") - // A source build that did not compile proot still pins a published pack, so the app - // downloads the patched binary instead of falling back to the rootfs copy. if (name == 'proot' && prootPackPin.PROOT_PACK_SHA256) { return [(name): [sha256: prootPackPin.PROOT_PACK_SHA256, size: (prootPackPin.PROOT_PACK_SIZE ?: '0') as long]] } @@ -208,7 +205,6 @@ android { } androidResources { - // aapt's default ignore list, plus the trees shipped as add-on packs (tools/packs). ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:_*:!CVS:!thumbs.db:!picasa.ini:!*~:droiddeck-xbox:droiddeck-comet:directaudio' } @@ -219,7 +215,6 @@ android { jniLibs { useLegacyPackaging = true pickFirsts += ['**/libtermux.so'] - // Shipped in the proot and tracking packs (tools/packs) and run or loaded from there. excludes += ['**/libproot.so', '**/libproot-loader.so', '**/libopencv_java4.so', '**/libmediapipe_tasks_jni.so'] } } @@ -238,7 +233,6 @@ android { outputs.upToDateWhen { false } outputs.doNotCacheIf('CI executes the test suite on every run') { true } } - // Robolectric keeps a sandbox per SDK level the tests ask for; seven no longer fit in Gradle's default 512 MB. maxHeapSize = '2g' maxParallelForks = 2 def logTarget = providers.systemProperty('robolectric.logging').orNull @@ -255,7 +249,6 @@ android { } } -// Vendor binaries remain in the user's SDK directory; never vendor them into Git. if (vitureSdkDir) { android.sourceSets.main.jniLibs.srcDir(new File(vitureSdkDir, 'android')) def stageVitureNotices = tasks.register('stageVitureNotices', Copy) { @@ -300,7 +293,6 @@ def stageGogNotices = tasks.register('stageGogNotices', Copy) { android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('gogNotices')) tasks.named('preBuild').configure { dependsOn(stageGogNotices) } -// CI publishes its packs; a local build pins packs nobody hosts (see addonPacks), so the APK carries them. if (!ciBuild) { def stageLocalPacks = tasks.register('stageLocalPacks', Sync) { from(layout.buildDirectory.dir('packs')) { include '*.tar.zst' } @@ -374,7 +366,6 @@ dependencies { implementation 'androidx.camera:camera-camera2:1.4.2' implementation 'androidx.camera:camera-lifecycle:1.4.2' implementation 'androidx.webkit:webkit:1.12.1' - // Google's WebRTC (BSD) for the native Xbox Cloud Gaming player (XboxNativePlayerActivity). implementation 'io.github.webrtc-sdk:android:125.6422.07' implementation("com.google.mediapipe:tasks-vision:${trackingPack.MEDIAPIPE_VERSION}") { exclude group: 'com.google.auto.value', module: 'auto-value' diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 61c7c9c2f..16c751605 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -8,7 +8,6 @@ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,max-page-size=16384 add_subdirectory(adrenotools) -# DXVK's DXBC->SPIR-V translator is zlib licensed (vendored under thirdparty/dxbc). add_subdirectory(thirdparty/dxbc) set(DDFG_UPSTREAM "v0.3.0") @@ -45,7 +44,6 @@ add_library(ddvenc SHARED venc/venc_server.cpp) target_compile_features(ddvenc PRIVATE cxx_std_17) target_link_libraries(ddvenc mediandk log) -# The optional proprietary SDK is supplied locally; its headers/libraries are not vendored. if(DROIDDECK_VITURE_SDK_DIR) add_library(droiddeck_viture SHARED viture/sdk_bridge.cpp) target_compile_features(droiddeck_viture PRIVATE cxx_std_17) diff --git a/app/src/main/cpp/fakeinput_steam.cpp b/app/src/main/cpp/fakeinput_steam.cpp index 9eb73a001..8906b01f7 100644 --- a/app/src/main/cpp/fakeinput_steam.cpp +++ b/app/src/main/cpp/fakeinput_steam.cpp @@ -78,7 +78,6 @@ static constexpr uint8_t GAMEPAD_AXIS_COUNT = 8; static constexpr uint8_t GAMEPAD_BUTTON_COUNT = 11; static constexpr uint32_t FAKE_INPUT_RING_MAGIC = 0x46494252; static constexpr uint32_t FAKE_INPUT_RING_VERSION = 2; -// Rings hold the 64-bit input_event layout that the arm64 writers use; 32-bit guests convert on read. struct RingEvent { int64_t sec; int64_t usec; @@ -391,7 +390,6 @@ send_vibration(int strong, int weak, uint16_t duration_ms, uint16_t slot) { syscall(SYS_close, sock); } -// Must match RumbleComponent.HOLD_MS: play until the slot's next effect or stop. static constexpr uint16_t RUMBLE_HOLD_MS = 0xffff; __attribute__((visibility("hidden"))) static void send_ff_effect(const struct ff_effect &effect, uint16_t slot) { @@ -1243,7 +1241,6 @@ static constexpr int TRITON_REPORT_ID_BYTES = 1; static constexpr int TRITON_STATE_BYTES = 45; static constexpr int TRITON_REPORT_INTERVAL_US = 4000; static constexpr int TRITON_FEATURE_TIMEOUT_MS = 500; -// The app clears output_length once it has taken a report; a writer waits this long for that before overwriting. static constexpr int TRITON_OUTPUT_TIMEOUT_MS = 50; static constexpr const char *TRITON_NAME = "Steam Controller"; static constexpr const char *TRITON_SERIAL = "DROIDDECKSC01"; @@ -2673,9 +2670,6 @@ __attribute__((visibility("hidden"))) static int triton_get_feature(DeckHidraw & static constexpr uint32_t TRITON_HWID = 72; static constexpr uint32_t TRITON_FW_TIMESTAMP = 0x6AC686B3; -// Steam compares the controller's firmware with TRITON_FW_TS in its bundled hardwareupdater.cfg (and blocks -// with a required update below MUST_UPDATE_TRITON_FW_TS), so the emulated pad reports at least the build -// Steam ships. static uint32_t triton_fw_timestamp() { static const uint32_t timestamp = [] { std::vector roots; @@ -3679,7 +3673,6 @@ EXPORT int ioctl(int fd, ioctl_request_t op, ...) { Logger::log("Hooking ioctl EVIOCGRAB for event %s\n", event); return 0; } else if (type == 0x6A && !strncmp(event, "event", 5)) { - // Older SDL2 builds treat any JSIOCGNAME answer as a joydev node and skip EVIOCGID, then drop the pad as 0000:0000. Logger::log("Rejecting joydev ioctl %d for event %s\n", number, event); guard.unlock(); errno = EINVAL; diff --git a/app/src/main/cpp/framegen/ddfg/record_impl.inc b/app/src/main/cpp/framegen/ddfg/record_impl.inc index 734d94cdc..f366d9a9c 100644 --- a/app/src/main/cpp/framegen/ddfg/record_impl.inc +++ b/app/src/main/cpp/framegen/ddfg/record_impl.inc @@ -267,8 +267,6 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur pFlowReg_, flowRegA_, flowRegB_, gmSsbo_, kLevels, cfg_); } - // Every generation of one real frame is recorded into the same command buffer, so the flow - // between prev and curr is computed once and each generation gets its own synth UBO and set. if (reuseFlow && !g_scratch.flowReady) return; if (!reuseFlow) { bool gmEngaged = updateGlobalMotion(gmSlot, extent_); diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_dll.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_dll.cpp index 3a37f032a..f9ee32463 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_dll.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_dll.cpp @@ -1,5 +1,4 @@ -// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS). -// Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later. +// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS). Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later. #include "lsfg_dll.h" #include "lsfg_dxbc.h" diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_dll.h b/app/src/main/cpp/framegen/lsfg/lsfg_dll.h index a20a6c631..81bf6781d 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_dll.h +++ b/app/src/main/cpp/framegen/lsfg/lsfg_dll.h @@ -1,5 +1,4 @@ -// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS). -// Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later. +// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS). Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later. #pragma once diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.h b/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.h index 17ba1876d..2f5286b26 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.h +++ b/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.h @@ -1,5 +1,4 @@ #pragma once -// DXVK's dxbc subset is zlib licensed; vendored under cpp/thirdparty/dxbc. #include #include diff --git a/app/src/main/cpp/questxr/gpu_frames.h b/app/src/main/cpp/questxr/gpu_frames.h index dc69dbbfb..77d7a6db6 100644 --- a/app/src/main/cpp/questxr/gpu_frames.h +++ b/app/src/main/cpp/questxr/gpu_frames.h @@ -76,7 +76,6 @@ public: } lastFrame_ = frame.header.frame_id; if (state == XrGpuFrameState::Stale) { - // No GPU read was submitted, so the producer can safely reclaim a dropped frame. dropped_++; if (!xr_gpu_send(peer_, &frame.header.frame_id, sizeof(frame.header.frame_id))) Disconnect(); return false; @@ -88,7 +87,6 @@ public: glBindImageTexture(0, raw_, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8); glDispatchCompute((width_ + 7) / 8, (height_ + 7) / 8, 1); glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); - // The producer may overwrite this slot only after the native GPU has finished reading it. glFinish(); if (glGetError() != GL_NO_ERROR) { rejected_++; Disconnect(); return false; } if (!xr_gpu_send(peer_, &frame.header.frame_id, sizeof(frame.header.frame_id))) Disconnect(); diff --git a/app/src/main/cpp/venc/venc_proto.h b/app/src/main/cpp/venc/venc_proto.h index 47fb6ef24..e69bbd822 100644 --- a/app/src/main/cpp/venc/venc_proto.h +++ b/app/src/main/cpp/venc/venc_proto.h @@ -3,10 +3,6 @@ #include -/* SOCK_SEQPACKET between the guest's Remote Play hooks and the app: the emulated /dev/video-enc0 and - * MediaCodec encoder, and remote touches. A config carries the buffer memfds (inputs first) as SCM_RIGHTS; - * every frame gets one reply, touches get none. */ - #define VENC_MAX_BUFFERS 16 enum venc_msg { VENC_CONFIG = 1, VENC_FRAME = 2, VENC_DONE = 3, VENC_BITRATE = 4, VENC_TOUCH = 5, VDEC_CONFIG = 6, VDEC_CAPTURE = 7, VDEC_PACKET = 8, VDEC_RESULT = 9 }; @@ -45,7 +41,6 @@ struct venc_bitrate { uint32_t bitrate; }; -/* Remote Play's touch-as-mouse turned back into a touch; x and y are fractions of the screen in 1/65535. */ struct venc_touch { uint32_t type; uint32_t action; diff --git a/app/src/main/cpp/venc/venc_server.cpp b/app/src/main/cpp/venc/venc_server.cpp index 17a5d7eed..fd3a9ad13 100644 --- a/app/src/main/cpp/venc/venc_server.cpp +++ b/app/src/main/cpp/venc/venc_server.cpp @@ -33,8 +33,8 @@ struct Mapping { constexpr int32_t kColorFormatYuv420SemiPlanar = 21; constexpr uint32_t kBufferFlagKeyFrame = 1; constexpr uint32_t kBufferFlagCodecConfig = 2; -constexpr int kTouchWidth = 1920, kTouchHeight = 1080; // InputSpace.kt -constexpr int kRemoteTouchId = 31; // the compositor's last touch slot (MAX_TOUCH_POINTS 32) +constexpr int kTouchWidth = 1920, kTouchHeight = 1080; +constexpr int kRemoteTouchId = 31; int32_t avc_profile(int32_t v4l2) { switch (v4l2) { @@ -549,7 +549,7 @@ void serve(int listener) { } } -} // namespace +} extern "C" JNIEXPORT jboolean JNICALL Java_com_droiddeck_launcher_session_VideoEncodeBridge_nativeStart(JNIEnv *env, jobject, jstring jpath) { diff --git a/app/src/main/cpp/viture/display_modes.h b/app/src/main/cpp/viture/display_modes.h index 3a78e03ae..b6b2288e2 100644 --- a/app/src/main/cpp/viture/display_modes.h +++ b/app/src/main/cpp/viture/display_modes.h @@ -1,20 +1,18 @@ #pragma once -// Our controller uses the standard display-mode vocabulary, even for Beast. -// Native mode reuses some numbers with different meanings; never pass them through. namespace droiddeck::viture { constexpr int to_native_mode(int standard) { switch (standard) { - case 0x31: return 0x31; // 1080p 60 - case 0x33: return 0x32; // 1080p 90 - case 0x34: return 0x33; // 1080p 120 - case 0x32: return 0x37; // 1080p SBS 60 - case 0x35: return 0x38; // 1080p SBS 90 - case 0x41: return 0x34; // 1200p 60 - case 0x43: return 0x35; // 1200p 90 - case 0x44: return 0x36; // 1200p 120 - case 0x42: return 0x3a; // 1200p SBS 60 - case 0x45: return 0x3b; // 1200p SBS 90 + case 0x31: return 0x31; + case 0x33: return 0x32; + case 0x34: return 0x33; + case 0x32: return 0x37; + case 0x35: return 0x38; + case 0x41: return 0x34; + case 0x43: return 0x35; + case 0x44: return 0x36; + case 0x42: return 0x3a; + case 0x45: return 0x3b; default: return -4; } } @@ -30,7 +28,7 @@ constexpr int from_native_mode(int native) { case 0x36: return 0x44; case 0x3a: return 0x42; case 0x3b: return 0x45; - default: return -4; // Ultrawide is not stereo. + default: return -4; } } } diff --git a/app/src/main/cpp/viture/mjpeg_payload.h b/app/src/main/cpp/viture/mjpeg_payload.h index a451a4eee..470ecf550 100644 --- a/app/src/main/cpp/viture/mjpeg_payload.h +++ b/app/src/main/cpp/viture/mjpeg_payload.h @@ -5,7 +5,6 @@ inline std::size_t mjpeg_payload_size(const uint8_t* bytes, std::size_t size) { if (!bytes || size < 4 || size > 4*1024*1024 || bytes[0] != 0xff || bytes[1] != 0xd8) return 0; std::size_t end = size; - // Luma Ultra UVC frames can pad the JPEG to an eight-byte boundary with zeros. while (end > 4 && size-end < 7 && bytes[end-1] == 0) --end; return bytes[end-2] == 0xff && bytes[end-1] == 0xd9 ? end : 0; } diff --git a/app/src/main/cpp/viture/sdk_bridge.cpp b/app/src/main/cpp/viture/sdk_bridge.cpp index 053c28335..04ee47a35 100644 --- a/app/src/main/cpp/viture/sdk_bridge.cpp +++ b/app/src/main/cpp/viture/sdk_bridge.cpp @@ -30,7 +30,6 @@ #include "stereo_frame_gate.h" namespace { -// SDK callbacks have no user-data parameter. Only one provider may exist at a time. std::mutex api_mutex; std::mutex pose_mutex; std::array latest_pose{}; @@ -64,7 +63,6 @@ void on_pose(float* data, uint64_t) { std::lock_guard lock(pose_mutex); if (!accepting_pose) return; std::copy_n(data + 3, 4, latest_pose.begin()); - // SDK device ticks are not Android's elapsed-realtime clock. latest_time = now_ns(); } @@ -234,8 +232,6 @@ int set_display(Session& s, int standard) { if (mode == 0) { int rc = xr_device_provider_set_display_mode(s.device, standard); if (rc != VITURE_GLASSES_ERROR_NOT_SUPPORTED) return rc; - // Older Beast firmware exposes fewer bypass modes. Fall back to its - // separate native table only when the SDK explicitly rejects the mode. } const int native = droiddeck::viture::to_native_mode(standard); if (native < 0) return native; diff --git a/app/src/main/cpp/waylandcomp/src/cardboard.c b/app/src/main/cpp/waylandcomp/src/cardboard.c index 03c434316..982c3bc47 100644 --- a/app/src/main/cpp/waylandcomp/src/cardboard.c +++ b/app/src/main/cpp/waylandcomp/src/cardboard.c @@ -34,7 +34,6 @@ void cardboard_clamp(struct cardboard_params *p) { p->screen_width_m = clampf(p->screen_width_m, 0.2f, 60.0f); p->k1 = clampf(p->k1, -1.0f, 2.0f); p->k2 = clampf(p->k2, -1.0f, 2.0f); - // Optical vertical FOV on supported glasses can be as low as ~22 degrees. p->fov_deg = clampf(p->fov_deg, 20.0f, 140.0f); p->lens_inset = clampf(p->lens_inset, -0.25f, 0.25f); p->chroma = clampf(p->chroma, 0.0f, 0.05f); diff --git a/app/src/main/cpp/waylandcomp/src/cardboard_frag.h b/app/src/main/cpp/waylandcomp/src/cardboard_frag.h index f74676d6a..fb098c4a3 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,0x000001ba,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 68039b444..471748a6d 100644 --- a/app/src/main/cpp/waylandcomp/src/compositor.c +++ b/app/src/main/cpp/waylandcomp/src/compositor.c @@ -289,7 +289,6 @@ static struct wl_event_source *g_fallback_timer; static int g_fallback_armed; volatile int g_fps_limit; -/* fps limit -1 (phone output, not mirrored): replaced buffers go back to the client at once instead of once per refresh. */ volatile int g_release_unpaced; volatile int g_hide_shell; @@ -446,15 +445,10 @@ static int g_cursor_w, g_cursor_h; static int g_cursor_hidden = 1; static int g_cursor_serial; -/* The pointer image as a compositor-owned texture, for outputs that cannot show - * the phone's Compose overlay (the glasses surface owns the picture there). */ -#define CURSOR_MAX_DRAWS 2 /* one per eye when the picture is side-by-side */ +#define CURSOR_MAX_DRAWS 2 static struct vkp_image *g_cursor_img; static int g_cursor_img_w, g_cursor_img_h, g_cursor_img_serial = -1; static atomic_int g_cursor_composite; -/* Last request from the app, used only to skip repeated calls. The applied flag below is - * written by the caller and read by the render path; message 22 only wakes the event loop, so - * a queued message can never put back an older value. */ static atomic_int g_cursor_composite_want; static void cursor_publish_shown(void) { @@ -1922,8 +1916,6 @@ static int hdr_plan(const struct draw_list *dl, int w, int h, int fx_on, int fra } if (un && g_zero_copy) { hs->color = droiddeck_color_of(un->resource); hs->who = un; return 3; } hs->hf.tonemap = !droiddeck_color_output(); - /* The pointer draws are appended after this plan: reserve their slots (zeroed, - * so they count as SDR content above the game) instead of reading past the end. */ if (dl->n <= 0 || !(hs->is_hdr = calloc((size_t)dl->n + CURSOR_MAX_DRAWS, 1))) return 0; for (int i = 0; i < dl->n; i++) { struct surface *s = surface_for_image(dl->d[i].img); @@ -2026,12 +2018,6 @@ static void hdr_count_copied(const struct draw_list *dl, const char *reason) { } } -/* - * Stream mode (the Xbox Cloud Gaming native player, in its own process): no Wayland clients, the scene is one - * picture handed over as AHardwareBuffers (ImageReader images of the decoded video). Each buffer is imported once - * and kept while the reader cycles through its few buffers; the newest frame is the scene, so the screen effects - * and frame generation apply to it as to a game. - */ #define STREAM_SLOTS 6 static pthread_mutex_t g_stream_lock = PTHREAD_MUTEX_INITIALIZER; static atomic_int g_stream_on; @@ -2051,7 +2037,6 @@ static void stream_forget_all(void) { g_stream_img = NULL; } -/* Compositor thread: the newest handed-over buffer becomes the scene picture. */ static void stream_take(void) { pthread_mutex_lock(&g_stream_lock); AHardwareBuffer *ahb = g_stream_pending; @@ -2065,7 +2050,7 @@ static void stream_take(void) { if (g_stream_slots[i].used < g_stream_slots[oldest].used) oldest = i; } if (slot >= 0) { - AHardwareBuffer_release(ahb); /* the slot keeps its own reference */ + AHardwareBuffer_release(ahb); } else { struct vkp_image *img = vkp_image_from_ahb(ahb); if (!img) { @@ -2077,7 +2062,7 @@ static void stream_take(void) { droiddeck_log("stream", "decoded frame buffer imported (%dx%d)", vkp_image_width(img), vkp_image_height(img)); slot = oldest; if (g_stream_slots[slot].img) { - vkp_wait_idle(); /* the GPU may still read the picture being replaced */ + vkp_wait_idle(); if (g_stream_img == g_stream_slots[slot].img) g_stream_img = NULL; vkp_image_destroy(g_stream_slots[slot].img); } @@ -2123,7 +2108,6 @@ void droiddeck_stream_set(int on) { (void)n; } -/* Any thread: hands over one decoded frame (acquires its own reference). */ void droiddeck_stream_frame(AHardwareBuffer *ahb) { if (!ahb || !atomic_load(&g_stream_on) || g_input_pipe[1] < 0) return; AHardwareBuffer_acquire(ahb); @@ -2131,7 +2115,7 @@ void droiddeck_stream_frame(AHardwareBuffer *ahb) { AHardwareBuffer *dropped = g_stream_pending; g_stream_pending = ahb; pthread_mutex_unlock(&g_stream_lock); - if (dropped) { AHardwareBuffer_release(dropped); return; } /* a wake-up is already queued */ + if (dropped) { AHardwareBuffer_release(dropped); return; } struct input_msg m = { 20, 0, 0, 0, 0 }; ssize_t n = write(g_input_pipe[1], &m, sizeof(m)); (void)n; @@ -2200,11 +2184,6 @@ static void render_scene(void) { hdr_note_precedence(hdr_route == 3 ? hs.who : NULL, fx_on, framegen); } if (hdr_route == 3) { fx_on = 0; framegen = 0; } - /* A game the compositor cannot import (an AHardwareBuffer-only surface, or HDR frames it - * has no texture for) is shown on its own display layer and is not part of the scene, so - * composing would leave the game out of the picture: the pointer stays out instead, the - * same trade-off as HDR route 3. Both are decided before the pointer is appended, because - * the appended draws change the draw list they are matched against. */ struct surface *ahb_only = ahb_layer_only_candidate(w, h); struct surface *layer_only = ahb_only ? ahb_only : hdr_unimported_fullscreen(&dl, w, h); const int cursor_on = hdr_route == 3 || layer_only ? 0 : add_cursor(&dl, w); @@ -2216,9 +2195,6 @@ static void render_scene(void) { } else if (!layer_only) { cursor_layer_said = 0; } - /* With cardboard the scene is composed anyway (the pass needs the whole picture), so the - * pointer reaches the output without switching routes: the HDR composition path would drop - * the cardboard pass, which is 8-bit only. */ static int cursor_hdr_said; if (cursor_on && hdr_route == 0 && hs.n_hdr > 0 && !cardboard) { hdr_route = 2; @@ -2257,8 +2233,6 @@ static void render_scene(void) { } if (hdr_route == 0 && vkp_quest_layer_presentation() && !framegen && !cardboard && !cursor_on && !ahb_only) { - /* The complete SDR scene stays on one display layer. Do not hide/re-show GAME - * on each desktop frame just because it is not a single fullscreen window. */ rendered = vkp_render(w, h, dl.d, dl.n) == 0; } else if (hdr_route == 0 || hdr_route == 3) { const int pass_on = fx_on || framegen; @@ -2367,8 +2341,6 @@ static int on_fallback_timer(void *data) { return 0; } -/* A new frame is drawn when it arrives instead of at the next screen tick, at most once per refresh, - while frames go straight back to the game (no compositor limit), without streaming or frame generation. */ static int commit_renders_on(void) { return g_render_on_commit && g_release_unpaced && !atomic_load(&g_stream_on) && !vkp_framegen_active(); } @@ -2427,10 +2399,6 @@ static void on_vsync(int64_t frame_time_ns) { if (vkp_effects_sync()) tick_change = 1; if (vkp_cardboard_pose_changed()) tick_change = 1; if (tick_change) g_dirty = 1; - /* A frame that arrived after this refresh was already drawn is left for the next arrival to draw; - drawing it here would block that arrival too and keep every frame waiting for the tick. A client - that draws again only after its frame callback sends no such arrival: once a held frame is still - waiting at the next tick, the tick draws every frame for a second before holding one again. */ if (g_tick_held && g_dirty) g_hold_missed_ns = now; g_tick_held = 0; if (!tick_change && commit_renders_on() && g_dirty_since_ns && now - g_dirty_since_ns < g_refresh_ns && @@ -2665,15 +2633,11 @@ static int pointer_locked_for_cursor(void) { static int cursor_scene_positions(int scene_w, int source, int ptr_x, int hx, int out[2]) { out[0] = ptr_x - hx; out[1] = -1; if (source != CARDBOARD_SOURCE_SBS || scene_w <= 1) return 1; - /* The pointer decides which eye it is in; the hotspot only shifts the image drawn there, - * so a hotspot that crosses the seam cannot put the second copy in the wrong eye. */ const int half = scene_w / 2; out[1] = (ptr_x < half ? ptr_x + half : ptr_x - half) - hx; return 2; } -/* Crops one pointer copy to the eye it belongs to (eye < 0 crops nothing). Returns 0 when no - * part of the copy falls inside that eye, so the caller skips it. */ static int cursor_clip_to_eye(int scene_w, int eye, int x0, int w, int *dx, int *sx, int *dw) { int left = x0, right = x0 + w; if (eye >= 0 && scene_w > 1) { @@ -2689,7 +2653,6 @@ static int cursor_clip_to_eye(int scene_w, int eye, int x0, int w, int *dx, int return 1; } -/* Appends the guest pointer as the topmost draw of the scene. */ static int add_cursor(struct draw_list *dl, int scene_w) { if (!atomic_load(&g_cursor_composite) || !g_ptr_known || pointer_locked_for_cursor()) return 0; @@ -2740,9 +2703,6 @@ static int add_cursor(struct draw_list *dl, int scene_w) { if (!d) return 0; dl->d = d; dl->cap = cap; } - /* The cardboard pass samples each half of the scene as one eye, so a copy that crosses the - * seam would show up as a fragment at the opposite edge of the other eye: crop each copy to - * the eye it belongs to and drop it when nothing of it lands there. */ const int sbs = source == CARDBOARD_SOURCE_SBS && scene_w > 1; const int pointer_eye = ptr_x < scene_w / 2 ? 0 : 1; int appended = 0; @@ -2765,8 +2725,6 @@ static void sync_lock_notify(void) { if (locked == last) return; last = locked; droiddeck_on_pointer_lock(locked, (int)g_ptr_x, (int)g_ptr_y); - /* The drawn pointer follows the lock state, so a game that stops presenting after it grabs - * the pointer would keep the last composited cursor on screen without a redraw here. */ if (atomic_load(&g_cursor_composite)) schedule_render(); } @@ -2849,8 +2807,6 @@ static void constraint_activate(struct constraint *k) { g_active_constraint = k; const double was_x = g_ptr_x, was_y = g_ptr_y; if (!k->is_lock) confine_clamp(k, &g_ptr_x, &g_ptr_y); - /* Confining can move the pointer while the lock state stays the same, so sync_lock_notify() - * below stays quiet; the drawn pointer follows the position, so repaint here. */ if ((g_ptr_x != was_x || g_ptr_y != was_y) && atomic_load(&g_cursor_composite)) schedule_render(); constraint_origin(k, &ox, &oy); @@ -2935,8 +2891,6 @@ static void constraints_surface_commit(struct surface *s) { if (k->active && !k->is_lock) confine_clamp(k, &g_ptr_x, &g_ptr_y); } } - /* A position hint or a newly committed confine region can move the pointer while the lock - * state stays the same, so nothing else would repaint the composited cursor. */ if ((g_ptr_x != was_x || g_ptr_y != was_y) && atomic_load(&g_cursor_composite)) schedule_render(); } @@ -3147,10 +3101,6 @@ static void pointer_input(double x, double y, int relative, uint32_t button, int uint32_t t = now_ms(); pointer_focus(target->resource, fx, fy); - // A button-only input must not replay a cached absolute position. A game - // may have warped its cursor since the last motion (before taking lock, - // or while releasing it), so that replay can become an unintended pan. - // Absolute touch events still carry their position, including on clicks. int rel_consumed = send_relative_motion(client, dx, dy); if (!(k && k->is_lock) && !(relative && rel_consumed) && (!relative || dx != 0 || dy != 0)) for_each_pointer_of(client, sp) wl_pointer_send_motion(sp->ptr, t, fx, fy); @@ -3182,9 +3132,6 @@ static void pointer_button(uint32_t button, int pressed) { pointer_input(0, 0, 1, button, pressed); return; } - // Releases also clean up interrupted gestures. Deliver them to the existing - // pointer focus without motion or hit-testing: native touch has independent - // coordinates, so replaying g_ptr_x/y here can warp the game cursor. uint32_t t = now_ms(); for (int i = 0; i < g_nptrs; i++) { struct seat_pointer *sp = &g_ptrs[i]; @@ -3679,8 +3626,6 @@ void droiddeck_wayland_send_scene_input(int type, int a, int b) { (void)n; } -/* Any thread: draw the pointer into the compositor's own output (the glasses - * surface owns the picture, so the phone's Compose overlay never reaches it). */ void droiddeck_wayland_set_cursor_composite(int on) { const int want = on ? 1 : 0; if (atomic_exchange(&g_cursor_composite_want, want) == want) return; @@ -3871,8 +3816,6 @@ int droiddeck_wayland_run(void) { open_session_log(); if (vkp_ready() != 0) WLOGE("renderer unavailable; clients will connect but nothing is drawn"); - /* Presentation compatibility is not HDR capability. Set the initial layer mode before - * clients bind, including when Desktop is the first session in this app process. */ if (vkp_quest_layer_presentation() && sc_layer_available()) g_zero_copy = 1; struct wl_display *display = wl_display_create(); diff --git a/app/src/main/cpp/waylandcomp/src/framegen_bridge.c b/app/src/main/cpp/waylandcomp/src/framegen_bridge.c index 9de2824d7..eceb79639 100644 --- a/app/src/main/cpp/waylandcomp/src/framegen_bridge.c +++ b/app/src/main/cpp/waylandcomp/src/framegen_bridge.c @@ -374,16 +374,11 @@ int vkp_framegen_run(VkCommandBuffer cmd, VkImage scene, VkImageView scene_view, mark_failed(kind); return 0; } - /* The ring and the engine's images are new on this frame. Dispatching into them now - * null-dereferenced Turnip (vkp_framegen_run, first Steam frame, 2560x1440 to the panel). - * Show this frame as it is; generation starts once the size has settled. */ if (size_changed || ring == 2) { FGLOG("frame generation catches up at %dx%d; this frame is shown as it is", w, h); return 0; } - /* The pacer's source rate must be the game's, not this loop's: a loop slowed by its own - * generations would otherwise read as a slower game and justify even more generations. */ ++g_source_frames; const uint64_t game_frames = droiddeck_game_frames(); uint32_t n_gen = fge_plan(g_ring_n, game_frames ? game_frames : g_source_frames, g_presented_rate); diff --git a/app/src/main/cpp/waylandcomp/src/gpu_clock_pin.c b/app/src/main/cpp/waylandcomp/src/gpu_clock_pin.c index 0f8e1f61b..8ce226922 100644 --- a/app/src/main/cpp/waylandcomp/src/gpu_clock_pin.c +++ b/app/src/main/cpp/waylandcomp/src/gpu_clock_pin.c @@ -103,7 +103,6 @@ static bool prepare_pin(void) { (off_t)pin.memory << 12); if (commands == MAP_FAILED) return false; pin.commands = commands; - // A3xx/A4xx use type-3 CP_NOP; newer cores use type-7 with zero odd-parity bits. uint32_t *words = commands; words[0] = pin.nop; words[1] = 0; @@ -124,7 +123,6 @@ static void *hold_pin(void *unused) { if (errno == EINTR || errno == ETIMEDOUT) continue; break; } - // Constraints expire after idle_timer (normally 80 ms); renew with a NOP, keeping devfreq alive. usleep(25000); if (atomic_load(&running) && !submit_pin()) break; } diff --git a/app/src/main/cpp/waylandcomp/src/headpose_shm.h b/app/src/main/cpp/waylandcomp/src/headpose_shm.h index 824232b43..22088dc6b 100644 --- a/app/src/main/cpp/waylandcomp/src/headpose_shm.h +++ b/app/src/main/cpp/waylandcomp/src/headpose_shm.h @@ -24,7 +24,6 @@ struct droiddeck_headpose { float eye_aspect; uint32_t eye_w, eye_h; float display_hz; - // Uses a formerly reserved word; older publishers leave this zero. uint32_t render_height; uint32_t reserved[10]; }; diff --git a/app/src/main/cpp/waylandcomp/src/hudframes.c b/app/src/main/cpp/waylandcomp/src/hudframes.c index 5a6fc4e99..ffbb657a5 100644 --- a/app/src/main/cpp/waylandcomp/src/hudframes.c +++ b/app/src/main/cpp/waylandcomp/src/hudframes.c @@ -8,7 +8,6 @@ #include "hudframes.h" -/* Layout written by gamescope patch 0125 (tools/gamescope/patches). */ #define HUD_FRAMES_MAGIC 0x46484444u #define HUD_FRAMES_HEADER 128u #define HUD_FRAMES_ENTRY 16u @@ -69,7 +68,6 @@ int hud_frames_poll(struct hud_frames *h, int64_t *frametimes, int64_t *times, i memcpy(&frametimes[n], e, 8); memcpy(×[n], e + 8, 8); } - /* The writer may have lapped the ring while we copied: drop what it overwrote. */ uint64_t after = load64(h->map + 16); uint64_t valid = after > cap ? after - cap : 0; if (from < valid) { diff --git a/app/src/main/cpp/waylandcomp/src/hudframes.h b/app/src/main/cpp/waylandcomp/src/hudframes.h index d0b1ff5bd..518f7be52 100644 --- a/app/src/main/cpp/waylandcomp/src/hudframes.h +++ b/app/src/main/cpp/waylandcomp/src/hudframes.h @@ -8,9 +8,7 @@ struct hud_frames; struct hud_frames *hud_frames_open(const char *path); void hud_frames_close(struct hud_frames *h); -/* New frames since the last poll, oldest first; -1 while the file holds no valid ring. */ int hud_frames_poll(struct hud_frames *h, int64_t *frametimes, int64_t *times, int max); -/* info: pid, output width, output height, refresh mHz, Steam focused. */ int hud_frames_info(const struct hud_frames *h, int32_t *info, char *engine, size_t engine_len); #endif diff --git a/app/src/main/cpp/waylandcomp/src/quest_device.h b/app/src/main/cpp/waylandcomp/src/quest_device.h index ee9c37b10..bcdcee239 100644 --- a/app/src/main/cpp/waylandcomp/src/quest_device.h +++ b/app/src/main/cpp/waylandcomp/src/quest_device.h @@ -3,7 +3,6 @@ #include #include -/* Presentation only; this does not select a GPU driver or expand driver support. */ static inline int quest_device_matches(const char *model, const char *device, const char *maker) { return !strcasecmp(model, "Quest 2") || !strcasecmp(model, "Quest 3") || !strcasecmp(model, "Quest 3S") || diff --git a/app/src/main/cpp/waylandcomp/src/sc_layer.c b/app/src/main/cpp/waylandcomp/src/sc_layer.c index f9d203ba1..ff90d37dc 100644 --- a/app/src/main/cpp/waylandcomp/src/sc_layer.c +++ b/app/src/main/cpp/waylandcomp/src/sc_layer.c @@ -291,7 +291,7 @@ int sc_layer_base_black(int width, int height) { ARect src = {0, 0, 16, 16}, dst = {0, 0, width, height}; api.setBuffer(tx, g_base_sc, black, -1); api.setBufferTransparency(tx, g_base_sc, ASC_TRANSPARENCY_OPAQUE); - api.setZOrder(tx, g_base_sc, 0); /* GAME = 1, OVERLAY = 2 */ + api.setZOrder(tx, g_base_sc, 0); api.setGeometry(tx, g_base_sc, &src, &dst, 0); api.setVisibility(tx, g_base_sc, ASC_VISIBILITY_SHOW); api.txApply(tx); api.txDelete(tx); @@ -973,7 +973,7 @@ void sc_layer_hide_overlay(void) { } void sc_layer_window_gone(void) { - base_hide(1); /* the backdrop belongs to the old window, just like GAME and OVERLAY */ + base_hide(1); if (api.state != 1) return; layers_init(); for (int i = SC_LAYER_COUNT - 1; i >= 0; i--) retire_sc(&g_layers[i]); diff --git a/app/src/main/cpp/waylandcomp/src/vk_present.c b/app/src/main/cpp/waylandcomp/src/vk_present.c index ffdc5cb1d..df1c0d817 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_present.c +++ b/app/src/main/cpp/waylandcomp/src/vk_present.c @@ -71,8 +71,6 @@ static int g_first_frame_done; static int g_base_black; int vkp_quest_layer_presentation(void) { - /* Compositor thread only; this is a process-wide device property, not a session setting. - * Quest 2 still needs its separate A6xx driver: this gate only chooses presentation. */ static int enabled = -1; if (enabled < 0) { char model[PROP_VALUE_MAX] = {0}, device[PROP_VALUE_MAX] = {0}, maker[PROP_VALUE_MAX] = {0}; @@ -477,7 +475,6 @@ static int dev_init(void) { g_vk.EnumerateDeviceExtensionProperties(g_pd, NULL, &ne, NULL); VkExtensionProperties *exts = calloc(ne ? ne : 1, sizeof(*exts)); g_vk.EnumerateDeviceExtensionProperties(g_pd, NULL, &ne, exts); - /* Only what the driver has: the system driver lacks the dma-buf ones, which the cloud player never needs. */ for (unsigned i = 0; i < 4; i++) { if (has_ext(exts, ne, import_exts[i])) dev_exts[n_dev_exts++] = import_exts[i]; else LOGE("present: driver MISSING %s (dmabuf import will fail)", import_exts[i]); @@ -489,7 +486,6 @@ static int dev_init(void) { } 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; - /* The cloud player's decoded frames arrive as AHardwareBuffers (vkp_image_from_ahb). */ g_ahb_import = has_ext(exts, ne, VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME) && has_ext(exts, ne, VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME); if (g_ahb_import) { @@ -1701,9 +1697,6 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i 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); - /* Frame generation samples every output pixel. Upscaling first made it interpolate - * the whole panel (3200x1440 on a 1440p phone) and rebuild on Steam's first frame. - * Grade at the game's size instead; the screen blit still scales to the panel. */ if (fg) { mapped_w = scene_w; mapped_h = scene_h; } 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); @@ -1803,9 +1796,6 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i int vkp_render(int scene_w, int scene_h, const struct vkp_draw *draws, int n) { if (vkp_quest_layer_presentation() && !vkp_framegen_active() && !vkp_cardboard_active()) { - /* The desktop (multiple windows), overlays and an empty scene must also avoid WSI. - * Merely enabling zero-copy handles only a single fullscreen window and leaves both - * the letterbox and every fallback frame going through the corrupted swapchain. */ vkp_apply_window_request(); sc_layer_hide_overlay(); if (n > 0) { @@ -1817,9 +1807,6 @@ int vkp_render(int scene_w, int scene_h, const struct vkp_draw *draws, int n) { } else { sc_layer_hide(); } - /* sc_layer_present_pass signals only after submitting client content. - * A backdrop alone, or a successful no-op while a layer slot is unavailable, - * must not dismiss the app's loading UI or mark Steam's window as shown. */ return vkp_base_black(scene_w, scene_h); } return render_impl(scene_w, scene_h, draws, n, NULL, NULL); @@ -1856,8 +1843,6 @@ 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; if (vkp_quest_layer_presentation()) { - /* Layer geometry needs the window size, not a Vulkan swapchain. In particular, do - * not create/present a swapchain just to paint black under an otherwise clean layer. */ int w = ANativeWindow_getWidth(g_window), h = ANativeWindow_getHeight(g_window); if (w <= 0 || h <= 0) return -1; g_extent = (VkExtent2D){(uint32_t)w, (uint32_t)h}; diff --git a/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c b/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c index adc2372de..5d69098a6 100644 --- a/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c +++ b/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c @@ -698,7 +698,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetGpuTurbo(JNIEnv * jclass error = (*env)->FindClass(env, "java/lang/IllegalStateException"); if (error) (*env)->ThrowNew(env, error, strerror(saved_errno)); } - // Clear a legacy global pin when recovering its marker after an app upgrade or crash. if (on != JNI_TRUE) adrenotools_set_turbo(false); } @@ -725,7 +724,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCardboardSpatialP vkp_cardboard_set_spatial_pose(m, p, live ? 1 : 0); } -/* The Xbox Cloud Gaming native player: its decoded frames are the whole scene (no Wayland clients). */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStreamMode(JNIEnv *env, jclass clazz, jboolean on) { droiddeck_stream_set(on ? 1 : 0); diff --git a/app/src/main/java/com/droiddeck/launcher/App.kt b/app/src/main/java/com/droiddeck/launcher/App.kt index de94c41a5..01657d289 100644 --- a/app/src/main/java/com/droiddeck/launcher/App.kt +++ b/app/src/main/java/com/droiddeck/launcher/App.kt @@ -19,7 +19,6 @@ class App : Application() { com.droiddeck.launcher.ui.Themes.loadGradient(this) com.droiddeck.launcher.ui.BuildWatermark.install(this) HomeApp.trackRecents(this) - // Helper processes must not initialize the main USB provider or guest graphics context. val process = if (android.os.Build.VERSION.SDK_INT >= 28) Application.getProcessName() else (getSystemService(ACTIVITY_SERVICE) as android.app.ActivityManager) .runningAppProcesses?.firstOrNull { it.pid == android.os.Process.myPid() }?.processName @@ -32,7 +31,6 @@ class App : Application() { return } if (process == "$packageName:xcloudnative") { - // The native cloud player: the stores log and nothing that touches the Linux session. com.droiddeck.launcher.stores.StoresState.init(this) return } @@ -62,7 +60,6 @@ class App : Application() { com.droiddeck.launcher.stores.StoresState.register(com.droiddeck.launcher.stores.battlenet.BattleNetBackend) } - /** Fetches the packs an existing setup already relies on, so the first use after an update does not wait. */ private fun prefetchAddonPacks() { val app = this Thread({ diff --git a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt index ec4c069f4..5fb8eea12 100644 --- a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt @@ -717,7 +717,6 @@ class FrontEndActivity : ComponentActivity() { LauncherIcon.set(this, style) launcherIcon = LauncherIcon.current(this) if (launcherIcon == before) return false - // Android finishes activities started through the alias that was just disabled. startActivity(Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER) .setComponent(LauncherIcon.activity(this)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) @@ -1797,7 +1796,6 @@ class FrontEndActivity : ComponentActivity() { private fun refreshSecondScreenDisplays() { if (!::displayManager.isInitialized) return - // The launcher can itself be shown on a non-default display (DeX, an external monitor); that one is not a "second screen". val ownDisplayId = if (android.os.Build.VERSION.SDK_INT >= 30) display?.displayId ?: android.view.Display.DEFAULT_DISPLAY else @Suppress("DEPRECATION") windowManager.defaultDisplay.displayId dualScreenMode = SessionPrefs.dualScreenMode(this) secondScreenDisplays = SecondScreenDisplays.available(displayManager, ownDisplayId) @@ -2491,7 +2489,6 @@ class FrontEndActivity : ComponentActivity() { !SessionPaths.currentSessionLogsRetained(this) private fun refresh() { - // Until the scan finishes, the other client's games would launch in this one. val listX64 = SessionPrefs.activeSteamClientArch(this) == "x86_64" if (listX64 != steamGamesX64) { steamGamesX64 = listX64 diff --git a/app/src/main/java/com/droiddeck/launcher/HomePins.kt b/app/src/main/java/com/droiddeck/launcher/HomePins.kt index 1040995e8..38acb1813 100644 --- a/app/src/main/java/com/droiddeck/launcher/HomePins.kt +++ b/app/src/main/java/com/droiddeck/launcher/HomePins.kt @@ -12,7 +12,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -/** Shortcuts other apps asked to put on the home screen. The system keeps the pinned set per launcher. */ object HomePins { class Pinned(val info: ShortcutInfo, val icon: Bitmap?, val appLabel: String, val userSerial: Long) { val key get() = "pin:${info.`package`}/${info.id}" + if (userSerial != 0L) "#$userSerial" else "" diff --git a/app/src/main/java/com/droiddeck/launcher/HomeWidgets.kt b/app/src/main/java/com/droiddeck/launcher/HomeWidgets.kt index 8f79f4354..886f5f0e8 100644 --- a/app/src/main/java/com/droiddeck/launcher/HomeWidgets.kt +++ b/app/src/main/java/com/droiddeck/launcher/HomeWidgets.kt @@ -197,7 +197,6 @@ object HomeWidgets { if (pendingId == AppWidgetManager.INVALID_APPWIDGET_ID) add(activity, provider.info, null) } - /** Places a widget another app asked for; its own app has already configured it. */ fun addPinned(activity: Activity, request: LauncherApps.PinItemRequest): Boolean { if (pendingId != AppWidgetManager.INVALID_APPWIDGET_ID) return false val info = request.getAppWidgetProviderInfo(activity) ?: return false diff --git a/app/src/main/java/com/droiddeck/launcher/ProtonMenu.kt b/app/src/main/java/com/droiddeck/launcher/ProtonMenu.kt index 8e8efc80c..5251b50da 100644 --- a/app/src/main/java/com/droiddeck/launcher/ProtonMenu.kt +++ b/app/src/main/java/com/droiddeck/launcher/ProtonMenu.kt @@ -20,7 +20,6 @@ internal class ProtonMenu( private val ui: Handler, private val idle: () -> Boolean = { true }, ) { - // Install workers and their results outlive the activity that starts them. private object Operation { val rows = mutableStateOf>(emptyList()) val busyId = mutableStateOf(null) diff --git a/app/src/main/java/com/droiddeck/launcher/QuestXrActivity.kt b/app/src/main/java/com/droiddeck/launcher/QuestXrActivity.kt index 28270e195..98e5a0b22 100644 --- a/app/src/main/java/com/droiddeck/launcher/QuestXrActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/QuestXrActivity.kt @@ -71,7 +71,6 @@ class QuestXrActivity : NativeActivity() { } } finally { synchronized(steamVrLock) { - // A retiring activity must not stop a replacement's SteamVR session. if (steamVrOwner === this) { if (sessionAlive()) SteamVr.run(this, "stop") steamVrOwner = null diff --git a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt index 3e8809134..d3b018dc0 100644 --- a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt @@ -265,7 +265,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (locked) openEvdevMouse() else closeEvdevMouse() padMouseCheckedAt = 0L if (::padMouse.isInitialized) padMouseMode() - // Touchpad deltas are measured from the middle while locked (moveLockedPointer). if (locked && ::touchpad.isInitialized) { drawnRect()?.let { touchpad.place(it.centerX(), it.centerY()) } cursorVisible = false @@ -513,8 +512,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } } - // A higher refresh rate allowed in Android's settings does not change the display while this window - // still asks for a lower mode, so the mode is chosen again when the setting changes. private val refreshSettingObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { override fun onChange(selfChange: Boolean) { applyOutputPacing() @@ -571,7 +568,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { finish() return } - // A cancelled install keeps its thread until the download stops; a new launch would reuse its stop flag and close itself. if (savedInstanceState == null && SessionState.installing != null && !SessionState.running) { runtimeRemovalBlocked = true android.widget.Toast.makeText(this, R.string.main_wait_desktop_install, android.widget.Toast.LENGTH_SHORT).show() @@ -582,7 +578,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { val mode = intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM SessionState.programArgs = intent.getStringArrayExtra(SessionService.EXTRA_PROGRAM_ARGS)?.toList().orEmpty() SessionEvents.begin(this, mode, SessionPaths.label(mode, intent.getStringExtra(SessionService.EXTRA_PROGRAM))) - // begin() moves the phase on, so the block below never runs for a new session; reset per-session flags here. SessionState.secondScreenAutoOpened = false SessionState.secondScreenProvisional = false } @@ -718,8 +713,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { ViewConfiguration.get(this).scaledTouchSlop.toFloat(), gestureOutput, ) refreshGestureConfig() - // A real touch target lets FrameLayout split fingers between the - // game surface and on-screen buttons, in either finger-down order. root.isMotionEventSplittingEnabled = true surfaceView.setOnTouchListener { _, event -> if (!event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) { @@ -925,7 +918,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { window.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(android.graphics.Color.BLACK)) }, ) - // Also runs when a second screen appears, so Automatic applies then with the running game known. LaunchedEffect(AgentGuest.focusRevision, secondScreenDisplays.isNotEmpty()) { val nextGame = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { if (SessionState.mode == SessionService.MODE_RUN) SessionState.program?.let { "program:$it" } @@ -1119,7 +1111,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { SessionAchievements(inFront.game, inFront.title, onClose = ::closeAchievements) } com.droiddeck.launcher.ui.AchievementNotificationOverlay() - // Auto and Native resume on their own as the session comes back; only a failed resume needs the button. val autoResume = suspendPolicy == SessionPrefs.SUSPEND_AUTO || suspendPolicy == SessionPrefs.SUSPEND_NATIVE LaunchedEffect(SessionState.manualPause, SessionState.suspended) { if (!SessionState.manualPause && !SessionState.suspended) pauseAnimated = false @@ -1480,7 +1471,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { )) } - /** The SteamVR driver switches pad sticks between hands and game on both stick clicks together. */ private fun switchVrSticks() { val chord = { down: Boolean -> padBridge?.applyTouch { state -> state.press(com.droiddeck.launcher.input.PadState.L3, down) @@ -2215,8 +2205,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } override fun surfaceDestroyed(holder: SurfaceHolder) { - // Hiding the phone surface after a handoff delivers a late destruction - // callback. It must not reset the new output's size or attachment. if (holder !== outputHolder()) return cancelTouchGestures() surfaceW = 0 @@ -2637,10 +2625,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (PadBridge.isControllerMotionEvent(event) && padMouseMode() && padMouse.onMotion(event)) return true if (padBridge?.onMotionEvent(event) == true) return true if (event.isFromSource(android.view.InputDevice.SOURCE_MOUSE) && !drawerOpen) { - /* The app's own controls get the pointer first. With the picture on the glasses the - * phone shows its panel, and hovering or clicking a control there must reach it - * instead of moving and clicking the guest pointer on the glasses. Whatever the UI - * leaves alone — the rest of the screen — still drives the guest pointer. */ if (appUiOwnsPointer()) { if (super.dispatchGenericMotionEvent(event)) return true if (onMouse(event)) return true @@ -2654,7 +2638,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { override fun dispatchTrackballEvent(event: MotionEvent): Boolean = onCapturedPointer(event) || super.dispatchTrackballEvent(event) - /** Whether one of the on-screen control layers takes a finger going down at this screen point. */ private fun controlTakesTouchAt(rawX: Float, rawY: Float): Boolean { val at = IntArray(2) fun takes(view: View, hit: (Float, Float) -> Boolean): Boolean { @@ -2832,9 +2815,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { override fun prepareRelativePan() { if (pointerLocked) return val rect = drawnRect() ?: return - // Gamescope retains the last absolute position while a game can warp - // its own cursor to the center. Its first unlocked motion must start - // from that same origin, before the game switches to pointer lock. moveTo(rect.centerX(), rect.centerY()) } override fun key(code: Int, down: Boolean) { @@ -3156,7 +3136,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { }), ) - /** Pages for the bottom-screen dashboard; the dashboard is offered only while at least one exists. */ private fun bottomPages(): List = sessionBottomPages(BottomPageActions( takeScreenshot = ::captureScreenshot, fps = { if (::hud.isInitialized) hud.liveFps else null }, @@ -3164,9 +3143,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { runningGame = { gestureGame }, )) - // A settings page opened on the bottom screen sits under the dashboard window, so the dashboard hides - // until the setting is on, or for at most SETTINGS_WATCH_CHECKS seconds when it is left off (another - // app's page closing cannot be seen); choosing Dashboard in the menu again shows it sooner. private fun openSettingsBelow(intent: android.content.Intent, done: () -> Boolean) { val presentation = secondScreenPresentation ?: return val options = android.app.ActivityOptions.makeBasic().setLaunchDisplayId(presentation.display.displayId) @@ -3178,7 +3154,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (!opened) return presentation.hide() var checks = 0 - // Show throws if the display went away while the settings page was open, as in showSecondScreen. fun showAgain() { try { presentation.show() @@ -3199,9 +3174,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { }, SETTINGS_WATCH_MS) } - /** Copies the game frame (no overlay) at the size it is shown and saves it to Pictures/DroidDeck. */ private fun captureScreenshot(onDone: (Boolean) -> Unit) { - // With the glasses as the output the game is drawn on their surface; the phone's surface is hidden then. val source = if (glassesOutputActive) glassesScreen?.surfaceView else if (::surfaceView.isInitialized) surfaceView else null val width = source?.width ?: 0 val height = source?.height ?: 0 @@ -3643,23 +3616,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (want) view.requestPointerCapture() else view.releasePointerCapture() } - /** Null until the first application, so the initial state is applied rather than skipped. */ private var platformPointerShown: Boolean? = null - /** - * The app's own UI is what the mouse is over, so it needs a pointer of its own: the drawer, - * keyboard, achievements and PiP controls, and — once the glasses own the picture — the whole - * phone, which then shows [GlassesPhonePanel] instead of the game. - */ private fun appUiOwnsPointer(): Boolean = drawerOpen || pcKeyboardOpen || achievementsOpen || pipUi || glassesOutputActive - /** - * Over the game neither this app nor Android draws a pointer: the compositor's cursor is the - * only one (Compose's on the phone, inside the picture on the glasses). Over the app's own UI - * the game gets no pointer events at all, so without this nothing visible follows the mouse - * while buttons still highlight and click. - */ private fun applyPlatformPointer(shown: Boolean) { if (!::sessionRoot.isInitialized || !::surfaceView.isInitialized) return if (shown == platformPointerShown) return @@ -3880,7 +3841,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (down || code !in gestureKeysDown) WaylandCompositor.nativeSendKey(code, if (down) 1 else 0) } - // The frozen game cannot draw again, so the paused overlay shows a copy taken while the surface still holds its frame. private fun capturePausedFrame(manual: Boolean = false) { if (!SessionState.running || pipUi || isInPictureInPictureMode || glassesOutputActive) return if (SessionState.suspended && pausedFrame != null) return @@ -3925,8 +3885,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { SessionService.pause(this) } - // Sessions that froze from picture-in-picture, or before onPause could copy a frame, have none; the - // compositor shows the frozen frame again once its output is back, so copy it from there. private fun requestPausedFrame() { if (pausedFrame != null) return pausedFrameWanted = true @@ -4163,8 +4121,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { Glasses.cancelSteamVrStart() } if (glasses.present) { - // Optional stereo starts only after RUNNING is observed. READY can - // arrive repeatedly while the launch worker is still starting VR. if (action == "install") steamVrStatus = SteamVr.Status.INSTALLING runSteamVrAction(action, ::currentRequest) { val ok = SteamVr.run(this, action) @@ -4357,8 +4313,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (holder !== outputHolder()) return glassesOutputActive = glassesScreen?.holder === holder if (CompositorHost.isStarted) WaylandCompositor.nativeSetCursorComposite(glassesOutputActive) - // Keep the phone's layout for touch/controller coordinates, but remove - // its SurfaceView so it cannot retain the last game frame under the hint. surfaceView.visibility = if (glassesOutputActive) View.INVISIBLE else View.VISIBLE Log.i(TAG, "compositor output attached: " + if (glassesOutputActive) "glasses display ${glassesScreen?.display?.displayId}" else "phone") @@ -4437,8 +4391,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } if (target != null) { val screen = GlassesScreen(this, target, Glasses.outputHz(), this, Glasses.outputResolution()) - // show() can create the surface synchronously. Select its holder - // first, otherwise surfaceCreated() rejects the glasses callback. glassesScreen = screen applyOutputPacing() surfaceW = 0 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 f8f1b2a3e..8d9ddcb1f 100644 --- a/app/src/main/java/com/droiddeck/launcher/audio/DirectAudioRelayComponent.java +++ b/app/src/main/java/com/droiddeck/launcher/audio/DirectAudioRelayComponent.java @@ -118,7 +118,6 @@ public class DirectAudioRelayComponent extends SessionPart { } } - // Android keeps the input stream's wake lock while a frozen helper still has it started. public boolean awaitMicReleased(long timeoutMs) { long deadline = System.currentTimeMillis() + timeoutMs; synchronized (micLock) { diff --git a/app/src/main/java/com/droiddeck/launcher/core/AddonPack.kt b/app/src/main/java/com/droiddeck/launcher/core/AddonPack.kt index cc6a1007a..383dd98bb 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/AddonPack.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/AddonPack.kt @@ -13,12 +13,6 @@ import kotlinx.coroutines.launch import java.io.File import java.io.FileInputStream -/** - * A pack built by CI alongside this APK (tools/packs). Main and release builds publish it to - * DroidDeck-Components under a content-addressed tag; a PR test build attaches packs that are not there - * yet to its pr-N release on DroidDeck-CI. The APK pins the pack's SHA-256, so only the bytes this build - * was made with install, whichever host serves them. - */ internal class AddonPack( val name: String, private val sha256: String, val size: Long, private val required: List, private val reuseDeviceCopy: Boolean = sha256.isEmpty() && !BuildConfig.CI_BUILD, @@ -43,13 +37,11 @@ internal class AddonPack( ) private fun root(context: Context) = File(context.filesDir, "addons/$name") - /** App-private and written only after the archive matched the pinned digest. */ fun dir(context: Context) = File(root(context), version(context)) fun file(context: Context, relative: String) = File(dir(context), relative) fun installed(context: Context): Boolean = version(context).isNotEmpty() && File(dir(context), COMPLETE).isFile - // A build made outside CI pins no packs, so it runs the newest one a CI build already verified and installed. private fun version(context: Context): String = if (!reuseDeviceCopy) sha256 else deviceCopy(context).orEmpty() private fun deviceCopy(context: Context): String? = root(context).listFiles().orEmpty() @@ -66,7 +58,6 @@ internal class AddonPack( .also { mutableState.value = it } } - /** Starts the download in the background unless the pack is already present or on its way. */ fun ensure(context: Context, onReady: (Context) -> Unit = {}) { val app = context.applicationContext synchronized(lock) { @@ -81,7 +72,6 @@ internal class AddonPack( } } - /** Blocks a worker thread until the pack is installed, starting the download if needed. */ @JvmOverloads fun await(context: Context, running: () -> Boolean = { true }, onState: (State) -> Unit = {}): Boolean { if (installed(context)) return true @@ -127,7 +117,6 @@ internal class AddonPack( return true } - // A local build pins the packs it compiled, which no release hosts, so it ships them as assets. internal fun bundled(context: Context, archive: File): Boolean { val copied = runCatching { bundle(context).use { input -> archive.outputStream().use { input.copyTo(it) } } 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 4163eb4a3..3a40300dc 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt @@ -42,7 +42,6 @@ object DeviceReport { } } - /** Name of an ApplicationExitInfo reason code, so a report says "LOW_MEMORY" instead of "3". */ internal fun exitReasonName(reason: Int): String = when (reason) { 0 -> "UNKNOWN" 1 -> "EXIT_SELF" diff --git a/app/src/main/java/com/droiddeck/launcher/core/DownloadSource.kt b/app/src/main/java/com/droiddeck/launcher/core/DownloadSource.kt index b11e3113a..d71f45120 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/DownloadSource.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/DownloadSource.kt @@ -126,11 +126,9 @@ object DownloadSource { fun isGithub(url: String): Boolean = GITHUB_HOSTS.any { url.startsWith("https://$it/") } - /** Catalogs carry the digests everything else is checked against, so a mirror must never supply one. */ fun isCatalog(url: String): Boolean = CATALOGS.any { url.startsWith(it) } fun candidates(target: String): List { - // GitHub API metadata carries executable URLs and digests; never proxy it. if (isCatalog(target) || target.startsWith("https://api.github.com/")) return listOf(target) val prefs = prefs() ?: return listOf(target) if (!isGithub(target)) return listOf(target) @@ -184,7 +182,6 @@ object DownloadSource { prefs(context)?.edit()?.putString(KEY_PROXY, normalizeProxy(value))?.apply() } - /** An HTTP proxy every Downloader connection uses, or null when none is configured. */ fun httpProxy(): Proxy? = proxyOf(prefs()?.getString(KEY_PROXY, "") ?: "") internal fun proxyOf(value: String): Proxy? { 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 31e8f45f5..19cec5acb 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/Downloader.java +++ b/app/src/main/java/com/droiddeck/launcher/core/Downloader.java @@ -91,7 +91,6 @@ public final class Downloader { if (isCancelled(cancelled)) return null; if (i > 0) Log.i(TAG, "retrying " + url + " via " + sources.get(i)); Fetch fetch = fetch(sources.get(i), cancelled); - // A cancelled fetch says nothing about the source, so it must not count as a failure. if (isCancelled(cancelled)) return null; if (fetch.body != null) { DownloadSource.INSTANCE.succeeded(sources.get(i)); @@ -103,7 +102,6 @@ public final class Downloader { return null; } - /** Fetches without the mirror list, for a URL whose body is a digest the payload is checked against. */ public static String downloadDirect(String url) { return fetch(url).body; } @@ -113,13 +111,11 @@ public final class Downloader { return downloadFile(url, destination, resume, null, progress); } - /** Verify each candidate before accepting it, so a bad mirror can fall back. */ public static boolean downloadFile(String url, File destination, boolean resume, String expectedSha256, java.util.function.Consumer progress) { return downloadFile(url, destination, resume, expectedSha256, null, progress); } - /** {@code cancelled} is polled while bytes arrive; a cancel stops at once and leaves the partial file for the caller. */ public static boolean downloadFile(String url, File destination, boolean resume, String expectedSha256, java.util.function.BooleanSupplier cancelled, java.util.function.Consumer progress) { @@ -128,13 +124,11 @@ public final class Downloader { return downloadCandidates(url, sources(url, expectedSha256), destination, resume, expectedSha256, cancelled, progress); } - /** A third-party mirror may only serve bytes that can be checked against a digest from a direct source. */ static List sources(String url, String expectedSha256) { return expectedSha256 == null ? java.util.Collections.singletonList(url) : DownloadSource.INSTANCE.candidates(url); } - // Package-private for the local multi-source HTTP test. static boolean downloadCandidates(String original, List sources, File destination, boolean resume, String expectedSha256, java.util.function.Consumer progress) { return downloadCandidates(original, sources, destination, resume, expectedSha256, null, progress); @@ -149,7 +143,6 @@ public final class Downloader { if (isCancelled(cancelled)) return false; if (i > 0) Log.i(TAG, "retrying " + original + " via " + source); Outcome outcome = attempt(source, destination, resume, identity, expectedSha256, cancelled, progress); - // A cancel is not a source failure: no other mirror, and no DownloadSource.failed(). if (outcome == Outcome.CANCELLED) return false; if (outcome == Outcome.DONE) { if (expectedSha256 != null) { @@ -192,7 +185,6 @@ public final class Downloader { return value != null && !value.isEmpty() && !value.startsWith("W/"); } - /** Only reuse saved bytes when we persisted a strong validator and their source. */ private static Identity loadIdentity(File dest, boolean resume, String expectedSha256) { Identity identity = new Identity(); File marker = resumeFile(dest); @@ -213,7 +205,6 @@ public final class Downloader { return identity; } } catch (IOException e) { - // Legacy partials or interrupted metadata writes have no trustworthy identity. } if (!discardPartial(dest, identity)) Log.w(TAG, "could not discard unidentified partial " + dest); return identity; @@ -239,7 +230,7 @@ public final class Downloader { try (FileOutputStream out = new FileOutputStream(marker)) { p.store(out, "DroidDeck partial download validator"); } catch (IOException e) { - marker.delete(); // No valid metadata means no future resume. + marker.delete(); Log.w(TAG, "cannot persist download validator", e); } } @@ -252,7 +243,6 @@ public final class Downloader { long have = resume && destination.isFile() ? destination.length() : 0; try { if (have > 0 && (!url.equals(identity.source) || !strongETag(identity.etag))) { - // A new source or unvalidated bytes must restart from byte zero. if (!discardPartial(destination, identity)) return Outcome.RETRY; have = 0; } @@ -286,7 +276,6 @@ public final class Downloader { return attempt(url, destination, false, identity, expectedSha256, cancelled, progress); } if (!appending && have > 0) { - // If-Range may legitimately return full 200 when representation changed. if (!discardPartial(destination, identity)) return Outcome.RETRY; have = 0; } @@ -333,14 +322,12 @@ public final class Downloader { } } - /** True when a `Content-Range: bytes START-END/TOTAL` header starts exactly at {@code start}. */ static boolean rangeStartsAt(String contentRange, long start) { if (contentRange == null) return false; java.util.regex.Matcher m = java.util.regex.Pattern.compile("^\\s*bytes\\s+(\\d+)-\\d+/(?:\\d+|\\*)\\s*$").matcher(contentRange); return m.matches() && Long.parseLong(m.group(1)) == start; } - /** The complete length in a `Content-Range` header, or -1 when the header leaves it unknown. */ static long rangeTotal(String contentRange) { if (contentRange == null) return -1; java.util.regex.Matcher m = java.util.regex.Pattern.compile("^\\s*bytes\\s+\\d+-\\d+/(\\d+)\\s*$").matcher(contentRange); @@ -390,7 +377,6 @@ public final class Downloader { } private static HttpURLConnection open(String url, int readTimeoutMs) throws java.io.IOException { - // An explicit Proxy routes the connection through the app setting instead of the system-wide proxy. Proxy proxy = DownloadSource.INSTANCE.httpProxy(); HttpURLConnection connection = (HttpURLConnection) (proxy != null ? new URL(url).openConnection(proxy) : new URL(url).openConnection()); diff --git a/app/src/main/java/com/droiddeck/launcher/core/GlobalSettings.kt b/app/src/main/java/com/droiddeck/launcher/core/GlobalSettings.kt index 8cbf9bf39..3e4bb5305 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/GlobalSettings.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/GlobalSettings.kt @@ -5,7 +5,6 @@ import android.net.Uri import android.provider.Settings object GlobalSettings { - // Settings.Global caches values in-process and misses deletions made over adb; ask the provider directly. fun read(context: Context, name: String): String? { val uri = Uri.withAppendedPath(Settings.Global.CONTENT_URI, name) context.contentResolver.query(uri, arrayOf(Settings.NameValueTable.VALUE), null, null, null)?.use { cursor -> @@ -19,6 +18,5 @@ object GlobalSettings { fun developerOptionsOn(context: Context): Boolean = isOn(context, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED) - // Settings.Global.ADB_WIFI_ENABLED is hidden; the key is stable since Android 11. fun wirelessDebuggingOn(context: Context): Boolean = isOn(context, "adb_wifi_enabled") } 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 f579adff0..3230e8ea4 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/LogRedactor.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/LogRedactor.kt @@ -11,7 +11,6 @@ object LogRedactor { size > 32 } - // Android matchers own native state, so reuse them instead of creating one for every log line. private val matchers = object : ThreadLocal() { override fun initialValue(): MatcherCache = MatcherCache() } @@ -145,7 +144,6 @@ object LogRedactor { val needs = name.lowercase().takeIf { name.all { it.code < 0x80 } } Learned(Regex("(?i)(?, - modifier: Modifier = Modifier, - links: List = emptyList(), -) { - Column(modifier = modifier.fillMaxWidth()) { - Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 2.dp, vertical = 2.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - items.forEach { item -> - TopTabCell( - icon = item.icon, - label = item.label, - selected = item.selected, - onClick = item.onClick, - modifier = Modifier.weight(1f), - ) - } - if (links.isNotEmpty()) { - Box( - modifier = Modifier - .padding(horizontal = 2.dp) - .width(1.dp) - .height(30.dp) - .background(MaterialTheme.colorScheme.outline.copy(alpha = 0.4f)), - ) - links.forEach { link -> - TopTabCell(link.icon, link.label, selected = false, onClick = link.onClick, isTab = false, modifier = Modifier.weight(1f)) - } - } - } - HorizontalDivider() - } -} - -@Composable -private fun TopTabCell( - icon: ImageVector, - label: String, - selected: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier, - isTab: Boolean = true, -) { - val tint = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .clip(RoundedCornerShape(9.dp)) - .background(if (selected) MaterialTheme.colorScheme.primary.copy(alpha = 0.14f) else Color.Transparent) - .focusGlide(RoundedCornerShape(9.dp)) - .then( - if (isTab) { - Modifier.selectable(selected = selected, role = androidx.compose.ui.semantics.Role.Tab, onClick = onClick) - } else { - Modifier.clickable(role = androidx.compose.ui.semantics.Role.Button, onClick = onClick) - }, - ) - .padding(vertical = 6.dp, horizontal = 1.dp), - ) { - Icon(icon, contentDescription = label, tint = tint, modifier = Modifier.size(20.dp)) - Spacer(Modifier.height(3.dp)) - Text( - label, - color = tint, - fontSize = 8.5.sp, - fontWeight = FontWeight.Bold, - letterSpacing = 0.2.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - ) - Spacer(Modifier.height(2.dp)) - Box( - modifier = Modifier - .height(2.dp) - .width(if (selected) 16.dp else 0.dp) - .clip(RoundedCornerShape(3.dp)) - .background(if (selected) MaterialTheme.colorScheme.primary else Color.Transparent), - ) - } -} 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 05d467ef5..6f5988fc1 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/FileOps.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/FileOps.kt @@ -57,7 +57,6 @@ object FileOps { return true } if (!copyWithProgress(src, dst, progress, isCancelled)) return false - // Once the destination is committed, finish deleting the source. return delete(src) } 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 273917b00..b19a2e987 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/StorageRoots.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/StorageRoots.kt @@ -63,7 +63,6 @@ object StorageRoots { } return true } - // vold-managed locations only hold removable volumes, whatever the fstype is. return path.startsWith("/storage/") || path.startsWith("/mnt/media_rw/") } @@ -92,8 +91,6 @@ object StorageRoots { .forEachIndexed { index, volume -> val uuid = volume.uuid?.takeIf { it.isNotBlank() } val dirName = volume.directory?.name?.takeIf { it.isNotBlank() } - // Unmounted volumes often report neither uuid nor directory; never drop the - // volume or a plugged-in drive stays invisible with no explanation. val descSlug = volume.getDescription(context)?.trim() ?.takeIf { it.isNotBlank() && !it.equals("android", ignoreCase = true) } ?.replace(Regex("[^A-Za-z0-9._-]+"), "-") @@ -108,8 +105,6 @@ object StorageRoots { if (name != null) { entry.candidates += File("/storage/$name") entry.candidates += File("/mnt/media_rw/$name") - // Only strict uuid matches own an app directory; a loose match would - // attach another volume's folder to this entry. if (uuid != null) appDirs.filter { belongsTo(storageManager!!, it, volume) } .forEach { entry.seeds += it } } @@ -122,8 +117,6 @@ object StorageRoots { File("/mnt/media_rw").listFiles().orEmpty().forEach { child -> if (child.isDirectory) volumeOf(child.name).candidates += child } - // USB OTG drives do not always appear as storage volumes or under /storage for - // third-party apps, but their kernel mounts are still visible in /proc/mounts. for (mount in externalMounts()) { val entry = volumeOf(mount.name) if (entry.label == null) entry.label = mount.name 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 52806833a..20f158bc5 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGames.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGames.kt @@ -180,7 +180,6 @@ object AddedGames { private fun entryFolders(context: Context): Set = AddedExes.list(context).map { it.folder }.toSet() - /** The card's Steam libraries: the arm64 client's steamapps and the x86-64 client's own folder beside it. */ internal fun isSteamLibraryFolder(name: String): Boolean = name.equals("steamapps", ignoreCase = true) || name == com.droiddeck.launcher.session.GameStorage.X64_CARD_DIR @@ -315,7 +314,6 @@ object AddedGames { fun addFolder(context: Context, dir: File, onGame: (Game) -> Unit = {}, onChecking: (String?) -> Unit = {}, showHidden: Boolean = false): FolderResult { val app = context.applicationContext - // Binds are mounted at session start, so a new one reaches the session from the next start. if (guestPath(app, dir) == null) bindFolder(app, dir) val removed = SessionPrefs.removedAddedGames(app).toSet() val listed = listedFolders(app, removed) 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 330265a0f..0983a2997 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/DependencyDetector.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/DependencyDetector.kt @@ -54,21 +54,6 @@ object DependencyDetector { "d3dx9", "d3dx11", "xact", "xact_x64", "xna31", "xna40", "physx", "oalinst", "XLiveRedist", ) - fun detectForExe(exeFile: File): List { - val start = exeFile.absoluteFile.parentFile ?: return emptyList() - var d: File? = start - var depth = 0 - while (d != null && depth < 4) { - val hasRedistContainer = try { - d.listFiles()?.any { it.isDirectory && REDIST_DIR_RE.matches(it.name.lowercase(Locale.US)) } == true - } catch (_: Throwable) { false } - if (hasRedistContainer) return detect(d) - d = d.parentFile - depth++ - } - return detect(start) - } - fun detect(gameDir: File): List { if (!gameDir.isDirectory) return emptyList() val hits = LinkedHashMap() 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 c623e4223..896d32d8d 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/Library.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/Library.kt @@ -87,7 +87,6 @@ object Library { internal fun steamLibraries(context: Context, arch: String = SessionPrefs.activeSteamClientArch(context)): List> { val root = SessionPrefs.steamRoot(context, arch) - // The session gives the x86-64 client only its card folder; the games folders stay the arm64 client's. val card = GameStorage.effective(context) if (arch == "x86_64") return listOfNotNull(root to "internal", card?.let { File(it.path, GameStorage.X64_CARD_DIR) to it.label }) return listOfNotNull( @@ -106,7 +105,6 @@ object Library { .firstOrNull { it.isDirectory } } - /** Every steamapps/compatdata folder, in the libraries [protonPrefix] searches. */ fun compatdataDirs(context: Context, libraries: List = steamLibraries(context).map { it.first }): List = prefixRoots(context, libraries).map { File(it, "steamapps/compatdata") }.filter { it.isDirectory } @@ -179,7 +177,6 @@ object Library { } } - /** Apps the user hid (the "hidden" collection) or marked private in Steam. */ internal fun concealedSteamApps(root: File): Set { val config = AddedGames.accountConfig(root) ?: return emptySet() val hidden = runCatching { @@ -220,7 +217,6 @@ object Library { return (steam + own.map { g -> addedGame(context, g, roots, played[g.appId] ?: 0L) }).distinctBy { it.gameId } } - /** Steam's LastPlayTime for each non-Steam shortcut, by its unsigned shortcut app id. */ internal fun shortcutsLastPlayed(root: File): Map = runCatching { val config = AddedGames.accountConfig(root) ?: return emptyMap() val bytes = File(config, "shortcuts.vdf").takeIf { it.isFile }?.readBytes() ?: return emptyMap() 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 cd30e400c..6fa35c753 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/LibraryCache.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/LibraryCache.kt @@ -10,7 +10,6 @@ object LibraryCache { private const val TAG = "LibraryCache" private fun file(context: Context) = File(context.filesDir, "library-cache.json") - /** The cached games and whether they are the x86-64 client's; a cache written as a bare list is arm64's. */ @Synchronized fun snapshot(context: Context): Pair, Boolean> = runCatching { val f = file(context) @@ -36,7 +35,6 @@ object LibraryCache { fun listedX64(context: Context): Boolean = snapshot(context).second - /** Without [x64] the list keeps the client already recorded. */ @Synchronized fun save(context: Context, games: List, x64: Boolean? = null) { runCatching { diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/LogoPosition.kt b/app/src/main/java/com/droiddeck/launcher/frontend/LogoPosition.kt index a047da09e..64491df82 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/LogoPosition.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/LogoPosition.kt @@ -2,7 +2,6 @@ package com.droiddeck.launcher.frontend import org.json.JSONObject -/** Where Steam pins a game's logo on its hero image and how big it draws it; the same JSON Steam keeps in `.json`. */ object LogoPosition { enum class Pin(val steam: String) { UPPER_LEFT("UpperLeft"), UPPER_CENTER("UpperCenter"), CENTER("CenterCenter"), BOTTOM_LEFT("BottomLeft"), BOTTOM_CENTER("BottomCenter"); @@ -17,7 +16,6 @@ object LogoPosition { const val MIN_PCT = 10 const val MAX_PCT = 100 - /** What Steam draws when a game has no custom position. */ val DEFAULT = Value(Pin.BOTTOM_LEFT, 50, 50) private const val VERSION = 1 @@ -25,7 +23,6 @@ object LogoPosition { fun sized(pin: Pin, size: Int): Value = size.coerceIn(MIN_PCT, MAX_PCT).let { Value(pin, it, it) } - /** `{"nVersion":1,"logoPosition":{...}}`, with whatever else [into] already held kept as it was. */ fun toJson(value: Value, into: JSONObject = JSONObject()): JSONObject = into .put("nVersion", VERSION) .put(KEY, JSONObject().put("pinnedPosition", value.pin.steam).put("nWidthPct", value.widthPct).put("nHeightPct", value.heightPct)) @@ -39,7 +36,6 @@ object LogoPosition { return Value(pin, inner.optInt("nWidthPct").coerceIn(MIN_PCT, MAX_PCT), inner.optInt("nHeightPct").coerceIn(MIN_PCT, MAX_PCT)) } - /** [existing] without the logo position; null when nothing but the version number would be left. */ fun without(existing: JSONObject): JSONObject? { existing.remove(KEY) val rest = existing.keys().asSequence().filter { it != "nVersion" }.toList() diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/SteamGrid.kt b/app/src/main/java/com/droiddeck/launcher/frontend/SteamGrid.kt index 88526f1bf..e52defb7d 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/SteamGrid.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/SteamGrid.kt @@ -7,11 +7,9 @@ import org.json.JSONObject import java.io.File import java.util.UUID -/** Steam keeps a game's custom art in `userdata//config/grid`; this is where DroidDeck's choices and Steam's meet. */ object SteamGrid { private const val TAG = "SteamGrid" - /** Every change to a game's art, here or in a scan, takes this first so two of them never interleave. */ internal val lock = Any() private const val MANIFEST = ".droiddeck-art.json" private val SUFFIX = mapOf(AddedGameArt.Slot.COVER to "p", AddedGameArt.Slot.WIDE to "", AddedGameArt.Slot.BACKGROUND to "_hero", AddedGameArt.Slot.LOGO to "_logo") @@ -36,7 +34,6 @@ object SteamGrid { private fun signature(file: File?) = if (file == null) "" else "${file.length()}:${file.lastModified()}" - /** Each Steam client has its own grid folder, so what was last seen there is kept per client. */ private fun sigName(context: Context, key: String) = if (SessionPrefs.activeSteamClientArch(context) == "x86_64") "grid-x86_64-$key" else "grid-$key" @@ -45,10 +42,6 @@ object SteamGrid { private const val GRID_ID = ".droiddeck-grid" private const val UNKNOWN_GRID = "?" - /** - * A random id kept in the grid folder, so a record made against another account's folder, or one wiped with the - * runtime, reads as "not seen yet" instead of "removed in Steam". "?" never matches, so nothing is dropped. - */ private fun gridId(grid: File): String = runCatching { val f = File(grid, GRID_ID) f.takeIf { it.isFile }?.readText()?.trim()?.takeIf { it.isNotEmpty() } ?: UUID.randomUUID().toString().also { id -> @@ -83,7 +76,6 @@ object SteamGrid { manifest.keys().asSequence().filter { name -> File(grid, name).let { it.isFile && it.length() == manifest.optLong(name, -1L) } }.toSet() }.getOrDefault(emptySet()) - /** Puts a choice (or, with null, the lack of one) where Steam reads it, whether or not Steam is running. */ fun write(context: Context, target: Target, slot: AddedGameArt.Slot, file: File?): Boolean = synchronized(lock) { place(context, target, slot, file) } private fun place(context: Context, target: Target, slot: AddedGameArt.Slot, file: File?): Boolean { @@ -116,7 +108,6 @@ object SteamGrid { private fun positionIn(file: File): LogoPosition.Value? = file.takeIf { it.isFile }?.let { runCatching { it.readText() }.getOrNull() }?.let(LogoPosition::parse) - /** The logo position chosen here (or taken over from Steam) for a game; null when Steam's own default applies. */ fun position(target: Target): LogoPosition.Value? = positionIn(File(target.artDir, POSITION)) private fun positionRecorded(context: Context, grid: File, target: Target): String? = @@ -132,12 +123,10 @@ object SteamGrid { return runCatching { file.writeText(LogoPosition.toJson(value).toString()) }.isSuccess } - /** Sets (or with null, clears) a game's logo position here and in the client's grid folder, whether or not Steam is running. */ fun writePosition(context: Context, target: Target, value: LogoPosition.Value?): Boolean = synchronized(lock) { placePosition(context, target, value) && storePosition(target, value) } - /** Steam keeps the position in `.json`; anything else that file holds is left as it was. */ private fun placePosition(context: Context, target: Target, value: LogoPosition.Value?): Boolean { val grid = dir(context) ?: return true val file = positionFile(grid, target.id) @@ -160,7 +149,6 @@ object SteamGrid { return true } - /** A `logoPosition` this version cannot read (a pin it does not know, say) is left alone rather than read as "removed". */ private fun hasUnknownPosition(file: File): Boolean { val json = runCatching { JSONObject(file.readText()) }.getOrNull() ?: return false return json.has("logoPosition") && LogoPosition.parse(json) == null @@ -176,7 +164,6 @@ object SteamGrid { when { before == null && own != null && theirs == null -> placePosition(context, target, own) before == null || now != before -> { - // Recorded only once the local side agrees, so a failed take-over is tried again on the next scan. if (theirs != null) { if (own == theirs || storePosition(target, theirs)) recordPosition(context, grid, target, now) } else { @@ -187,7 +174,6 @@ object SteamGrid { } } - /** Takes over what changed in Steam since the last look; returns true when a choice was added, replaced or dropped. */ fun sync(context: Context, targets: List): Boolean = synchronized(lock) { reconcile(context, targets) } private fun reconcile(context: Context, targets: List): Boolean { 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 e022ebc8e..a287c2299 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/SteamRedists.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/SteamRedists.kt @@ -15,7 +15,6 @@ object SteamRedists { "228989" to ("vcredist2022_dll" to "VC++ 2022"), "228990" to ("d3dx9" to "DirectX (June 2010)"), ) - /** Redists a game's own install script runs on Windows that its shared depots don't name. */ val SCRIPTED = mapOf( 72850 to listOf("xact" to "DirectX XACT audio"), 489830 to listOf("xact_x64" to "DirectX XACT audio (64-bit)"), @@ -28,7 +27,6 @@ object SteamRedists { return File(steamapps, "appmanifest_$appId.acf").takeIf { it.isFile } } - /** Redists the game's own install script has Steam run on Windows, from its "Run Process" entries. */ fun installScript(gameDir: File?): List> = gameDir?.listFiles { f -> f.isFile && f.length() < 1L shl 20 && f.name.lowercase().let { it.startsWith("installscript") && it.endsWith(".vdf") } 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 8e91102f3..22044c76d 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/WinCompSources.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/WinCompSources.kt @@ -51,10 +51,8 @@ object WinCompSources { fun installer(path: String): List = if (UE_PREREQ.containsMatchIn(path)) listOf("vcredist2022", "d3dx9") else DependencyDetector.componentsFor(path) private val XBOX_KNOWN = Regex("""> { val config = folder?.listFiles().orEmpty().firstOrNull { it.isFile && it.name.equals("MicrosoftGame.config", ignoreCase = true) && it.length() < 1L shl 20 } val declared = config?.let { file -> runCatching { XBOX_KNOWN.findAll(file.readText()).map { it.groupValues[1] }.toList() }.getOrNull() }.orEmpty() diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/Glasses.kt b/app/src/main/java/com/droiddeck/launcher/glasses/Glasses.kt index fe09a7809..f14d92da2 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/Glasses.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/Glasses.kt @@ -563,7 +563,6 @@ object Glasses { val device = selected?.first val m = selected?.second val controllable = m?.let(::canOpenModel) == true - // XREAL uses Android's Ethernet sockets, without opening or claiming USB interfaces. val permitted = controllable && device != null && (m === XrealModel || usb.hasPermission(device)) val identity = device?.let { GlassesModeState.Device(it.productId, if (permitted) runCatching { it.serialNumber }.getOrNull() else null) @@ -677,8 +676,6 @@ object Glasses { internal fun selectGlassesDisplay(displays: List, usbPresent: Boolean): Display? { val candidates = displays .filter { it.displayId != Display.DEFAULT_DISPLAY && it.flags and Display.FLAG_PRESENTATION != 0 } - // Android can expose the glasses name on either the logical display or - // its product metadata. A generic label on one must not hide the other. return candidates.firstOrNull { display -> isGlassesDisplayName(display.name) || (Build.VERSION.SDK_INT >= 31 && display.deviceProductInfo?.let { product -> @@ -728,7 +725,6 @@ object Glasses { if (device.productId == VitureProtocol.PRO2_PRODUCT_ID) { VitureV2Link.open(usb, device, v2Listener(generation)) } else if ((model as? VitureProtocol.Model)?.needsSdk == true) { - // Android can evict cacheDir while the SDK is running. Log.i(TAG, "opening VITURE SDK pid=0x%04X spatial=%s".format(device.productId, spatial)) VitureSdkLink.open(usb, device, File(app.noBackupFilesDir, "viture"), sdkListener(generation), (model as? VitureProtocol.Model)?.family, spatial) @@ -854,7 +850,6 @@ object Glasses { if (generation != linkGeneration) return@post closeLink() publish { it.copy(controlError = app.getString(R.string.glasses_connection_failed)) } - // Let a reconnect or explicit Allow retry restart the SDK, not an endless failure loop. } } @@ -974,7 +969,6 @@ object Glasses { } if (!s.usable || s.sbs || modeState.pendingStereo) return val hz = _settings.value.refreshHz - // Native 30 Hz is selected by the Android presentation, without a vendor mode command. if (hz == 30 && GlassesRendering.native30HzAvailable(_settings.value, s)) return if (glassesMode == m.mode(hz, false, outputResolution())) return if (!force) { diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/GlassesHardware.kt b/app/src/main/java/com/droiddeck/launcher/glasses/GlassesHardware.kt index 5de0f03f5..3bf992cbd 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/GlassesHardware.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/GlassesHardware.kt @@ -1,6 +1,5 @@ package com.droiddeck.launcher.glasses -/** Hardware properties; display refresh is selected from Android's advertised modes. */ interface GlassesModel { val name: String val controllable: Boolean @@ -18,9 +17,6 @@ interface GlassesModel { fun modeForStep(step: Int): Int? } -/** The command IDs here are the existing glasses controller's internal vocabulary. - * Each transport translates them to its own wire protocol; unsupported controls return false. - */ internal interface GlassesLink { val hasImu: Boolean val badFrames: Long get() = 0L diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/GlassesMenuSurface.kt b/app/src/main/java/com/droiddeck/launcher/glasses/GlassesMenuSurface.kt index cefe3d932..501aaae1b 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/GlassesMenuSurface.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/GlassesMenuSurface.kt @@ -63,7 +63,6 @@ class GlassesMenuSurface( override fun onDetachedFromWindow() { close() super.onDetachedFromWindow() - // GLSurfaceView may exit its GL thread before queued cleanup is consumed. releaseInput() } diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/GlassesRendering.kt b/app/src/main/java/com/droiddeck/launcher/glasses/GlassesRendering.kt index b07221556..01df9fe56 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/GlassesRendering.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/GlassesRendering.kt @@ -2,7 +2,6 @@ package com.droiddeck.launcher.glasses import com.droiddeck.launcher.gpu.Cardboard -/** Share the actual picture mode with settings so controls match the rendered view. */ internal object GlassesRendering { enum class Picture { FLAT, DUPLICATED, VIRTUAL_SCREEN, STEREO } @@ -51,11 +50,7 @@ internal object GlassesRendering { outputActive: Boolean, steamVrRunning: Boolean, ): Cardboard { - // Mirror output still uses the glasses' picture and sensors during VR. if (!usesGlasses(state, outputActive, steamVrRunning)) { - // SteamVR uses the renderer's enabled flag as headset proximity. - // A flat phone preview still needs an active HMD and one SBS eye; - // otherwise SteamVR enters standby and leaves a frozen frame. return if (steamVrRunning && !base.enabled) base.copy( enabled = true, source = Cardboard.SOURCE_SBS, diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/GlassesScreen.kt b/app/src/main/java/com/droiddeck/launcher/glasses/GlassesScreen.kt index c15e9c623..43f80bfc3 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/GlassesScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/GlassesScreen.kt @@ -36,7 +36,6 @@ class GlassesScreen(context: Context, display: Display, refreshHz: Int, private fun prefer(hz: Int, resolution: GlassesResolution? = null) { wantHz = hz wantResolution = resolution - // A 2D/SBS transition can replace the active mode without changing Hz. applyMode() } diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/HandImageBuffer.kt b/app/src/main/java/com/droiddeck/launcher/glasses/HandImageBuffer.kt index b6aa9844a..a3d94cd6a 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/HandImageBuffer.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/HandImageBuffer.kt @@ -51,7 +51,6 @@ internal class HandImageBuffer { pixels.clear() pixels.put(rgb) pixels.rewind() - // MediaPipe applies the model metadata's float normalization to these RGB bytes. return ByteBufferImageBuilder(pixels, width, height, MPImage.IMAGE_FORMAT_RGB).build() } } diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/LumaRgbDiagnostic.kt b/app/src/main/java/com/droiddeck/launcher/glasses/LumaRgbDiagnostic.kt index 6a483a1a0..5f6175474 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/LumaRgbDiagnostic.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/LumaRgbDiagnostic.kt @@ -150,7 +150,6 @@ class LumaRgbCaptureService : Service() { intent?.getIntExtra("durationMs",5000) ?: 5000) } finally { stopSelf() - // SDK 2.4.0 can crash while cancelling failed USB transfers; isolate each diagnostic. android.os.Process.killProcess(android.os.Process.myPid()) } }, "luma-rgb-diagnostic").start() diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoCalibration.kt b/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoCalibration.kt index 8e579aee3..23ebcce81 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoCalibration.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoCalibration.kt @@ -40,12 +40,9 @@ internal data class RayNeoCalibration(val gyroBiasDps: Vec3, val level: Quaterni internal class RayNeoCalibrationStore(private val prefs: SharedPreferences) { companion object { - // GT and GT Max can share USB identity fields. Keep the non-Max mounting - // correction separate from existing Max profiles and their legacy keys. fun profileKey(base: String, gtUsb: Boolean, boardId: Int): String = if (gtUsb && boardId == RayNeoProtocol.BOARD_GT) "$base:gt-standard-v1" else base } - // A serial isolates physical units. Devices without one share their model profile. fun key(vid: Int, pid: Int, serial: String?, modelName: String = ""): String = "rayneo-cal-v1:$vid:$pid:${serial?.takeIf { it.isNotBlank() } ?: "model:$modelName"}" fun load(key: String): RayNeoCalibration? = RayNeoCalibration.decode(prefs.getString(key, null)) @@ -55,7 +52,6 @@ internal class RayNeoCalibrationStore(private val prefs: SharedPreferences) { } } -/** One upside-down stationary pose estimates gyro bias and tilt, never yaw or accelerometer scale. */ internal class RayNeoCalibrationCollector(private val axes: RayNeoSensorAxes, private val beganMs: Long) { var phase = CalibrationPhase.SETTLING; private set var progress = 0; private set diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoLink.kt b/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoLink.kt index c9770ed22..78c2275b4 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoLink.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoLink.kt @@ -6,7 +6,6 @@ import android.hardware.usb.UsbManager import android.os.SystemClock import android.util.Log -/** RayNeo command/response and IMU share one endpoint. Keep reads on a single worker. */ internal class RayNeoLink private constructor( private val connection: UsbDeviceConnection, private val endpoints: RayNeoEndpointSelection, @@ -52,7 +51,6 @@ internal class RayNeoLink private constructor( ready && (mode == RayNeoProtocol.CMD_SWITCH_TO_3D || mode == RayNeoProtocol.CMD_SWITCH_TO_2D) && command(mode) } - // No VITURE brightness, volume, film, rate or raw command is sent to RayNeo. else -> false } } @@ -159,7 +157,6 @@ internal class RayNeoLink private constructor( } val wxyz = filter.update(packet.sample) if (wantImu && collector == null && lastSensor >= warmupUntil) { - // Normalize basis and component order at the hardware boundary. listener.onPose(raw, RayNeoOrientationFilter.glassesPose(wxyz), SystemClock.elapsedRealtimeNanos()) } @@ -176,7 +173,6 @@ internal class RayNeoLink private constructor( override fun close() { synchronized(writeLock) { if (!running) return - // Best effort: stop the sensor before releasing the claimed interface. if (ready) runCatching { command(RayNeoProtocol.CMD_CLOSE_IMU) } running = false } diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoModel.kt b/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoModel.kt index ccb19f744..72c88b34b 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoModel.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoModel.kt @@ -8,8 +8,6 @@ object RayNeoModel { fun model(vid: Int, pid: Int, name: String?): GlassesModel? { if (!isVendor(vid)) return null - // GT's identity is confirmed by verncat's SDK; do not claim other products - // from that vendor just because their name resembles a supported model. val gt = RayNeoProtocol.isGt(vid, pid) if (vid == RayNeoProtocol.GT_VENDOR_ID && !gt) return null if (!(vid == RayNeoProtocol.VENDOR_ID && pid == RayNeoProtocol.PRODUCT_ID) && @@ -23,8 +21,6 @@ object RayNeoModel { name.contains("rayneo", ignoreCase = true) || name.contains("ray neo", ignoreCase = true) || modelName.containsMatchIn(name) - // Observed on GT Max: Android exposes an unbranded EDID monitor name. - // Match its product metadata, not every display called SmartGlasses/TCL. fun isDisplayProduct(name: String?, manufacturer: String?, productId: String?): Boolean = name?.trim().equals("SmartGlasses", ignoreCase = true) && manufacturer.equals("TCL", ignoreCase = true) && productId == "980" @@ -33,7 +29,7 @@ object RayNeoModel { override val controllable = true override val brightnessMax = 0 override val volumeMax = 0 - override val diagonalFovDeg = 46f // Conservative existing default, not a calibrated model profile. + override val diagonalFovDeg = 46f override val sbs90 = false override fun mode(hz: Int, sbs: Boolean) = if (sbs) RayNeoProtocol.CMD_SWITCH_TO_3D else RayNeoProtocol.CMD_SWITCH_TO_2D diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoOrientationFilter.kt b/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoOrientationFilter.kt index 96bff8500..22a3a8965 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoOrientationFilter.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoOrientationFilter.kt @@ -1,5 +1,4 @@ -// Adapted from informalTechCode/RayNeo-Air-Series-3DoF-Scaffold (Apache-2.0). -// See docs/third-party/rayneo/. Returns scaffold quaternions in (w, x, y, z) order. +// Adapted from informalTechCode/RayNeo-Air-Series-3DoF-Scaffold (Apache-2.0); see docs/third-party/rayneo/ for license and attribution. package com.droiddeck.launcher.glasses import kotlin.math.cos @@ -123,10 +122,6 @@ internal class RayNeoOrientationFilter( private var lastRealtimeNs: Long = 0L companion object { - /** Scaffold: right +X, up +Y, back +Z. VITURE: right -Y, up +Z, back -X. - * Conjugate by HeadPose.GLASSES_AXES before entering the unchanged VITURE pose path. - * This is a basis conversion, not a projection or seated-origin adjustment. - */ fun glassesPose(wxyz: FloatArray): FloatArray = floatArrayOf(-wxyz[3], -wxyz[1], wxyz[2], wxyz[0]) } @@ -168,8 +163,6 @@ internal class RayNeoOrientationFilter( if (accelNorm > 1e-3f) { val measuredUp = accel.normalized() if (!gravityInitialized) { - // Align gravity before publishing the first pose. Starting at - // identity and settling after recenter can rotate the horizon. val axis = measuredUp.cross(worldUp) q = if (measuredUp.y < -0.9999f) { Quaternion.fromAxisAngle(Vec3(1f, 0f, 0f), Math.PI.toFloat()) @@ -179,7 +172,6 @@ internal class RayNeoOrientationFilter( gravityInitialized = true } val predictedUp = q.conjugate().rotate(worldUp).normalized() - // q maps body to world: measured x predicted reduces gravity error. val correction = measuredUp.cross(predictedUp) gyro += correction * accelGain } diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoProtocol.kt b/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoProtocol.kt index 9f65b04df..781f0555d 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoProtocol.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/RayNeoProtocol.kt @@ -40,14 +40,13 @@ data class RayNeoEndpointSelection( object RayNeoProtocol { const val VENDOR_ID = 0x1BBB const val PRODUCT_ID = 0xAF50 - // verncat/RayNeo-Air-3S-Pro-OpenVR PR #3 (MIT), tested on GT hardware. const val GT_VENDOR_ID = 0x3941 private const val GT_CONTROL_INTERFACE = 5 fun isGt(vid: Int, pid: Int) = vid == GT_VENDOR_ID && pid == PRODUCT_ID const val BOARD_AIR_4_PRO = 0x3A - const val BOARD_GT = 0x40 // Observed on the user's non-Max GT. + const val BOARD_GT = 0x40 const val PKT_MAGIC = 0x99 const val PKT_SENSOR = 0x65 @@ -131,7 +130,6 @@ object RayNeoProtocol { endpoint.type != UsbConstants.USB_ENDPOINT_XFER_BULK) { continue } - // Exclude small button reports from GT's protocol transport. if (gt && endpoint.maxPacketSize < 64) continue if (endpoint.direction == UsbConstants.USB_DIR_IN && inEndpoint == null) { @@ -159,8 +157,6 @@ object RayNeoProtocol { } } - // Interface numbers can change with firmware. Use a unique protocol-sized - // pair when interface 5 is absent, never an arbitrary composite interface. return if (gt) gtCandidates.singleOrNull() else fallback } diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/RokidLink.kt b/app/src/main/java/com/droiddeck/launcher/glasses/RokidLink.kt index dbf10a6e6..3b1002eba 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/RokidLink.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/RokidLink.kt @@ -9,7 +9,6 @@ import android.hardware.usb.UsbManager import android.os.SystemClock import android.util.Log -/** Controls are vendor control transfers; the HID interrupt endpoint streams IMU reports unprompted. */ internal class RokidLink private constructor( private val connection: UsbDeviceConnection, private val usbInterface: UsbInterface, @@ -44,7 +43,7 @@ internal class RokidLink private constructor( ?.let { listener.onValue(VitureProtocol.MSG_BRIGHTNESS, RokidModel.brightnessStep(it)) } != null VitureProtocol.MSG_BRIGHTNESS -> value in 0..RokidModel.BRIGHTNESS_STEPS && control(RokidModel.REQUEST_SET_BRIGHTNESS, RokidModel.brightnessRaw(value), RokidModel.INDEX_BRIGHTNESS) - else -> false // Volume is the glasses' USB audio device; there is no film or IMU-rate control. + else -> false } } @@ -54,7 +53,6 @@ internal class RokidLink private constructor( if (!running) return null val buffer = ByteArray(64) val count = connection.controlTransfer(VENDOR_IN, request, 0, index, buffer, buffer.size, TIMEOUT_MS) - // Display mode is reported in the second byte, brightness in the first. val offset = if (request == RokidModel.REQUEST_GET_DISPLAY) 1 else 0 if (count > offset) buffer[offset].toInt() and 0xff else null } @@ -90,7 +88,6 @@ internal class RokidLink private constructor( if (enabled && !wasTracking) fusion = ImuFusion() wasTracking = enabled if (!enabled) continue - // Max captures: already right/up/back; level gravity +Y, turn left +gyroY, look up +gyroX, roll right -gyroZ. fusion.update(sample.timeNs, sample.accel, sample.gyro)?.let { if (running && tracking) listener.onPose(buffer.copyOf(count), it, SystemClock.elapsedRealtimeNanos()) } diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/RokidModel.kt b/app/src/main/java/com/droiddeck/launcher/glasses/RokidModel.kt index 35227206b..62d5ab81d 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/RokidModel.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/RokidModel.kt @@ -3,7 +3,6 @@ package com.droiddeck.launcher.glasses import java.nio.ByteBuffer import java.nio.ByteOrder -/** Rokid Air/Max share one USB identity; Max 2 has a separate product ID. */ internal class RokidModel private constructor(override val name: String, private val max: Boolean) : GlassesModel { override val controllable = true override val brightnessMax = BRIGHTNESS_STEPS @@ -36,7 +35,6 @@ internal class RokidModel private constructor(override val name: String, private const val MAX2_PRODUCT_ID = 0x2002 const val BRIGHTNESS_STEPS = 10 - // Vendor control requests, device recipient. const val REQUEST_SET_DISPLAY = 0x01 const val REQUEST_GET_DISPLAY = 0x81 const val REQUEST_SET_BRIGHTNESS = 0x02 @@ -51,7 +49,6 @@ internal class RokidModel private constructor(override val name: String, private const val MODE_SBS_1200_60 = 5 val MODES = setOf(MODE_2D_60, MODE_SBS_1080_60, MODE_2D_120, MODE_SBS_1200_90, MODE_SBS_1200_60) - // 0x04d2 is a shared vendor ID, so the Rokid product string is required too. fun model(vid: Int, pid: Int, name: String?): GlassesModel? { if (vid != VENDOR_ID || (pid != PRODUCT_ID && pid != MAX2_PRODUCT_ID)) return null val product = name?.trim()?.takeIf { it.startsWith("Rokid", ignoreCase = true) } ?: return null @@ -60,7 +57,6 @@ internal class RokidModel private constructor(override val name: String, private fun isDisplay(name: String) = name.contains("Rokid", ignoreCase = true) - /** Device brightness is 0..100; the shared slider uses ten steps. */ fun brightnessStep(raw: Int): Int = ((raw.coerceIn(0, 100) + 5) / 10).coerceIn(0, BRIGHTNESS_STEPS) fun brightnessRaw(step: Int): Int = step.coerceIn(0, BRIGHTNESS_STEPS) * 10 } @@ -74,7 +70,6 @@ internal object RokidProtocol { private const val SENSOR_ACCEL = 1 private const val SENSOR_GYRO = 2 - /** Max sends one combined report; Air sends accel and gyro reports that share a timestamp. */ class Parser { private var accel: Vec3? = null private var accelTime = -1L @@ -87,7 +82,7 @@ internal object RokidProtocol { return when (packet[0].toInt() and 0xff) { COMBINED -> Sample(b.getLong(1), vec(b, 9), vec(b, 21), packet[59].toInt() and 0xff) SEPARATE -> { - val time = b.getLong(9) * 1000 // Air reports microseconds; Max reports nanoseconds. + val time = b.getLong(9) * 1000 when (packet[1].toInt() and 0xff) { SENSOR_ACCEL -> { accel = vec(b, 21); accelTime = time } SENSOR_GYRO -> { gyro = vec(b, 21); gyroTime = time } diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/StereoCalibration.kt b/app/src/main/java/com/droiddeck/launcher/glasses/StereoCalibration.kt index 027f0b92e..8c08316d5 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/StereoCalibration.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/StereoCalibration.kt @@ -26,7 +26,6 @@ internal class StereoCalibration(val identity: String, val width: Int, val heigh val source get() = if (bundledBase && !verified) "luma-ultra-base" else "device" private val p1 = Mat.zeros(3, 4, CvType.CV_64F) private val p2 = Mat.zeros(3, 4, CvType.CV_64F) - private val undistortionMaps = Array(2) { arrayOf(Mat(),Mat()) } init { val a = Mat.eye(3, 4, CvType.CV_64F) val b = Mat.zeros(3, 4, CvType.CV_64F) @@ -35,18 +34,6 @@ internal class StereoCalibration(val identity: String, val width: Int, val heigh Core.gemm(k1, a, 1.0, empty, 0.0, p1); Core.gemm(k2, b, 1.0, empty, 0.0, p2) a.release(); b.release(); empty.release() } - fun undistort(src: Mat, dst: Mat, right: Boolean) { - val k = if (right) k2 else k1; val d = if (right) d2 else d1 - val maps=undistortionMaps[if(right) 1 else 0] - if(maps[0].empty()) { - val identity=Mat.eye(3,3,CvType.CV_64F) - try { - if(fisheye) Calib3d.fisheye_initUndistortRectifyMap(k,d,identity,k,src.size(),CvType.CV_16SC2,maps[0],maps[1]) - else Calib3d.initUndistortRectifyMap(k,d,identity,k,src.size(),CvType.CV_16SC2,maps[0],maps[1]) - } finally { identity.release() } - } - Imgproc.remap(src,dst,maps[0],maps[1],Imgproc.INTER_LINEAR) - } fun triangulateRaw(left: List, right: List): List? { val points = triangulateLandmarksRaw(left,right) ?: return null return if(points.any { it == null }) null else points.filterNotNull() @@ -74,10 +61,6 @@ internal class StereoCalibration(val identity: String, val width: Int, val heigh return triangulatePoints(l.toList(),r.toList()) } finally { l.release(); r.release(); identity.release() } } - fun triangulate(left: List, right: List): List? { - val points = triangulatePoints(left,right) ?: return null - return if(points.any { it == null }) null else points.filterNotNull() - } private fun triangulatePoints(left: List, right: List): List? { if (left.size != right.size || left.isEmpty()) return null val a = Mat(2, left.size, CvType.CV_64F); val b = Mat(2, right.size, CvType.CV_64F); val out = Mat() @@ -105,7 +88,7 @@ internal class StereoCalibration(val identity: String, val width: Int, val heigh val tmp = File(file.parentFile,file.name+".tmp"); tmp.writeText(j.toString()) java.nio.file.Files.move(tmp.toPath(),file.toPath(),java.nio.file.StandardCopyOption.REPLACE_EXISTING) } - override fun close() { (listOf(k1,d1,k2,d2,r,t,p1,p2)+undistortionMaps.flatMap { it.toList() }).forEach { it.release() } } + override fun close() { listOf(k1,d1,k2,d2,r,t,p1,p2).forEach { it.release() } } companion object { fun load(file: File, identity: String, width: Int, height: Int): StereoCalibration? = runCatching { StereoCalibrationProfile.parse(file.readText(), identity, width, height)?.let(::fromProfile) }.getOrNull() @@ -286,7 +269,6 @@ internal class StereoCalibrator(private val context: Context) : AutoCloseable { Mat().also { output -> corrected.add(output); Calib3d.fisheye_undistortPoints(pixels,output,k,d,identity,k) } } try { - // OpenCV 4.13 fisheye stereo fitting assumes equal corner counts in every view. Calib3d.stereoCalibrate(objects,images(left,k1,d1),images(right,k2,d2),k1,if(fish)zero else d1,k2,if(fish)zero else d2, size,r,t,e,f,Calib3d.CALIB_FIX_INTRINSIC) } finally { (corrected+listOf(e,f,identity,zero)).forEach(Mat::release) } diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/StereoHandEngine.kt b/app/src/main/java/com/droiddeck/launcher/glasses/StereoHandEngine.kt index becad63a0..78aad3f61 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/StereoHandEngine.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/StereoHandEngine.kt @@ -383,7 +383,6 @@ internal class StereoHandEngine(private val context: Context, private val token: message=context.getString(R.string.spatial_frame_changed) calibrationProgress = calibrationProgress.copy(collecting = false, capturing = false, captureMessage = message) } - // The callback pose's basis is undocumented; use canonical GL pose history. val observation=HandObservation(time,generation,trackingFrame) var captureHead=observation.headPose(SpatialTracking.session,metadata[6]>0) val w=metadata[0].toInt();val h=metadata[1].toInt() @@ -499,7 +498,6 @@ internal class StereoHandEngine(private val context: Context, private val token: dl=pair.first.hands;dr=pair.second.hands if(scheduled) inferenceSchedule.observe(stamp,dl.map { it.side },dr.map { it.side },pair.first.rawCount,pair.second.rawCount) val inferenceMs=(System.nanoTime()-inferenceStart)/1e6f - // VIO callbacks can arrive during inference; verify this exact camera stamp before publishing. captureHead=observation.headPose(SpatialTracking.session, metadata[6]>0 || VitureSdk.nativeCameraSynchronized(token,metadata[2])) val hands=Array(2) { SpatialHand() } @@ -631,7 +629,7 @@ internal class StereoHandEngine(private val context: Context, private val token: SpatialTracking.publish(System.nanoTime()) } } - } catch(e: InterruptedException) { /* Shutdown interrupts the idle wait. */ } + } catch(e: InterruptedException) { } catch(e: Throwable) { message=context.getString(R.string.spatial_hand_error, e.message.orEmpty()) val unavailable=HandObservationDiagnostic(HandObservationOutcome.INFERENCE_UNAVAILABLE) diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/StereoInferenceRunner.kt b/app/src/main/java/com/droiddeck/launcher/glasses/StereoInferenceRunner.kt index 351fe332e..a8c994504 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/StereoInferenceRunner.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/StereoInferenceRunner.kt @@ -31,7 +31,6 @@ internal class StereoInferenceRunner : AutoCloseable { catch (e: ExecutionException) { throw e.cause ?: e } } } finally { - // Native inference must finish before either camera's pixels or detector can be reused. if (interrupted) Thread.currentThread().interrupt() } } diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/UvcRgbDiagnostic.kt b/app/src/main/java/com/droiddeck/launcher/glasses/UvcRgbDiagnostic.kt index 6bd9c6b15..ce119c044 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/UvcRgbDiagnostic.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/UvcRgbDiagnostic.kt @@ -12,8 +12,6 @@ internal object UvcRgbDiagnostic { internal data class UvcStreamHealth(val observationMs: Double, val observedFps: Double, val maximumGapMs: Double, val healthy: Boolean) { companion object { - // Health requires the full requested window, 80% of requested FPS, and no gap - // (including the final silent interval) above max(250 ms, three frame periods). fun assess(metadata: LongArray, requestedFps: Int, durationMs: Int): UvcStreamHealth { require(metadata.size == 14 && requestedFps > 0 && durationMs > 0) val first = metadata[7] diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/VitureProtocol.kt b/app/src/main/java/com/droiddeck/launcher/glasses/VitureProtocol.kt index 573f0f8dc..55302dfc3 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/VitureProtocol.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/VitureProtocol.kt @@ -81,7 +81,6 @@ object VitureProtocol { } } - /** SDK validates the PID before returning this market name. Never guess a new PID. */ fun sdkModel(marketName: String): Model? { val family = when (marketName) { "One", "Lite" -> Family.ONE diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/VitureSdk.kt b/app/src/main/java/com/droiddeck/launcher/glasses/VitureSdk.kt index 1b5f09e3d..bdad575c1 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/VitureSdk.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/VitureSdk.kt @@ -3,7 +3,6 @@ package com.droiddeck.launcher.glasses import android.util.Log import kotlin.math.sqrt -/** JNI adapter to VITURE's Android C SDK. The vendor binary is a local build input. */ internal object VitureSdk { val available: Boolean by lazy { runCatching { @@ -27,7 +26,6 @@ internal object VitureSdk { external fun nativeSpatialDiagnostics(token: Long): DoubleArray? external fun nativeTracking(token: Long, enabled: Boolean): Int external fun nativeCommand(token: Long, command: Int, value: Int): Int - /** Returns CLOCK_BOOTTIME receipt time, zero for no new pose, or a negative SDK error. */ external fun nativePose(token: Long, wxyz: FloatArray): Long external fun nativeNextState(token: Long, event: IntArray): Int external fun nativeClose(token: Long) @@ -48,12 +46,9 @@ internal object VitureSpatialIdentity { } internal object VitureSdkPose { - // Pro 2 hardware captures use OpenGL axes despite the generic Gen2 callback's - // NWU documentation. Keep that model-specific exception out of other Gen2 devices. fun usesOpenGlAxes(carina: Boolean, family: VitureProtocol.Family?) = carina || family == VitureProtocol.Family.PRO2 - /** SDK quaternions are wxyz; normalize their coordinate basis at the boundary. */ fun toGlasses(wxyz: FloatArray, openGlAxes: Boolean): FloatArray? { if (wxyz.size != 4 || wxyz.any { !it.isFinite() }) return null val length = sqrt(wxyz.sumOf { it.toDouble() * it }.toFloat()) diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/VitureSdkLink.kt b/app/src/main/java/com/droiddeck/launcher/glasses/VitureSdkLink.kt index 36ceedf6e..fe42adc71 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/VitureSdkLink.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/VitureSdkLink.kt @@ -7,7 +7,6 @@ import android.os.SystemClock import android.util.Log import java.io.File -/** SDK owns interface selection (including Ultra bulk endpoints) and all USB I/O. */ internal class VitureSdkLink private constructor( private val connection: UsbDeviceConnection, private val token: Long, @@ -47,7 +46,6 @@ internal class VitureSdkLink private constructor( if (rc == 0 || !wanted) tracking = wanted return result("tracking", rc) } - // Frequency is negotiated against SDK capabilities, including Pro 2's pose limit. VitureProtocol.MSG_IMU_RATE -> return tracking VitureProtocol.MSG_GET_IMU -> return true } @@ -89,7 +87,6 @@ internal class VitureSdkLink private constructor( if (VitureSdk.nativeNextState(token, event) == 1) when (event[0]) { 0 -> listener.onValue(VitureProtocol.MSG_GET_BRIGHTNESS, event[1]) 1 -> listener.onValue(VitureProtocol.MSG_GET_VOLUME, event[1]) - // A device event can use a different display-mode namespace. 2 -> send(VitureProtocol.MSG_GET_DISPLAY) 3 -> listener.onValue(VitureProtocol.MSG_FILM, event[1]) } @@ -122,7 +119,6 @@ internal class VitureSdkLink private constructor( if (!running) return running = false VitureSdk.nativeClose(token) - // Keep Java's descriptor alive until the SDK and its worker threads have stopped. connection.close() } if (Thread.currentThread() !== worker) worker.join(1000) diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/VitureV2Link.kt b/app/src/main/java/com/droiddeck/launcher/glasses/VitureV2Link.kt index d46c246b7..16bd1195a 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/VitureV2Link.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/VitureV2Link.kt @@ -69,7 +69,6 @@ internal class VitureV2Link internal constructor( return true } if (msgId == VitureProtocol.MSG_GET_IMU) return true - // Keep the measured 120 Hz mode; the generic controller requests 240 Hz. if (msgId == VitureProtocol.MSG_IMU_RATE) return false val query = when (msgId) { VitureProtocol.MSG_GET_BRIGHTNESS -> VitureV2Protocol.MSG_GET_BRIGHTNESS @@ -146,9 +145,7 @@ internal class VitureV2Link internal constructor( tracking(true) if (becameReady) listener.onReady() val q = pose.quaternion - // Upstream right/up/back pose converted to the shared VITURE legacy basis. val shared = floatArrayOf(-q[2], -q[0], q[1], q[3]) - // Upstream does not establish device tick units; use host monotonic receipt time. if (running && wanted && streamOn) listener.onPose(received.bytes, shared, received.timeNs) return } diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/VitureV2Protocol.kt b/app/src/main/java/com/droiddeck/launcher/glasses/VitureV2Protocol.kt index 548ad6b91..e2e4fdbfc 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/VitureV2Protocol.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/VitureV2Protocol.kt @@ -1,5 +1,4 @@ -// Adapted from elasticjava/viture-v2, MIT, Copyright (c) 2026 Holger Bartnick. -// Source: ece8c30485f7d357fefa362ae74f3eabac9b3899; see docs/third-party/viture-v2/. +// Adapted from elasticjava/viture-v2, MIT, Copyright (c) 2026 Holger Bartnick. Source: ece8c30485f7d357fefa362ae74f3eabac9b3899; see docs/third-party/viture-v2/. package com.droiddeck.launcher.glasses import kotlin.math.sqrt @@ -37,7 +36,6 @@ object VitureV2Protocol { val success: Boolean get() = status == 0 } - // Device ticks have no established time unit; quaternion is xyzw with no axis remapping. class Pose internal constructor( val deviceTick: Long, val quaternion: FloatArray, @@ -64,7 +62,6 @@ object VitureV2Protocol { } fun imuControl(streams: Int, rate: Rate = Rate.HZ_120): ByteArray { - // Upstream measured that stream mask 3 ACKs but produces no events on Pro 2. require(streams == STREAM_OFF || streams == STREAM_POSE || streams == STREAM_RAW) require(streams != STREAM_POSE || rate.hz <= 240) val wireRate = if (streams == STREAM_OFF) 0 else rate.wireValue diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/XrealConnection.kt b/app/src/main/java/com/droiddeck/launcher/glasses/XrealConnection.kt index 3dbd22eb3..e972cd450 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/XrealConnection.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/XrealConnection.kt @@ -24,7 +24,6 @@ internal interface XrealConnector : Closeable { internal class XrealNetworkMissing : IOException("XREAL USB Ethernet network is not available") -/** Bind individual sockets, never the process: Steam must retain its Internet network. */ internal class AndroidXrealConnector(context: Context) : XrealConnector { private val manager = context.getSystemService(ConnectivityManager::class.java) private val address = InetAddress.getByName(XrealProtocol.HOST) diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/XrealLink.kt b/app/src/main/java/com/droiddeck/launcher/glasses/XrealLink.kt index 86f93fbef..18ce636ee 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/XrealLink.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/XrealLink.kt @@ -7,7 +7,6 @@ import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicReference -/** One connection owns two TCP channels. All control requests are serialized off the UI thread. */ internal class XrealLink internal constructor( private val connector: XrealConnector, private val listener: Listener, @@ -47,7 +46,7 @@ internal class XrealLink internal constructor( VitureProtocol.MSG_DISPLAY -> value in 2..5 || value == XrealModel.NATIVE_60HZ VitureProtocol.MSG_BRIGHTNESS -> value in 0..9 VitureProtocol.MSG_FILM -> value in 0..1 - else -> false // No guessed volume, sensor-rate, or VITURE wire commands. + else -> false } return valid && commands.offer(Command(msgId, value)) } @@ -88,7 +87,7 @@ internal class XrealLink internal constructor( } } catch (e: Exception) { failure.set(e) - current.close() // Wake a control read immediately. + current.close() } }, "xreal-reports").apply { isDaemon = true; start() } ready = true @@ -145,12 +144,11 @@ internal class XrealLink internal constructor( tracking = false commands.clear() connection.getAndSet(null)?.close() - connector.close() // Includes sockets still inside connect(). + connector.close() worker.interrupt() } } -/** Kept independent of Android so the real framing/acknowledgement path is testable. */ internal class XrealControl(private val channels: XrealChannels, private val settleDelayMs: Long = 150) { private var transaction = 0 private fun exchange(command: Int, value: Int? = null): ByteArray { @@ -158,7 +156,7 @@ internal class XrealControl(private val channels: XrealChannels, private val set channels.controlOutput.write(XrealProtocol.request(command, transaction, value)) channels.controlOutput.flush() val deadline = System.nanoTime() + 3_000_000_000L - repeat(32) { // Bound unsolicited button/status packets and stale responses. + repeat(32) { if (System.nanoTime() > deadline) throw IOException("XREAL control timed out") XrealProtocol.response(XrealProtocol.read(channels.controlInput), command, transaction)?.let { return it } } @@ -172,9 +170,6 @@ internal class XrealControl(private val channels: XrealChannels, private val set fun changeDisplay(mode: Int): Int { val stereo = mode == 5 set(XrealProtocol.SET_INPUT, if (stereo) 1 else 0) - // On 1S, SET_INPUT itself changes EDID. Sending SET_DISPLAY immediately - // receives error 5001 while that change is pending. GET_DISPLAY returns - // an empty property during the transition; wait for a real mode report. val current = awaitDisplay { (it == 5) == stereo } if (current == mode) return current set(XrealProtocol.SET_DISPLAY, mode) diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/XrealModel.kt b/app/src/main/java/com/droiddeck/launcher/glasses/XrealModel.kt index 19cd2097c..2de2236c7 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/XrealModel.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/XrealModel.kt @@ -1,10 +1,9 @@ package com.droiddeck.launcher.glasses -/** First One-family target. Recovery and Air USB products must not use this transport. */ object XrealModel : GlassesModel { const val VENDOR_ID = 0x3318 const val PRODUCT_1S = 0x043e - const val NATIVE_60HZ = 29 // Captured on 1S: 1920 x 1200 at 60 Hz. + const val NATIVE_60HZ = 29 override val name = "XREAL 1S" override val controllable = true override val brightnessMax = 9 @@ -16,7 +15,6 @@ object XrealModel : GlassesModel { fun matches(vid: Int, pid: Int) = vid == VENDOR_ID && pid == PRODUCT_1S fun isDisplay(name: String) = name.contains("XREAL", ignoreCase = true) - // One-family EDID configuration values, not VITURE's display mode IDs. override fun mode(hz: Int, sbs: Boolean) = when { sbs -> 5 hz >= 120 -> 4 diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/XrealOrientationFilter.kt b/app/src/main/java/com/droiddeck/launcher/glasses/XrealOrientationFilter.kt index e8d34f44a..c10bb9473 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/XrealOrientationFilter.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/XrealOrientationFilter.kt @@ -2,19 +2,14 @@ package com.droiddeck.launcher.glasses import kotlin.math.abs -/** 1S radians/sec and device nanoseconds, mapped using directed hardware captures. */ internal class XrealOrientationFilter { private val fusion = ImuFusion() - // 1S reports right/down/forward. Rotate 180 degrees about X into the - // shared right/up/back basis before either gravity alignment or fusion. - // Captures: level gravity -Y; looking left -gyroY; up +gyroX; right tilt +gyroZ. fun update(sample: XrealProtocol.Sample): FloatArray? = fusion.update(sample.timeNs, Vec3(sample.accel.x, -sample.accel.y, -sample.accel.z), Vec3(sample.gyro.x, -sample.gyro.y, -sample.gyro.z)) } -/** Gyro integration with gravity correction in the shared right/up/back basis, rad/s and device ns. */ internal class ImuFusion { private var previous = 0L private var rotation = Quaternion.IDENTITY @@ -27,7 +22,7 @@ internal class ImuFusion { previous = timeNs val norm = accel.length() val gravityValid = abs(norm - 9.81f) < 1.5f - if (dt > 0.25f) initialized = false // Never integrate a reconnect/stall as motion. + if (dt > 0.25f) initialized = false if (!initialized) { if (!gravityValid) return null val up = accel.normalized() @@ -47,7 +42,6 @@ internal class ImuFusion { } rotation = rotation.integrateBodyRate(rate, dt).normalized() } - // Same right/up/back -> shared glasses basis used by the RayNeo adapter. return RayNeoOrientationFilter.glassesPose(floatArrayOf(rotation.w, rotation.x, rotation.y, rotation.z)) } } diff --git a/app/src/main/java/com/droiddeck/launcher/glasses/XrealProtocol.kt b/app/src/main/java/com/droiddeck/launcher/glasses/XrealProtocol.kt index d18ca1a94..09bed0179 100644 --- a/app/src/main/java/com/droiddeck/launcher/glasses/XrealProtocol.kt +++ b/app/src/main/java/com/droiddeck/launcher/glasses/XrealProtocol.kt @@ -6,7 +6,6 @@ import java.io.InputStream import java.nio.ByteBuffer import java.nio.ByteOrder -/** Independent implementation of the One-family wire format; see xreal-investigation.md. */ internal object XrealProtocol { const val HOST = "169.254.2.1" const val REPORT_PORT = 52998 @@ -46,7 +45,7 @@ internal object XrealProtocol { fun sample(frame: Frame): Sample? { if (frame.command != REPORT || frame.body.size != 128) return null val b = ByteBuffer.wrap(frame.body).order(ByteOrder.LITTLE_ENDIAN) - if (b.getInt(24) != 11) return null // Magnetometer reports are a different type. + if (b.getInt(24) != 11) return null val time = b.getLong(8) val gyro = Vec3(b.getFloat(28), b.getFloat(32), b.getFloat(36)) val accel = Vec3(b.getFloat(40), b.getFloat(44), b.getFloat(48)) @@ -64,7 +63,6 @@ internal object XrealProtocol { .putInt(4 + payload.size).putInt(transaction or Int.MIN_VALUE).put(payload).array() } - /** No acknowledgement is inferred from a write; match both command and transaction. */ fun response(frame: Frame, command: Int, transaction: Int): ByteArray? { if (frame.command != command || frame.body.size < 4 || ByteBuffer.wrap(frame.body).int != transaction) return null diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/AutoDriverTrial.kt b/app/src/main/java/com/droiddeck/launcher/gpu/AutoDriverTrial.kt index a63a80249..fc55247a9 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/AutoDriverTrial.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/AutoDriverTrial.kt @@ -48,10 +48,6 @@ object AutoDriverTrial { fun bad(context: Context): Set = prefs(context).getStringSet("bad", emptySet()).orEmpty() - /** - * Counted before the compositor loads the trial display driver, so a driver that kills the process during - * Vulkan init still counts towards a rollback; sessionStarting runs too late for that. - */ fun displayStarting(context: Context, display: String) { val p = prefs(context) if (p.getString("release", null) == null || p.getString("display", "") != display) return 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 3d493c606..c31cd348e 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/FrameGen.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/FrameGen.kt @@ -52,7 +52,6 @@ object FrameGen { fun apply(context: Context, refreshHz: Float): String? = apply(context, mode(context), refreshHz) - /** Arms [mode] in this process's compositor; the cloud player passes its own mode and frame-rate cap. */ fun apply(context: Context, mode: Mode, refreshHz: Float): String? { when (mode.engine) { ENGINE_LSFG -> { 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 d74b5a3f1..42dce3d64 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriver.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriver.java @@ -25,14 +25,6 @@ public final class LinuxVulkanDriver { return values; } - 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); - if (!m.isInstalled(value)) return value + " (imported, missing - uses the runtime default)"; - String ver = m.getDriverVersion(value); - return m.getDriverName(value) + (ver.isEmpty() ? "" : " " + ver) + " (imported)"; - } - 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/SteamVrDeviceLossLog.kt b/app/src/main/java/com/droiddeck/launcher/gpu/SteamVrDeviceLossLog.kt index 4d94b6628..8709c3a74 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/SteamVrDeviceLossLog.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/SteamVrDeviceLossLog.kt @@ -6,7 +6,6 @@ import java.io.RandomAccessFile import java.nio.file.Files import java.nio.file.attribute.BasicFileAttributes -/** Only new compositor output is evidence of a fault in the current session. */ internal class SteamVrDeviceLossLog(private val file: File) { private var identity: Any? = identity() private var position = file.length() @@ -34,7 +33,6 @@ internal class SteamVrDeviceLossLog(private val file: File) { } identity = current prefix = head - // Fault loops can produce megabytes per second. Bound work and keep the newest output. if (input.length() - position > MAX_READ) { position = input.length() - MAX_READ partial = "" diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/SteamVrRecovery.kt b/app/src/main/java/com/droiddeck/launcher/gpu/SteamVrRecovery.kt index 8c045b68e..d8a546eb5 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/SteamVrRecovery.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/SteamVrRecovery.kt @@ -11,7 +11,6 @@ import com.droiddeck.launcher.runtime.LinuxRuntime import com.droiddeck.launcher.session.SessionEvents import java.io.File -/** Process-wide cursor survives Activity recreation; a new session ignores older log errors. */ internal object SteamVrHealth { private var session: String? = null private var log: SteamVrDeviceLossLog? = null @@ -49,7 +48,6 @@ internal class SteamVrRecovery(private val activity: Activity, private val foreg .create() prompt.setOnDismissListener { countdown?.let(handler::removeCallbacks); countdown = null; dialog = null } dialog = prompt - // A touch meant for the game must not silently dismiss the recovery controls. prompt.setCanceledOnTouchOutside(false) prompt.show() if (automatic) { @@ -66,7 +64,6 @@ internal class SteamVrRecovery(private val activity: Activity, private val foreg private fun restart() { if (!foreground()) return - // Commit before crossing processes so a repeated fault cannot create a restart loop. if (!activity.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit() .putLong(LAST_ATTEMPT, System.currentTimeMillis()).commit()) return SessionEvents.record("steamvr.recovery_requested") diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/SteamVrRecoverySettings.kt b/app/src/main/java/com/droiddeck/launcher/gpu/SteamVrRecoverySettings.kt index c05cc2129..63efb3c87 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/SteamVrRecoverySettings.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/SteamVrRecoverySettings.kt @@ -5,13 +5,11 @@ import org.json.JSONObject import java.io.File internal object SteamVrRecoverySettings { - /** After a graphics crash SteamVR can quarantine our HMD, leaving a server with no VR output. */ fun unblockHeadset(file: File): Boolean { if (!file.isFile) return false val settings = JSONObject(file.readText()) val driver = settings.optJSONObject("driver_droiddeck") ?: return false if (!driver.optBoolean("blocked_by_safe_mode")) return false - // Preserve every other setting, including safe mode and blocks on other drivers. driver.put("blocked_by_safe_mode", false) val atomic = AtomicFile(file) val output = atomic.startWrite() diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/SteamVrRestartActivity.kt b/app/src/main/java/com/droiddeck/launcher/gpu/SteamVrRestartActivity.kt index 671132c4f..0cf01a9d6 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/SteamVrRestartActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/SteamVrRestartActivity.kt @@ -14,7 +14,6 @@ import com.droiddeck.launcher.SessionActivity import com.droiddeck.launcher.session.SessionService import org.json.JSONObject -/** A visible, private process survives destruction of the app's native graphics context. */ class SteamVrRestartActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -41,7 +40,6 @@ class SteamVrRestartActivity : Activity() { }, SystemClock::elapsedRealtime, SystemClock::sleep) if (shutdown != SteamVrRecoveryPolicy.Shutdown.COMPLETE) android.util.Log.w("SteamVrRecovery", "Restarting before the session finished stopping: $shutdown") - // Only the verified main process, never a caller-supplied PID or an external app. Process.killProcess(main.pid) val exited = SystemClock.elapsedRealtime() + 5000 while (java.io.File("/proc/${main.pid}").exists() && SystemClock.elapsedRealtime() < exited) SystemClock.sleep(50) diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/TpUbwcHint.kt b/app/src/main/java/com/droiddeck/launcher/gpu/TpUbwcHint.kt index 893d569a6..0ac66004b 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/TpUbwcHint.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/TpUbwcHint.kt @@ -13,9 +13,6 @@ object TpUbwcHint { @Volatile var activeMode: String? = null private set - // On Adreno 740, TPL1_DBG_ECO_CNTL1.TP_UBWC_FLAG_HINT is shared by every driver on the GPU and must - // match the system driver. A mismatch corrupts Android UI. - // Samsung's One UI firmware sets it whatever blob version it reports. internal fun features(model: Int, systemDriverVersion: String?, samsung: Boolean = false, mode: String = SessionPrefs.TP_UBWC_AUTO): String? { if (model != 740) return null diff --git a/app/src/main/java/com/droiddeck/launcher/handtracking/TrackingPack.kt b/app/src/main/java/com/droiddeck/launcher/handtracking/TrackingPack.kt index 85a1e3844..d58a5a881 100644 --- a/app/src/main/java/com/droiddeck/launcher/handtracking/TrackingPack.kt +++ b/app/src/main/java/com/droiddeck/launcher/handtracking/TrackingPack.kt @@ -8,7 +8,6 @@ import java.io.FileInputStream import java.nio.ByteBuffer import java.nio.channels.FileChannel -/** Hand-tracking models and the OpenCV/MediaPipe native libraries, downloaded on first use. */ internal object TrackingPack { const val HAND = "hand_landmarker.task" const val HAND_LITE = "hand_landmarker_lite.task" @@ -32,10 +31,6 @@ internal object TrackingPack { fun model(context: Context, name: String): ByteBuffer = FileInputStream(file(context, name)).use { it.channel.map(FileChannel.MapMode.READ_ONLY, 0, it.channel.size()) } - /** - * Loads OpenCV and MediaPipe from the pack. MediaPipe's own System.loadLibrary then resolves to the - * already-loaded library by soname, so this must run before any MediaPipe task class initializes. - */ fun loadNative(context: Context): Boolean { if (nativeLoaded) return true synchronized(lock) { @@ -50,7 +45,6 @@ internal object TrackingPack { } } - /** Blocks the calling worker thread until the pack is usable, starting the download if needed. */ fun awaitNative(context: Context, running: () -> Boolean, onState: (AddonPack.State) -> Unit): Boolean = pack.await(context, running, onState) && loadNative(context) } diff --git a/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudConfig.kt b/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudConfig.kt index 7a565cb95..86e9f8f39 100644 --- a/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudConfig.kt +++ b/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudConfig.kt @@ -3,12 +3,7 @@ package com.droiddeck.launcher.hud import android.content.Context import com.droiddeck.launcher.session.SessionPrefs -/** DroidDeck HUD's settings as one "key=value,…" string (DroidDeckHudView.applyConfig), stored in SessionPrefs. */ internal object DroidDeckHudConfig { - /** - * Bannerlator's defaults (DeckOverlaySettings), with the FPS graph from the old DroidDeck HUD. The HUD starts - * locked, so like the old HUD it lets touches through until it is unlocked to move it. - */ val DEFAULTS: Map = linkedMapOf( "hudSize" to "minimal", "hudLocked" to "1", "hudSnap" to "1", "hudScale" to "100", "hudDock" to "none", "hudOpacity" to "80", "hudColor" to "mid", "hudOutline" to "40", "hudOutlineAccent" to "1", @@ -20,7 +15,6 @@ internal object DroidDeckHudConfig { "tempUnit" to "c", "tempBands" to "1", "tempAuto" to "1", "tempRedCpu" to "90", "tempRedGpu" to "90", "tempRedBat" to "48", ) - /** Switches Bannerlator sets together (DeckOverlaySettings twins). */ private val TWINS = mapOf("showCPUUsage" to "showCPULoad", "showTemp" to "showBatteryTemp", "showEngine" to "showRenderer") fun load(context: Context): MutableMap { diff --git a/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudHost.kt b/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudHost.kt index 296e0487f..a3c217e4b 100644 --- a/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudHost.kt +++ b/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudHost.kt @@ -19,10 +19,6 @@ import com.droiddeck.launcher.wayland.WaylandCompositor import java.io.File import kotlin.math.roundToInt -/** - * DroidDeck HUD on the session screen, in place of the old text HUD: the view, the frame feed from gamescope - * (MangoHud's frame times) and MangoHud's readings, started with the session's first frame. - */ class DroidDeckHudHost(context: Context) { private val context = context.applicationContext private val handler = Handler(Looper.getMainLooper()) @@ -36,8 +32,6 @@ class DroidDeckHudHost(context: Context) { private var polling = false private var wantX = Float.NaN private var wantY = Float.NaN - // With snapping on, the side the HUD keeps to (0 = left, 1 = right), so it stays against that side when its - // size changes. NaN = not snapped. private var anchorX = Float.NaN private var snaps = true private var snapping = false @@ -51,13 +45,10 @@ class DroidDeckHudHost(context: Context) { var onPresentingWindowChanged: (() -> Unit)? = null - /** The frame rate the HUD shows; null while the HUD is off and not counting frames. */ val liveFps: Float? get() = if (polling) feed.stats.fps else null - /** The HUD itself changed a setting: a tap cycles the size, a long press locks it. */ var onConfigChanged: (() -> Unit)? = null - /** Screen point → whether an on-screen control takes a touch there; a locked HUD lets those through. */ var passesTouchAt: ((rawX: Float, rawY: Float) -> Boolean)? = null private val listener = object : WaylandCompositor.GameListener { @@ -86,7 +77,6 @@ class DroidDeckHudHost(context: Context) { get() = SessionPrefs.hudEnabled(context) && !File(Environment.getExternalStorageDirectory(), "Download/droiddeck-no-hud").exists() - /** Adds the HUD to [root] at [index] (above the game surface, under the session overlay). */ fun attach(root: FrameLayout, index: Int) { if (view != null) return val hud = DroidDeckHudView(root.context) @@ -171,13 +161,11 @@ class DroidDeckHudHost(context: Context) { view?.visibility = View.GONE } - /** Picture-in-picture and similar: keep running, draw nothing. */ fun setHidden(hide: Boolean) { hidden = hide applyShown() } - /** The session screen is stopped (another app in front): draw nothing and stop reading sensors. */ fun setBackgrounded(stopped: Boolean) { backgrounded = stopped applyShown() @@ -188,7 +176,6 @@ class DroidDeckHudHost(context: Context) { val show = !hidden && !backgrounded view?.visibility = if (show) View.VISIBLE else View.GONE showing = show - // Frames are read once per display frame only while the HUD is drawn. if (show != polling) { polling = show if (show) Choreographer.getInstance().postFrameCallback(frameCallback) @@ -196,14 +183,11 @@ class DroidDeckHudHost(context: Context) { } } - /** A sensor report (DroidDeck HUD's diagnostics) in Downloads/DroidDeck; null when it can't be written. */ fun exportDiagnostics(): File? = runCatching { val dir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "DroidDeck") dir.mkdirs() val stamp = java.text.SimpleDateFormat("yyyyMMdd-HHmmss", java.util.Locale.US).format(java.util.Date()) val file = File(dir, "HUD-diagnostics-$stamp.txt") - // Its own reader: the HUD's CPU load is the change since its previous sample, which an export must - // not move. Two samples half a second apart, MangoHud's sampling period. val metrics = HudMetrics(context) metrics.setMangoReadings(MangoReadings(File(context.filesDir, "session"), LinuxRuntime.rootDir(context))) metrics.snapshot() @@ -243,17 +227,14 @@ class DroidDeckHudHost(context: Context) { return if (stats.size > 3 && stats[3] > 1f) stats[3] else 0f } - /** The edge the HUD was dropped over, once a good part of it hangs past that edge; null when it stays. */ private fun dockEdgeFor(v: View): DroidDeckHudDock? { val over = overhang(v) ?: return null return over.key.takeIf { over.value >= DOCK_OVERHANG } } - /** The edge the HUD hangs furthest past, with how much of it does, as a share of its own size. */ private fun overhang(v: View): Map.Entry? { val p = v.parent as? View ?: return null if (p.width <= 0 || p.height <= 0 || v.width <= 0 || v.height <= 0) return null - // The Strip spans the screen and is short: measure its pull in a set distance rather than its own size. val strip = (v as? DroidDeckHudView)?.isStrip == true val density = v.resources.displayMetrics.density val w = if (strip) minOf(v.width.toFloat(), STRIP_PULL_ACROSS_DP * density) else v.width.toFloat() @@ -266,7 +247,6 @@ class DroidDeckHudHost(context: Context) { ).maxByOrNull { it.value } } - /** The tab follows the finger along its edge and away from it, staying on screen. */ private fun dragTab(hud: DroidDeckHudView, x: Float, y: Float) { val p = hud.parent as? View ?: return stopMotion(hud) @@ -275,7 +255,6 @@ class DroidDeckHudHost(context: Context) { hud.y = y.coerceIn(0f, (p.height - hud.height).toFloat().coerceAtLeast(0f)) } - /** A tab pulled out by more than its own size comes out as the HUD there; a shorter pull slides it back onto its edge. */ private fun dropTab(hud: DroidDeckHudView) { val p = hud.parent as? View ?: return val maxX = (p.width - hud.width).toFloat().coerceAtLeast(0f) @@ -298,13 +277,11 @@ class DroidDeckHudHost(context: Context) { .withEndAction { snapping = false }.start() } - /** Stops the HUD's running move, and the per-frame listener a dock animation hangs on it, which outlives a cancel. */ private fun stopMotion(hud: DroidDeckHudView) { hud.animate().setUpdateListener(null) hud.animate().cancel() } - /** Glides the HUD from (fromX, fromY) to where it now is, back at full size. */ private fun glideFrom(hud: DroidDeckHudView, fromX: Float, fromY: Float) { if (Motion.scale == 0f || (fromX == hud.x && fromY == hud.y && hud.scaleX == 1f && hud.scaleY == 1f)) { hud.scaleX = 1f; hud.scaleY = 1f @@ -317,7 +294,6 @@ class DroidDeckHudHost(context: Context) { .withEndAction { snapping = false; keepOnScreen(hud) }.start() } - /** A drop short of docking fades the tab preview back out. */ private fun fadeDockPreview(hud: DroidDeckHudView) { val from = hud.dockPreviewBlend if (from <= 0f) return @@ -331,10 +307,6 @@ class DroidDeckHudHost(context: Context) { } } - /** - * Dropped, the HUD glides to the nearer side of the screen at the height it was let go, as a picture-in-picture - * window does, carried to the side a fling throws it toward. - */ private fun snapToAnchor(hud: DroidDeckHudView, vx: Float) { val p = hud.parent as? View ?: return val maxX = (p.width - hud.width).toFloat().coerceAtLeast(0f) @@ -354,10 +326,6 @@ class DroidDeckHudHost(context: Context) { .withEndAction { snapping = false; keepOnScreen(hud) }.start() } - /** - * Dragged past an edge, the HUD shrinks around the finger toward the tab it docks as, reaching the tab's - * size where a drop would dock it, so the drop finishes a contraction the drag already started. - */ private fun shrinkPastEdge(hud: DroidDeckHudView, rawX: Float, rawY: Float) { if (hud.dock != DroidDeckHudDock.NONE) return val p = hud.parent as? View ?: return @@ -372,7 +340,6 @@ class DroidDeckHudHost(context: Context) { hud.pivotX = rawX - at[0] - hud.x hud.pivotY = rawY - at[1] - hud.y if (hud.isStrip) { - // The bar draws in sideways toward the tab's width; scaled evenly it would thin to a line. hud.scaleX = 1f + (tabW / hud.width - 1f) * e hud.scaleY = 1f return @@ -382,7 +349,6 @@ class DroidDeckHudHost(context: Context) { hud.scaleX = s; hud.scaleY = s } - /** Tucks the HUD against [edge] as its FPS tab, or with [DroidDeckHudDock.NONE] brings it back out from where it was docked. */ private fun setDock(hud: DroidDeckHudView, edge: DroidDeckHudDock) { if (edge == hud.dock) return animateDock(hud, edge) { commitDock(hud, edge) } @@ -405,10 +371,6 @@ class DroidDeckHudHost(context: Context) { onConfigChanged?.invoke() } - /** - * Where the HUD comes out of its tab, decided before it expands so it grows straight there: where the tab is - * (against its edge, or wherever it was pulled to), and with snapping on, against the nearer side. - */ private fun settleOutOfTab(hud: DroidDeckHudView, tabX: Float, tabY: Float) { val p = hud.parent as? View val maxX = if (p == null) Float.MAX_VALUE else (p.width - hud.contentWidth).coerceAtLeast(0f) @@ -419,11 +381,6 @@ class DroidDeckHudHost(context: Context) { SessionPrefs.setDroidDeckHudPosition(context, wantX, wantY) } - /** - * One shape contracts into the tab, and the tab expands back out to the HUD: the view is scaled onto the - * tab's exact rectangle, and the layouts swap at that same size and place. Timing and curve are the - * launcher's own ([Motion]), and off when animations are off. - */ private fun animateDock(hud: DroidDeckHudView, edge: DroidDeckHudDock, commit: () -> Unit) { stopMotion(hud) previewFade?.cancel(); previewFade = null @@ -436,7 +393,6 @@ class DroidDeckHudHost(context: Context) { commit(); return } if (edge != DroidDeckHudDock.NONE) { - // Continue from what the drag left on screen: the shrunk HUD, and the tab where it is along the edge. val left = hud.x + hud.pivotX * (1f - hud.scaleX); val top = hud.y + hud.pivotY * (1f - hud.scaleY) val shownW = hud.width * hud.scaleX; val shownH = hud.height * hud.scaleY hud.pivotX = 0f; hud.pivotY = 0f @@ -480,11 +436,9 @@ class DroidDeckHudHost(context: Context) { } } - /** The pivot, in parent coordinates, that scaling by [scale] takes a span starting at [from] onto one starting at [to]. */ private fun pivotFor(from: Float, to: Float, scale: Float): Float = if (scale > 0.999f) from else (to - from * scale) / (1f - scale) - /** Where a [w] x [h] HUD or tab sits once [keepOnScreen] has settled it against [dock]. */ private fun settled(p: View, dock: DroidDeckHudDock, w: Float, h: Float): Pair { val maxX = (p.width - w).coerceAtLeast(0f); val maxY = (p.height - h).coerceAtLeast(0f) var x = wantX.coerceIn(0f, maxX); var y = wantY.coerceIn(0f, maxY) @@ -498,9 +452,6 @@ class DroidDeckHudHost(context: Context) { return Pair(x, y) } - // The HUD is clamped from where the user left it (wantX/wantY, set on a drop or restore), not from where - // it is drawn: a window that shrinks and grows back (picture-in-picture, a fold) must not leave it in the - // corner the small window pushed it to. private fun keepOnScreen(v: View) { if (snapping) return val p = v.parent as? View ?: return @@ -528,27 +479,19 @@ class DroidDeckHudHost(context: Context) { } companion object { - /** How much of the HUD, as a share of its own size, must hang past a screen edge for it to dock there. */ private const val DOCK_OVERHANG = 0.4f - /** How far ahead a fling carries the HUD when picking the spot it snaps to, as seconds of its speed. */ private const val FLING_REACH_S = 0.18f - /** How far toward docking a drag goes, as a share of the way, before the tab starts to fade in over the HUD. */ private const val PREVIEW_FROM = 0.55f - /** How far the Strip is pulled sideways, or off its edge, for its overhang to count as its whole size. */ private const val STRIP_PULL_ACROSS_DP = 600f private const val STRIP_PULL_OFF_DP = 150f - /** The share of the expand animation over which the tab fades out to the HUD. */ private const val EXPAND_FADE = 0.5f private val EASE = PathInterpolator(0.2f, 0.8f, 0.2f, 1f) - /** True while the HUD is on screen; DroidDeck keeps sampling VRAM for it (GpuMemComponent). */ @Volatile @JvmStatic var showing = false private set - /** "Adreno (TM) 750" as the driver names it, "Adreno 750" as DroidDeck does elsewhere. */ internal fun cleanGpuName(name: String): String = name.replace("(TM)", "").replace(Regex("\\s+"), " ").trim() - /** The Proton build of a game process: the first STEAM_COMPAT_TOOL_PATHS entry's folder. */ internal fun protonName(pid: Int): String? = runCatching { val environ = File("/proc/$pid/environ").readBytes().toString(Charsets.UTF_8).split('\u0000') val paths = environ.firstOrNull { it.startsWith("STEAM_COMPAT_TOOL_PATHS=") }?.substringAfter('=') ?: return null diff --git a/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudModels.kt b/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudModels.kt index ab61d670b..9f59512e5 100644 --- a/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudModels.kt +++ b/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudModels.kt @@ -8,16 +8,11 @@ * link to https://github.com/The412Banner/FusionHUD in the project documentation * AND in the in-app credits/about screen. * - * Ported to DroidDeck from FusionHUD (Bannerlator's HUD); DroidDeck changes are marked in the code. + * Ported to DroidDeck from FusionHUD (Bannerlator's HUD) and modified for DroidDeck. */ package com.droiddeck.launcher.hud -/** - * Size modes for the DroidDeck HUD (the 4th selectable in-game overlay style). One shared color-coded - * visual language, four amounts of detail — see the approved mockup. The config value is the lower-case - * token stored under the `hudSize` key; the label is the user-facing string. - */ enum class DroidDeckHudSize(val token: String, val label: String) { FULL("full", "Full"), TILES("tiles", "Tiles"), @@ -26,18 +21,11 @@ enum class DroidDeckHudSize(val token: String, val label: String) { MEGA("mega", "Mega"), STRIP("strip", "Strip"); - /** Next size in the tap-cycle: Full → Tiles → Pill → Minimal → Mega → Strip → Full. */ fun next(): DroidDeckHudSize { val all = values() return all[(ordinal + 1) % all.size] } - /** - * The metric toggle chips this size actually renders — the SINGLE source of truth queried by BOTH - * config surfaces ({@code FpsCounterConfigDialog} + in-game {@code XServerDrawer} HudContent) so the - * chip list can't drift from what the view draws. Chip ids are the chip LABELS used in the UI. - * (Hiding a chip is purely UI; buildConfig() still emits every key — strip-invariant.) - */ fun supportedChips(): Set = when (this) { MINIMAL -> MINIMAL_CHIPS PILL -> PILL_CHIPS @@ -49,11 +37,6 @@ enum class DroidDeckHudSize(val token: String, val label: String) { fun from(token: String?): DroidDeckHudSize = values().firstOrNull { it.token.equals(token, ignoreCase = true) } ?: FULL - // Chip-label ids (must match the labels the config UIs build). "Clock" is a small subtle - // corner readout available in EVERY size, so it's in all sets. - // The API/engine label ("Engine" chip) shows on the FPS row in EVERY size, so it's in all sets. - // "Wrapper" and "DX ver" are Mega-only: the wrapper appears solely below Mega's frametime graph, - // and "DX ver" gates the DX-version suffix on Mega's FPS-row engine label. val MINIMAL_CHIPS = setOf("FPS", "FPS graph", "0.01% low", "FPS .1", "Engine", "Clock") val PILL_CHIPS = setOf("FPS", "FPS graph", "GPU", "CPU", "VRAM", "RAM", "Battery", "GPU model", "FPS .1", "Engine", "Clock") val STANDARD_CHIPS = setOf( @@ -67,7 +50,6 @@ enum class DroidDeckHudSize(val token: String, val label: String) { } } -/** The screen edge the HUD is tucked against, where only its FPS tab shows ([NONE] = floating free). */ enum class DroidDeckHudDock(val token: String) { NONE("none"), LEFT("left"), RIGHT("right"), TOP("top"), BOTTOM("bottom"); @@ -76,21 +58,12 @@ enum class DroidDeckHudDock(val token: String) { } } -/** - * The Wayland HDR state the DroidDeck HUD shows on its own line, directly under the latency · display-server - * line (the host sets it with [DroidDeckHudView.setHdrState]). [NONE] = no line at all: every session whose - * HDR gate is closed (X11, SDR screens, HDR off) keeps the HUD exactly as it was. - */ object DroidDeckHudHdr { const val NONE = 0 - const val ON = 1 // HDR frames on screen with HDR headroom - const val NO_HEADROOM = 2 // HDR frames on screen, but the display gives them no headroom (5 s+) - const val OFF = 3 // the drawer's HDR output switch is off (tone-mapped to SDR) - const val READY = 4 // HDR open for the session, no HDR frames on screen right now - const val TONEMAPPED = 5 // HDR output on, but the frames are shown tone-mapped (frame generation - // on a screen that offers no HDR10 swapchain) - const val NOT_ON_THIS_SCREEN = 6 // HDR open for the session, but the screen the game is on NOW - // reports no HDR10 (the TV was unplugged mid-game). The offer to the - // game cannot be withdrawn once made, so the gate stays open for the - // session — but "ready" would be a lie on a panel that cannot show it. + const val ON = 1 + const val NO_HEADROOM = 2 + const val OFF = 3 + const val READY = 4 + const val TONEMAPPED = 5 + const val NOT_ON_THIS_SCREEN = 6 } diff --git a/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudView.kt b/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudView.kt index 919e6ea92..80d3deb88 100644 --- a/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudView.kt +++ b/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudView.kt @@ -8,7 +8,7 @@ * link to https://github.com/The412Banner/FusionHUD in the project documentation * AND in the in-app credits/about screen. * - * Ported to DroidDeck from FusionHUD (Bannerlator's HUD); DroidDeck changes are marked in the code. + * Ported to DroidDeck from FusionHUD (Bannerlator's HUD) and modified for DroidDeck. */ package com.droiddeck.launcher.hud @@ -53,28 +53,12 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull -/** - * DroidDeck HUD — the 4th selectable in-game overlay. One color-coded visual language rendered at four - * SIZE modes (Full / Tiles / Pill / Minimal / Mega), all driven by [FrameStats] (gamescope's frame times - * read by MangoHud's rules) + a single cached [HudMetrics.Snapshot]. Pure Canvas drawing. - * - * DroidDeck: the readouts refresh every 500 ms (MangoHud's sampling period) on a Main-dispatcher - * coroutine, collecting the snapshot on Dispatchers.IO; the frame graph is redrawn whenever new frames - * arrive ([onFrames]), as MangoHud draws it. - * - * Gestures go through [HudLockController]: long-press toggles the position lock (with a lock/unlock - * badge fade), a tap cycles the size (Full→Tiles→Pill→Minimal→Full), and a drag repositions — tap + - * drag are frozen while locked. A locked HUD lets a touch through where an on-screen control under it - * takes it ([passesTouchAt]); everywhere else it keeps the touch, so a long press there unlocks it. - */ class DroidDeckHudView(context: Context) : View(context) { private var frames: FrameStats? = null internal fun setFrameStats(stats: FrameStats?) { frames = stats } internal fun setMangoReadings(readings: MangoReadings?) { metrics.setMangoReadings(readings) } - // Frames per second actually reaching the panel while DroidDeck's frame generation adds frames the - // game never made; 0 = nothing is adding frames, and the readout is the game's rate alone. private var presentedFps = 0f fun setPresentedFps(fps: Float) { presentedFps = fps } @@ -85,7 +69,6 @@ class DroidDeckHudView(context: Context) : View(context) { private val density = resources.displayMetrics.density - // ---- Config ----------------------------------------------------------- private var size = DroidDeckHudSize.FULL private var scale = 1f private var bgOpacity = 0.8f @@ -109,8 +92,7 @@ class DroidDeckHudView(context: Context) : View(context) { private var showGraph = false private var showLow001 = true private var fpsDecimal = true - private var showClockTime = true // subtle corner/footer clock, available in EVERY size - // Mega-only rows + private var showClockTime = true private var showPerCore = true private var showSwap = true private var showNet = true @@ -121,21 +103,15 @@ class DroidDeckHudView(context: Context) : View(context) { private var showSession = true private var engineLabel = "" - private var displayServer = "" // "X11" / "Wayland": which display server the game runs on - private var hdrState = DroidDeckHudHdr.NONE // Wayland HDR, on its own line under latency · display server + private var displayServer = "" + private var hdrState = DroidDeckHudHdr.NONE private var gpuModel = "" - // The GPU name a Wayland session reports to games instead of the real adapter ("" = none). It has - // to travel separately from gpuModel: Wayland hands the spoof to DXVK and leaves the Vulkan device - // alone, so everything the host reads back is still the real chip. On X11 the driver wrapper renames - // the device itself, so there gpuModel already IS the spoofed name and this stays empty. private var gpuSpoofName = "" - // Stack-layer version strings (Mega bottom band + DX version on the engine row); fed by the host. - private var wineVersion = "" // "Proton 10.0-4" - private var graphicsWrapper = "" // graphics-driver wrapper package, e.g. "GameNative", "bcn_layer 20260719" - private var dxvkVersion = "" // DXVK version, appended to a DXVK engine row - private var vkd3dVersion = "" // VKD3D version, appended to a VKD3D engine row + private var wineVersion = "" + private var graphicsWrapper = "" + private var dxvkVersion = "" + private var vkd3dVersion = "" - // ---- Live data (updated on the refresh coroutine, read on draw) -------- private var snap: HudMetrics.Snapshot? = null private var fpsNow = 0f private var fpsAvg = 0f @@ -146,25 +122,17 @@ class DroidDeckHudView(context: Context) : View(context) { private val graphPath = Path() private var resolution = "" - // DroidDeck look: the theme's main colour for the FPS label, graph and accent outline, the theme's - // surface behind the readouts, a DroidDeck header (wordmark) on Full/Tiles/Minimal/Mega and the logo - // chip on the Pill. private var accent = 0xFF1A9FFF.toInt() private var surfaceRgb = 0x121417 private var headerMark: RectF? = null private var chipRect: RectF? = null - // Pushed past a screen edge, the HUD shrinks to a tab with the FPS that stays at that edge. var dock = DroidDeckHudDock.NONE private set private var tabRect: RectF? = null - // The Strip is a bar across the whole screen, kept against the top or bottom edge. private var stripRect: RectF? = null private val tabPath = Path() - // Every size sits in the DroidDeck mark's D: a straight left side, round on the - // right. Its fixed layers (fill, header band, watermark, glowing outline) go into a bitmap, because - // Android before 9 skips glow on shapes drawn with hardware acceleration. private var dPanel: RectF? = null private var dRadius = 0f private var dDividerTop = -1f @@ -172,7 +140,6 @@ class DroidDeckHudView(context: Context) : View(context) { private var dFrameKey = "" private val glowPaint = Paint(Paint.ANTI_ALIAS_FLAG) - // ---- Colors (mockup) -------------------------------------------------- private val colGpu = 0xFF5EE08A.toInt() private val colCpu = 0xFF58A6FF.toInt() private val colVram = 0xFFC98BFF.toInt() @@ -183,10 +150,9 @@ class DroidDeckHudView(context: Context) : View(context) { private val colValue = 0xFFF2F5F9.toInt() private val colDim = 0xFF9AA4B2.toInt() private val colLo = 0xFFE4E8EE.toInt() - private val colDisp = 0xFF4DD0E1.toInt() // display server (X11 / Wayland) - private val colHdr = 0xFFFFD54F.toInt() // HDR on screen + private val colDisp = 0xFF4DD0E1.toInt() + private val colHdr = 0xFFFFD54F.toInt() - // ---- Paints ----------------------------------------------------------- private val measurePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { typeface = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD) } @@ -205,16 +171,10 @@ class DroidDeckHudView(context: Context) : View(context) { typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD) } - // ---- Built geometry --------------------------------------------------- private class Glyph(val x: Float, val baseline: Float, val text: String, val color: Int, val sizePx: Float, val sans: Boolean = false, val glow: Boolean = false) { fun moved(dx: Float, dy: Float) = Glyph(x + dx, baseline + dy, text, color, sizePx, sans, glow) } private class Span(val text: String, val color: Int, val sizePx: Float, val glow: Boolean = false) - /** - * A "label + value-run" row for the aligned grid sizes (Full / Mega). When [inline] is true the row - * opts OUT of the shared label column — its value run is placed directly after the label — so a long - * label (the graphics-wrapper on the FPS row) can't widen the column and shove every value right. - */ private class HudRow(val label: Span, val vals: List, val inline: Boolean = false) private val glyphs = ArrayList() private val tileRects = ArrayList() @@ -222,7 +182,6 @@ class DroidDeckHudView(context: Context) : View(context) { private var contentW = 0f private var contentH = 0f - // ---- Listeners -------------------------------------------------------- private var onMovedListener: BiConsumer? = null private var onSizeCycledListener: Consumer? = null private var onLockChangedListener: Consumer? = null @@ -232,13 +191,10 @@ class DroidDeckHudView(context: Context) : View(context) { private var onDockChangedListener: Consumer? = null private var onScaledListener: IntConsumer? = null private var onDraggedListener: BiConsumer? = null - /** The HUD moved under the finger, at that screen point. */ fun setOnDraggedListener(l: BiConsumer?) { onDraggedListener = l } private var onFlungListener: BiConsumer? = null - /** A drag ended in a fling, at this velocity (pixels per second), just before the move is reported. */ fun setOnFlungListener(l: BiConsumer?) { onFlungListener = l } fun setOnDockChangedListener(l: Consumer?) { onDockChangedListener = l } - /** A pinch settled on this size, in percent. */ fun setOnScaledListener(l: IntConsumer?) { onScaledListener = l } private val lockController = HudLockController(context, this, object : HudLockController.Callbacks { @@ -256,7 +212,6 @@ class DroidDeckHudView(context: Context) : View(context) { override fun onPinchEnd() { onScaledListener?.accept((scale * 100f).roundToInt()) } }) - // A docked tab always takes touches, locked or not: a tap brings the HUD back, a drag moves the tab. private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop private var tabDownRawX = 0f private var tabDownRawY = 0f @@ -266,10 +221,8 @@ class DroidDeckHudView(context: Context) : View(context) { private var onTabDraggedListener: BiConsumer? = null private var onTabDroppedListener: Runnable? = null private var onTabDragCancelledListener: Runnable? = null - /** The docked tab is dragged: where the finger would put it (x, y in the parent). */ fun setOnTabDraggedListener(l: BiConsumer?) { onTabDraggedListener = l } fun setOnTabDroppedListener(l: Runnable?) { onTabDroppedListener = l } - /** The system took the tab's drag away: the tab goes back to where it was docked. */ fun setOnTabDragCancelledListener(l: Runnable?) { onTabDragCancelledListener = l } private fun onTabTouch(event: MotionEvent): Boolean { @@ -296,7 +249,6 @@ class DroidDeckHudView(context: Context) : View(context) { return true } - // On the way into the tab or out of it, the tab fades in over the HUD at the tab's true size on screen. private var previewEdge = DroidDeckHudDock.NONE private var previewBlend = 0f internal fun setDockPreview(edge: DroidDeckHudDock, blend: Float) { @@ -307,32 +259,15 @@ class DroidDeckHudView(context: Context) : View(context) { } internal val dockPreviewBlend: Float get() = previewBlend - // ---- Public surface (symmetric with the other overlays) --------------- fun setEngineLabel(s: String?) { if ((s ?: "") != engineLabel) { engineLabel = s ?: ""; post { rebuildAndInvalidate() } } } fun setGpuModel(s: String?) { if ((s ?: "") != gpuModel) { gpuModel = s ?: ""; post { rebuildAndInvalidate() } } } - /** This session's Wayland GPU-name spoof (null / empty = none, the default): the GPU row names it - * in place of the real model, marked so it can't be read as the chip that is really rendering. */ - fun setGpuSpoofName(s: String?) { gpuSpoofName = s ?: ""; post { rebuildAndInvalidate() } } - fun setDisplayServer(s: String?) { displayServer = s ?: ""; post { rebuildAndInvalidate() } } - /** [DroidDeckHudHdr] code; [DroidDeckHudHdr.NONE] (the default) draws no HDR line anywhere. Any thread. */ - fun setHdrState(state: Int) { - val v = if (state in DroidDeckHudHdr.ON..DroidDeckHudHdr.NOT_ON_THIS_SCREEN) state else DroidDeckHudHdr.NONE - post { if (v != hdrState) { hdrState = v; rebuildAndInvalidate() } } - } fun setWineVersion(s: String?) { if ((s ?: "") != wineVersion) { wineVersion = s ?: ""; post { rebuildAndInvalidate() } } } - fun setGraphicsWrapper(s: String?) { graphicsWrapper = s ?: ""; post { rebuildAndInvalidate() } } - fun setDxWrapper(dxvk: String?, vkd3d: String?) { - dxvkVersion = dxvk ?: ""; vkd3dVersion = vkd3d ?: ""; post { rebuildAndInvalidate() } - } - /** The game's output from gamescope ("1280x720 @ 144Hz"), as MangoHud shows it; "" = this display. */ fun setResolution(s: String?) { resolution = s ?: "" } private val periodDone = Channel(Channel.CONFLATED) private var seenFpsUpdates = 0L - /** New frames arrived: redraw the graph (MangoHud redraws it every frame), and the readouts when a - * sampling period completed, at the moment MangoHud redraws its own. */ fun onFrames() { val updates = frames?.fpsUpdates ?: 0L if (updates != seenFpsUpdates) { @@ -342,10 +277,6 @@ class DroidDeckHudView(context: Context) : View(context) { if (graphRect != null) invalidate() } - /** The HUD has no orientation (tap cycles size instead); kept for a symmetric host surface. */ - fun setVertical(vertical: Boolean) { /* no-op */ } - fun isVertical(): Boolean = false - private fun cycleSize() { size = size.next() onSizeCycledListener?.accept(size.token) @@ -372,7 +303,7 @@ class DroidDeckHudView(context: Context) : View(context) { showGraph = cfg.get("showFPSGraph", "0") == "1" showLow001 = cfg.get("showLow001", "1") == "1" fpsDecimal = cfg.get("fpsDecimal", "1") == "1" - showClockTime = cfg.get("showClock", "1") == "1" // the clock defaults ON + showClockTime = cfg.get("showClock", "1") == "1" showPerCore = cfg.get("showPerCore", "1") == "1" showSwap = cfg.get("showSwap", "1") == "1" showNet = cfg.get("showNet", "1") == "1" @@ -397,7 +328,6 @@ class DroidDeckHudView(context: Context) : View(context) { rebuildAndInvalidate() } - // ---- Lifecycle: self-refresh ------------------------------------------ override fun onAttachedToWindow() { super.onAttachedToWindow() if (isShown) startUpdates() @@ -408,8 +338,6 @@ class DroidDeckHudView(context: Context) : View(context) { super.onDetachedFromWindow() } - // The host hides the HUD with GONE (HUD off, picture-in-picture) and keeps it attached: read the - // sensors only while it shows. override fun onVisibilityChanged(changedView: View, visibility: Int) { super.onVisibilityChanged(changedView, visibility) if (isAttachedToWindow && isShown) startUpdates() else stopUpdates() @@ -438,13 +366,11 @@ class DroidDeckHudView(context: Context) : View(context) { graphMaxMs = stats?.graphMaxMs() ?: 0f onRefresh?.run() rebuildAndInvalidate() - // Without frames (a still screen) MangoHud redraws nothing; the HUD keeps refreshing. withTimeoutOrNull(REFRESH_MS) { periodDone.receive() } } } } - /** Runs on the main thread just before each readout refresh, for the host's labels. */ var onRefresh: Runnable? = null private fun applyTheme() { @@ -459,10 +385,6 @@ class DroidDeckHudView(context: Context) : View(context) { colGraph = lighten(accent, 0.3f) } - /** - * Inner Gamer turns the colour wheel inside the app's Compose UI, which is not running while the drawer is - * closed, so the HUD turns it here and shares the hue so the drawer picks up where the HUD is. - */ private fun startHueCycle() { if (hueJob?.isActive == true) return if (SessionPrefs.theme(context) != Themes.INNER_GAMER) return @@ -504,7 +426,6 @@ class DroidDeckHudView(context: Context) : View(context) { invalidate() } - // ---- Text helpers ----------------------------------------------------- private fun blend(color: Int): Int { val i = colorIntensity.coerceIn(0f, 1f) if (i >= 0.999f) return color @@ -537,27 +458,19 @@ class DroidDeckHudView(context: Context) : View(context) { private fun one(v: Float): String = if (fpsDecimal) String.format(Locale.US, "%.1f", v) else v.roundToInt().toString() - // Shows "30->118" while something downstream of the game is adding frames. - // The game's own rate stays visible next to the panel rate, because the gap - // between the two is precisely what frame generation is doing. private fun fpsText(v: Float): String = if (differs(v)) "${one(v)}\u2192${one(presentedFps)}" else one(v) - // Either direction: up = frames added (LSFG Native), down = frames lost - // between the guest and the panel, which is the failure win-fg can hit - // and which a single number hides completely. private fun differs(v: Float): Boolean = presentedFps > 0f && kotlin.math.abs(presentedFps - v) > maxOf(1f, v * 0.10f) private fun fmt1(v: Float): String = String.format(Locale.US, "%.1f", v) private fun lowText(v: Float): String? = if (v <= 0f) null else fmt1(v) - // number (white, or "—" muted when null) + unit (muted, smaller) private fun numUnit(num: String?, unit: String, numPx: Float, unitPx: Float): List = if (num == null) listOf(Span("—", colDim, numPx), Span(unit, colDim, unitPx)) else listOf(Span(num, colValue, numPx), Span(unit, colDim, unitPx)) - // "0.2GiB"/"75%"/"1938MHz" → white number + muted suffix private fun valueUnit(text: String, numPx: Float, unitPx: Float): List { val i = text.indexOfFirst { !(it.isDigit() || it == '.' || it == '-') } return if (i <= 0) listOf(Span(text, colValue, numPx)) @@ -566,7 +479,7 @@ class DroidDeckHudView(context: Context) : View(context) { private fun tempSpans(c: Int?, sensor: HudMetrics.TempSensor, numPx: Float, unitPx: Float): List { if (c == null) return emptyList() - val txt = HudMetrics.formatTemp(c.toFloat(), tempDisplay, false) // "81°C" + val txt = HudMetrics.formatTemp(c.toFloat(), tempDisplay, false) val col = HudMetrics.tempColor( c.toFloat(), metrics.resolveThresholds(sensor, tempDisplay), tempDisplay, colValue) val i = txt.indexOf('°') @@ -576,13 +489,9 @@ class DroidDeckHudView(context: Context) : View(context) { private fun gap(unitPx: Float) = Span(" ", colDim, unitPx) - /** The frame-rate number in neon, as on Minimal. */ private fun glowing(spans: List): List = spans.mapIndexed { i, s -> if (i == 0) Span(s.text, s.color, s.sizePx, glow = true) else s } - // ---- Wayland HDR state (one line of its own; nothing at all for DroidDeckHudHdr.NONE) ---- - /** The state as a full line: "HDR", "HDR (no headroom)", "HDR off", "HDR ready", "HDR tone-mapped", - * "HDR not on this screen". */ private fun hdrLine(px: Float): List = when (hdrState) { DroidDeckHudHdr.ON -> listOf(Span("HDR", colHdr, px)) DroidDeckHudHdr.NO_HEADROOM -> listOf(Span("HDR", colHdr, px), Span(" (no headroom)", colBat, px)) @@ -592,8 +501,6 @@ class DroidDeckHudView(context: Context) : View(context) { DroidDeckHudHdr.NOT_ON_THIS_SCREEN -> listOf(Span("HDR", colDim, px), Span(" not on this screen", colBat, px)) else -> emptyList() } - /** The value after an "HDR" label (Full / Mega): "on", "no headroom", "off", "ready", "tone-mapped", - * "not on this screen". */ private fun hdrValue(px: Float): List = when (hdrState) { DroidDeckHudHdr.ON -> listOf(Span("on", colHdr, px)) DroidDeckHudHdr.NO_HEADROOM -> listOf(Span("no headroom", colBat, px)) @@ -605,12 +512,7 @@ class DroidDeckHudView(context: Context) : View(context) { } private fun hdrText(): String = hdrLine(1f).joinToString("") { it.text } - // ---- GPU model (with the Wayland GPU-name spoof) ---- - /** What the GPU row names: the spoofed GPU when this session reports one, else the real model. */ private fun gpuNameText(): String = gpuSpoofName.ifBlank { gpuModel } - /** The GPU row's value run. A spoofed name carries a marker in the same warning colour the HDR line - * qualifies its state with, so the row can't be read as the chip that is really rendering: - * "GeForce GTX 1080 (spoof)", and where the line is tightest (the pill) "GeForce GTX 1080 spoof". */ private fun gpuModelSpans(px: Float, color: Int, compact: Boolean = false): List { val name = Span(gpuNameText(), color, px) if (gpuSpoofName.isBlank()) return listOf(name) @@ -619,7 +521,6 @@ class DroidDeckHudView(context: Context) : View(context) { private val inkRect = android.graphics.Rect() - // ---- Layout builders -------------------------------------------------- private fun rebuild() { glyphs.clear(); tileRects.clear(); graphRect = null headerMark = null; chipRect = null; dPanel = null; dDividerTop = -1f; tabRect = null; stripRect = null @@ -636,7 +537,6 @@ class DroidDeckHudView(context: Context) : View(context) { } } - /** The docked tab: the FPS alone, in a box square on the edge it sits against. */ private fun buildDockTab() { val numPx = sp(20f) val spans = tabSpans() @@ -649,7 +549,6 @@ class DroidDeckHudView(context: Context) : View(context) { private fun tabSpans() = listOf(Span(one(fpsNow), colValue, sp(20f)), Span("fps", colDim, sp(8.5f))) - /** The size the docked tab has, so the HUD can contract to exactly that shape before the tab replaces it. */ internal fun dockTabSize(): Pair = Pair(runWidth(tabSpans()) + 2f * sp(9f), capHeight(sp(20f)) + 2f * sp(7f)) @@ -675,18 +574,14 @@ class DroidDeckHudView(context: Context) : View(context) { canvas.drawPath(tabPath, strokePaint) } - // ---- DroidDeck panel, header and chip ----------------------------------- - /** The clock sits in the header on every size but the Pill, which keeps it under the FPS. */ private fun footerClock(): Boolean = showClockTime && size == DroidDeckHudSize.PILL private fun measureSans(text: String, sizePx: Float): Float { wordmarkPaint.textSize = sizePx; return wordmarkPaint.measureText(text) } - private fun panelInset() = sp(8f) // room for the glow around the panel - private fun panelLeft() = sp(13f) // the outline's glow sticks out of the left side + private fun panelInset() = sp(8f) + private fun panelLeft() = sp(13f) private fun contentLeft() = panelLeft() + sp(16f) - /** The DroidDeck mark, "DroidDeck" and the clock across the top, starting on [left], with the same - * space above and below the mark. Returns the header's bottom edge and its right end. */ private fun addPanelHeader(left: Float, inner: Float): Pair { val wordPx = sp(10f); val clockPx = sp(9.5f); val gapH = sp(7f) wordmarkPaint.textSize = wordPx @@ -714,11 +609,6 @@ class DroidDeckHudView(context: Context) : View(context) { return Pair(dDividerTop + max(1f, density), x) } - /** - * Moves a size's content, laid out from [pad] at the origin, into the panel: under the header, or after - * the logo chip on the Pill. Lines that start on the content's left edge are lined up by their ink, so - * the logo, labels and numbers share one edge. - */ private fun intoPanel(pad: Float, header: Boolean) { if (contentW <= 0f || contentH <= 0f) return for (i in glyphs.indices) { @@ -748,8 +638,6 @@ class DroidDeckHudView(context: Context) : View(context) { finishPanel(panel, halfRound = !header) } - /** Closes the panel around everything placed so far, widened until no line, tile or graph reaches the - * round side: a half circle on the short sizes (Pill, Minimal), large round corners on the tall ones. */ private fun finishPanel(panel: RectF, halfRound: Boolean) { val r = if (halfRound) panel.height() / 2f else min(panel.height() / 2f, sp(56f)) val margin = outlineWidth() + sp(4f) @@ -773,7 +661,6 @@ class DroidDeckHudView(context: Context) : View(context) { contentH = panel.bottom + panelInset() } - // ---- Shared small-info helpers ---------------------------------------- private fun clockTimeString(): String = DateFormat.getTimeFormat(context).format(Date()) private fun elapsedString(): String { @@ -783,31 +670,21 @@ class DroidDeckHudView(context: Context) : View(context) { else String.format(Locale.US, "%02d:%02d", m, s) } - /** - * The FPS row's API/engine label — the DX→Vulkan translator ([engineLabel]: "DXVK"/"VKD3D"/"Zink"), - * gated by the "Engine" chip. This is short and shows in EVERY size. Falls back to a bare "FPS" when - * the toggle is off or no engine label is set. (The graphics WRAPPER is separate — Mega-only, drawn - * below the frametime graph — not on the FPS row.) - */ private fun apiLabel(): String = if (showEngine && engineLabel.isNotBlank()) engineLabel else "FPS" - /** The DX-wrapper version WITH its engine name (e.g. "DXVK 2.4.1", "VKD3D 2.14"), or "" if none. - * Used for the Mega stack line below the graph. */ private fun dxVersionLabeled(): String = when { engineLabel.contains("VKD3D", true) && vkd3dVersion.isNotBlank() -> "VKD3D $vkd3dVersion" engineLabel.contains("DXVK", true) && dxvkVersion.isNotBlank() -> "DXVK $dxvkVersion" else -> "" } - /** The DX-wrapper version for the current engine, or "" (used for the Tiles "API" tile sub-line). */ private fun dxVersion(): String = when { engineLabel.contains("VKD3D", true) -> vkd3dVersion engineLabel.contains("DXVK", true) -> dxvkVersion else -> "" } - /** Resolution + refresh, e.g. "1280x720 @ 120Hz". Read from the current display (Main thread). */ private fun resolutionString(): String { if (resolution.isNotBlank()) return resolution val dm = resources.displayMetrics @@ -816,15 +693,13 @@ class DroidDeckHudView(context: Context) : View(context) { return if (hz >= 1f) "$base @ ${hz.roundToInt()}Hz" else base } - /** A subtle, low-emphasis time-of-day tucked into the bottom-right of any size. Grows contentH slightly. */ private fun addSubtleClock(pad: Float, leftAligned: Boolean = false) { if (!footerClock()) return val px = sp(9.5f) val t = clockTimeString() val w = measure(t, px) - val footerTop = contentH - pad // reuse the bottom padding band as the footer line + val footerTop = contentH - pad val baseline = footerTop - ascent(px) - // Right-aligned footer; on the Pill it sits on the left edge, under the FPS. val x = if (leftAligned) pad else (contentW - pad - w).coerceAtLeast(pad) glyphs.add(Glyph(x, baseline, t, blend(colDim), px)) contentH = footerTop + lineH(px) + pad * 0.5f @@ -878,7 +753,6 @@ class DroidDeckHudView(context: Context) : View(context) { val v = ArrayList() v += glowing(numUnit(fpsText(fpsNow), "FPS", rowPx, unitPx)) v += gap(unitPx); v += numUnit(fmt1(1000f / max(fpsNow, 1f)), "ms", rowPx, unitPx) - // Short API/engine label ("DXVK"/"VKD3D"/"Zink", else "FPS") — a normal aligned row. rows.add(HudRow(Span(apiLabel(), colFps, rowPx), v)) rows.add(HudRow(Span("AVG", colLo, rowPx), numUnit(fmt1(fpsAvg), "FPS", rowPx, unitPx))) rows.add(HudRow(Span("1%", colLo, rowPx), numUnit(lowText(lows.low1Fps), "FPS", rowPx, unitPx))) @@ -902,9 +776,6 @@ class DroidDeckHudView(context: Context) : View(context) { } if (showFPS && showGraph) { - // Frametime min/max line + green graph. "Frametime" is far wider than the short-label grid - // column, so place the stat AFTER the ACTUAL label width (not labelCol) — otherwise the two - // overlap and render as "Frametimemin:.. max:..". val ftPx = unitPx * 1.15f val baseline = y - asc placeRun(pad, baseline, listOf(Span("Frametime", colFps, ftPx))) @@ -945,8 +816,6 @@ class DroidDeckHudView(context: Context) : View(context) { if (showVram && s.vramText() != null) tiles.add(Tile("VRAM", colVram, valueUnit(s.vramText()!!, valPx, unitPx), null, false)) if (showRAM) tiles.add(Tile("RAM", colRam, numUnit(s.ramPercent.roundToInt().toString(), "%", valPx, unitPx), "${s.ramUsedText()} / ${s.ramTotalText()}", false)) - // API/engine tile — short translator name (DXVK/VKD3D/Zink) with the DX version on the sub-line; - // a normal half-width tile that naturally fills the empty slot beside RAM. Gated by "Engine". if (showEngine && engineLabel.isNotBlank()) tiles.add(Tile("API", colFps, listOf(Span(engineLabel, colValue, valPx)), dxVersion().ifBlank { null }, false)) @@ -970,7 +839,7 @@ class DroidDeckHudView(context: Context) : View(context) { var normalW = 0f for (t in tiles) if (!t.wide) normalW = max(normalW, tileW(t)) if (normalW == 0f) for (t in tiles) normalW = max(normalW, tileW(t) / 2f) - val tileH = innerPad * 2 + keyH + lineGap + valH + lineGap + subH // uniform (room for a sub line) + val tileH = innerPad * 2 + keyH + lineGap + valH + lineGap + subH val fullW = normalW * 2 + tileGap var x = pad; var y = pad; var col = 0 @@ -1011,20 +880,12 @@ class DroidDeckHudView(context: Context) : View(context) { val pad = sp(10f); val midGap = sp(12f); val stkLineGap = sp(3f) val left = ArrayList() - // "30->118" is roughly three times the width of "30", and the pill's - // number is set at 30sp precisely because it is the one big thing on a - // small overlay. Shrink it while it carries both figures so the pill - // does not balloon across the screen; it is still the largest element. val generating = differs(fpsNow) val fpsPx = if (generating) bigPx * 0.55f else bigPx left += Span(fpsText(fpsNow), colValue, fpsPx, glow = true) left += Span("fps", colDim, if (generating) bigUnitPx * 0.8f else bigUnitPx) val stack = ArrayList>() - // The GPU name normally heads the stack. A name wider than every stat under it (a Wayland spoof - // such as "Radeon RX 6800/6800 XT / 6900 XT spoof") would set the stack's width and stretch the - // whole capsule around empty space, so that name gets its own line across the top of the pill - // instead, starting over the API caption (see nameOnTop below). val gpuName = if (showGpuModel && gpuNameText().isNotBlank()) gpuModelSpans(stkPx, colDim, compact = true) else null run { val l = ArrayList() @@ -1032,11 +893,7 @@ class DroidDeckHudView(context: Context) : View(context) { if (showCPU) { if (l.isNotEmpty()) l += Span(" · ", colDim, stkPx); l += Span("CPU ${s.cpuPercent ?: "—"}%", colCpu, stkPx) } if (l.isNotEmpty()) stack.add(l) } - // RAM % (system-wide) takes the slot the latency row used to occupy, matching the GPU/CPU idiom - // on the row above it. colRam keeps RAM's identity colour consistent with the Full/Tiles sizes. if (showRAM) stack.add(listOf(Span("RAM ${s.ramPercent.roundToInt()}%", colRam, stkPx))) - // One battery row, as on the other sizes: each part shows on its own switch, so the battery - // temperature no longer goes with the RAM switch and watts show without the battery level. if (showBattery || showPower || showBatteryTemp) { val l = ArrayList() fun part(spans: List) { @@ -1049,9 +906,6 @@ class DroidDeckHudView(context: Context) : View(context) { if (showPower && s.battery.watts > 0f) part(listOf(Span("${fmt1(s.battery.watts)}W", colDim, stkPx))) if (l.isNotEmpty()) stack.add(l) } - // Latency (+ the display server, + optional VRAM) drops to the bottom of the stack, under the - // battery row. The display server sits here, not on the GPU-model line: that line is the - // pill's widest and ran into the capsule's rounded edge. run { val l = ArrayList() l += Span("${fmt1(1000f / max(fpsNow, 1f))}ms", colDim, stkPx) @@ -1059,8 +913,6 @@ class DroidDeckHudView(context: Context) : View(context) { if (showVram && s.vramText() != null) { l += Span(" · ", colDim, stkPx); l += Span("${s.vramText()} vram", colDim, stkPx) } stack.add(l) } - // Wayland HDR state on its OWN line directly under the latency · display-server line (it ran off - // the capsule's right edge as "40.3ms · Wayland · HDR (no headroom)"). Only in HDR sessions. if (hdrState != DroidDeckHudHdr.NONE) stack.add(hdrLine(stkPx)) var statsW = 0f @@ -1068,8 +920,6 @@ class DroidDeckHudView(context: Context) : View(context) { val nameOnTop = gpuName != null && runWidth(gpuName) > statsW if (gpuName != null && !nameOnTop) stack.add(0, gpuName) - // Left column: a small API/engine caption (DXVK/VKD3D/Zink) above the big FPS and the clock below - // it, all on the left edge, so the left reads API · FPS · clock top-to-bottom. val apiStr = apiLabel() val hasApi = apiStr != "FPS" val apiH = if (hasApi) lineH(stkPx) else 0f @@ -1088,14 +938,12 @@ class DroidDeckHudView(context: Context) : View(context) { contentW = pad + leftBlockW + midGap + stackW + pad contentH = pad + topLineH + innerH + pad - // left column (API caption + big FPS), vertically centered as a block var ly = pad + topLineH + (innerH - leftColH) / 2f if (hasApi) { placeRun(pad, ly - ascent(stkPx), listOf(Span(apiStr, colFps, stkPx))) ly += apiH + apiGap } placeRun(pad, ly - ascent(bigPx), left) - // stack, vertically centered var sy = pad + topLineH + (innerH - stackTotalH) / 2f for (l in stack) { placeRun(pad + leftBlockW + midGap, sy - ascent(stkPx), l) @@ -1103,22 +951,17 @@ class DroidDeckHudView(context: Context) : View(context) { } addSubtleClock(pad, leftAligned = true) if (nameOnTop && gpuName != null) { - // Left-aligned over the API caption; the pill only widens if the name is wider than the - // API/FPS column + stats. val end = placeRun(pad, pad - ascent(stkPx), gpuName) contentW = max(contentW, end + pad) } } - // One column as wide as the widest row: the logo, FPS, lows, graph and engine line start on its left - // edge (ink, not advance), and the header keeps the same space above and below the logo. private fun buildMinimal(s: HudMetrics.Snapshot) { val bigPx = sp(34f); val bigUnitPx = bigPx * 0.32f; val subPx = sp(11.5f); val footPx = sp(9.5f) val stroke = outlineWidth() val left = contentLeft() val inner = panelInset() + stroke - // Same reasoning as the pill: 34sp is sized for one number, not two. val minGenerating = differs(fpsNow) val big = listOf( Span(fpsText(fpsNow), colValue, if (minGenerating) bigPx * 0.55f else bigPx), @@ -1164,11 +1007,6 @@ class DroidDeckHudView(context: Context) : View(context) { finishPanel(panel, halfRound = true) } - /** - * Full's readouts on one line across the screen: the mark, the FPS, the graph, the lows, then GPU, CPU, - * memory, battery, display server and GPU model, with the clock on the right end. Groups that no longer - * fit before the clock are left off, from the end. - */ private fun buildStrip(s: HudMetrics.Snapshot) { val rowPx = sp(11.5f); val unitPx = rowPx * 0.8f; val fpsPx = sp(15f) val padX = sp(10f); val groupGap = sp(13f) @@ -1265,8 +1103,6 @@ class DroidDeckHudView(context: Context) : View(context) { return inkRect.left.toFloat() } - /** Places a column of "label + value" rows; returns (bottomY, rightX). Appends glyphs. Rows flagged - * [HudRow.inline] opt out of the shared label column (value follows the label directly). */ private fun layoutColumn(rows: List, x: Float, y0: Float, rowPx: Float, lineGap: Float, lvGap: Float): Pair { if (rows.isEmpty()) return Pair(y0, x) @@ -1286,7 +1122,6 @@ class DroidDeckHudView(context: Context) : View(context) { return Pair(y - lineGap, right) } - /** Places wrapping " · "-separated info fragments within maxWidth; returns (bottomY, rightX). */ private fun layoutWrap(fragments: List>, x: Float, y0: Float, maxWidth: Float, px: Float, lineGap: Float): Pair { if (fragments.isEmpty()) return Pair(y0, x) @@ -1307,7 +1142,6 @@ class DroidDeckHudView(context: Context) : View(context) { val rowPx = sp(11.5f); val unitPx = rowPx * 0.62f; val bandPx = sp(9.5f) val pad = sp(10f); val lineGap = sp(3.5f); val lvGap = sp(7f); val gutter = sp(16f) - // ---- LEFT column: GPU, aggregate CPU, then per-core rows ---- val left = ArrayList() if (showGpuModel && gpuNameText().isNotBlank()) left.add(HudRow(Span("GPU", colGpu, rowPx), gpuModelSpans(rowPx, colValue))) @@ -1339,7 +1173,6 @@ class DroidDeckHudView(context: Context) : View(context) { } } - // ---- RIGHT column: VRAM, RAM, SWP, NET, BAT, engine, lows ---- val right = ArrayList() if (showVram && s.vramText() != null) right.add(HudRow(Span("VRAM", colVram, rowPx), valueUnit(s.vramText()!!, rowPx, unitPx))) @@ -1377,8 +1210,6 @@ class DroidDeckHudView(context: Context) : View(context) { val v = ArrayList() v += glowing(numUnit(fpsText(fpsNow), "FPS", rowPx, unitPx)) v += gap(unitPx); v += numUnit(fmt1(1000f / max(fpsNow, 1f)), "ms", rowPx, unitPx) - // FPS/engine row: just the short API label. The DX version moved to the stack lines - // below the frametime graph (see below), so this row stays compact. right.add(HudRow(Span(apiLabel(), colFps, rowPx), v)) right.add(HudRow(Span("AVG", colLo, rowPx), numUnit(fmt1(fpsAvg), "FPS", rowPx, unitPx))) right.add(HudRow(Span("1%", colLo, rowPx), numUnit(lowText(lows.low1Fps), "FPS", rowPx, unitPx))) @@ -1387,15 +1218,12 @@ class DroidDeckHudView(context: Context) : View(context) { right.add(HudRow(Span("0.01%", colLo, rowPx), numUnit(lowText(lows.low001Fps), "FPS", rowPx, unitPx))) } - // ---- Two columns side by side (each reflows independently) ---- val (leftBottom, leftRight) = layoutColumn(left, pad, pad, rowPx, lineGap, lvGap) val rightX = (if (left.isEmpty()) pad else leftRight + gutter) val (rightBottom, rightRight) = layoutColumn(right, rightX, pad, rowPx, lineGap, lvGap) var y = max(leftBottom, rightBottom) var maxRight = max(leftRight, rightRight) - // ---- Bottom band spanning both columns: res · Proton · elapsed ---- - // (The graphics wrapper moved to the FPS-row label above, so it's no longer in this band.) val band = ArrayList>() if (showResolution) band.add(listOf(Span("RES ", colDim, bandPx), Span(resolutionString(), colValue, bandPx))) if (displayServer.isNotBlank()) band.add(listOf(Span("DISP ", colDim, bandPx), Span(displayServer, colDisp, bandPx))) @@ -1408,7 +1236,6 @@ class DroidDeckHudView(context: Context) : View(context) { y = bBottom; maxRight = max(maxRight, bRight) } - // ---- Full-width frametime graph ---- if (showGraph) { y += sp(3f) val gh = sp(22f) @@ -1418,9 +1245,6 @@ class DroidDeckHudView(context: Context) : View(context) { y += gh } - // ---- Stack-layer lines BELOW the frametime graph, subtle (same size/style as the bottom - // band): the DX-wrapper version first (e.g. "DXVK 2.4.1-…", gated by "DX ver"), then the - // graphics wrapper beneath it (e.g. "Original", gated by "Wrapper"). ---- val dxLine = dxVersionLabeled() val hasDx = showDxVer && dxLine.isNotBlank() if (hasDx) { @@ -1436,12 +1260,10 @@ class DroidDeckHudView(context: Context) : View(context) { contentW = maxRight + pad contentH = y + pad - addSubtleClock(pad) // subtle time-of-day, consistent with the other sizes + addSubtleClock(pad) } - // ---- Measure / draw --------------------------------------------------- override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - // The Strip spans the parent, past the side margin the floating sizes keep. if (stripRect != null) { setMeasuredDimension(contentW.roundToInt(), resolveSize(contentH.roundToInt(), heightMeasureSpec)); return } setMeasuredDimension( resolveSize(contentW.roundToInt(), widthMeasureSpec), @@ -1461,7 +1283,6 @@ class DroidDeckHudView(context: Context) : View(context) { drawTabPreview(canvas, blend) } - /** The docked tab drawn at its own size in the middle of the view, undoing the view's scale. */ private fun drawTabPreview(canvas: Canvas, blend: Float) { val (w, h) = dockTabSize() if (scaleX <= 0f || scaleY <= 0f) return @@ -1487,7 +1308,6 @@ class DroidDeckHudView(context: Context) : View(context) { dPanel?.let { panel -> panelFrame(panel)?.let { canvas.drawBitmap(it, 0f, 0f, null) } } - // Tile backings if (tileRects.isNotEmpty()) { tilePaint.color = Color.argb((14 * bgOpacity).roundToInt().coerceIn(8, 40), 255, 255, 255) val tr = sp(6f) @@ -1498,7 +1318,6 @@ class DroidDeckHudView(context: Context) : View(context) { tabRect?.let { drawTab(canvas, it) } stripRect?.let { drawStrip(canvas, it) } - // Text for (g in glyphs) { val paint = if (g.sans) wordmarkPaint else drawPaint paint.textSize = g.sizePx @@ -1509,14 +1328,11 @@ class DroidDeckHudView(context: Context) : View(context) { } headerMark?.let { DroidDeckMark.draw(canvas, it.left, it.top, it.height(), blend(colValue), accent) } - // Frametime graph graphRect?.let { drawGraph(canvas, it) } lockController.drawBadge(canvas) } - // MangoHud's frametime graph: the last 200 frames on a fixed 0-50 ms scale, longer frames clipped - // to 49.9 ms (hud_elements.cpp TransformForward_Custom). private fun drawGraph(canvas: Canvas, rect: RectF) { val stats = frames ?: return stats.graph(graphBuf) @@ -1528,7 +1344,6 @@ class DroidDeckHudView(context: Context) : View(context) { if (i == 0) graphPath.moveTo(px, py) else graphPath.lineTo(px, py) } if (dPanel != null) { - // Neon without a blur: a blur would be redrawn with every frame. graphPaint.strokeWidth = sp(4.5f) graphPaint.color = withAlpha(blend(colGraph), 56) canvas.drawPath(graphPath, graphPaint) @@ -1538,7 +1353,6 @@ class DroidDeckHudView(context: Context) : View(context) { canvas.drawPath(graphPath, graphPaint) } - /** The bar, with the accent line on the side that faces the game (under it at the top, over it at the bottom). */ private fun drawStrip(canvas: Canvas, rect: RectF) { headerPaint.shader = null headerPaint.color = panelColor() @@ -1560,8 +1374,6 @@ class DroidDeckHudView(context: Context) : View(context) { private fun withAlpha(color: Int, alpha: Int): Int = Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)) - /** The D: straight left side with small corners, round on the right with radius [rad] (a half circle - * when it is half the height). */ private fun dShape(l: Float, t: Float, r: Float, b: Float, rad: Float, corner: Float): Path { return Path().apply { moveTo(l, t + corner) @@ -1575,7 +1387,6 @@ class DroidDeckHudView(context: Context) : View(context) { } } - /** Where the inside of the outline's round side is at height [y]. */ private fun roundEdgeX(p: RectF, y: Float, stroke: Float): Float { val r = dRadius - stroke val cx = p.right - dRadius @@ -1605,7 +1416,6 @@ class DroidDeckHudView(context: Context) : View(context) { headerPaint.color = withAlpha(accent, 90) c.drawRect(panel.left, dDividerTop, roundEdgeX(panel, dDividerTop, stroke), dividerBottom, headerPaint) } - // The watermark fills the half circle on the short sizes and sits on the right of the tall ones. val halfRound = dRadius >= panel.height() / 2f - 0.5f val wmH = if (halfRound) panel.height() * 0.92f else min(panel.height(), panel.width()) * 0.55f DroidDeckMark.draw(c, panel.right - DroidDeckMark.widthFor(wmH) - (if (halfRound) panel.height() * 0.02f else sp(10f)), @@ -1616,7 +1426,6 @@ class DroidDeckHudView(context: Context) : View(context) { val outline = dShape(panel.left + h, panel.top + h, panel.right - h, panel.bottom - h, dRadius - h, corner) val blur = BlurMaskFilter((sp(3.2f) - 0.5f).coerceAtLeast(0.5f) / 0.57735f, BlurMaskFilter.Blur.NORMAL) glowPaint.strokeWidth = stroke - // Two blurred passes under the sharp one, as the 4-in-1 preview's neon. for (pass in 0..2) { glowPaint.maskFilter = if (pass < 2) blur else null glowPaint.style = Paint.Style.STROKE; glowPaint.color = outlineColor() @@ -1657,10 +1466,8 @@ class DroidDeckHudView(context: Context) : View(context) { DroidDeckMark.draw(canvas, rect.centerX() - markW / 2f, rect.centerY() - markH / 2f, markH, blend(colValue), accent) } - /** Screen point → whether an on-screen control under the HUD takes a touch there. */ internal var passesTouchAt: ((rawX: Float, rawY: Float) -> Boolean)? = null - // Declining the down event hands the whole gesture to the views under the HUD. override fun onTouchEvent(event: MotionEvent): Boolean { if (dock != DroidDeckHudDock.NONE) return onTabTouch(event) if (event.actionMasked == MotionEvent.ACTION_DOWN && lockController.isLocked() && diff --git a/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckMark.kt b/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckMark.kt index dd21683f0..621f6dc1f 100644 --- a/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckMark.kt +++ b/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckMark.kt @@ -5,7 +5,6 @@ import android.graphics.Matrix import android.graphics.Paint import android.graphics.Path -/** artwork/droiddeck-mark.svg: the split D with its circular cut-out and the blue orb. */ internal object DroidDeckMark { private const val VIEW_X = -55.98f const val VIEW_W = 239.92f @@ -36,7 +35,6 @@ internal object DroidDeckMark { fun widthFor(height: Float): Float = height * VIEW_W / VIEW_H - /** Where the drawn mark starts and ends inside [widthFor]: the artwork has empty space on both sides. */ fun inkLeftFor(height: Float): Float = (-31.98f - VIEW_X) * height / VIEW_H fun inkRightFor(height: Float): Float = (175.93f - VIEW_X) * height / VIEW_H diff --git a/app/src/main/java/com/droiddeck/launcher/hud/GameFrames.kt b/app/src/main/java/com/droiddeck/launcher/hud/GameFrames.kt index adf3e067e..2bc92b98d 100644 --- a/app/src/main/java/com/droiddeck/launcher/hud/GameFrames.kt +++ b/app/src/main/java/com/droiddeck/launcher/hud/GameFrames.kt @@ -3,13 +3,6 @@ package com.droiddeck.launcher.hud import java.io.File import kotlin.math.roundToInt -/** - * Frame statistics by MangoHud's rules, so the HUD and Steam's performance overlay agree: MangoHud - * overlay.cpp update_hud_info_with_frametime and hud_elements.cpp fps() / frame_timing(). The FPS is the - * frames counted over a sampling period of at least 500 ms, the frame time next to it is 1000 / FPS, and - * the graph holds the last 200 frame times (zeros until filled) on a fixed 0-50 ms scale, min and max - * taken over those 200. Average and lows are DroidDeck HUD's own readouts. - */ internal class FrameStats { private val graph = FloatArray(GRAPH_FRAMES) private var graphHead = 0 @@ -29,11 +22,9 @@ internal class FrameStats { @Volatile var fps = 0f private set - /** Completed sampling periods; MangoHud redraws its readouts when one completes. */ @Volatile var fpsUpdates = 0L private set - /** Frames added so far; the HUD redraws its graph when this moves. */ @Volatile var count = 0L private set @@ -81,10 +72,8 @@ internal class FrameStats { count++ } - /** MangoHud's frame time readout: 1000 / FPS. */ fun frametimeMs(): Float = 1000f / maxOf(fps, 1f) - /** The graph, oldest first, into [out] (GRAPH_FRAMES long). */ @Synchronized fun graph(out: FloatArray) { for (i in 0 until GRAPH_FRAMES) out[i] = graph[(graphHead + i) % GRAPH_FRAMES] @@ -96,7 +85,6 @@ internal class FrameStats { @Synchronized fun graphMaxMs(): Float = graph.max() - /** Average over the frames this session, from the first frame to [nowNs]'s last frame. */ @Synchronized fun averageFps(): Float { if (sessionFrames < 2 || lastPresentNs <= sessionStartNs) return 0f @@ -112,7 +100,6 @@ internal class FrameStats { } } - /** FPS at the 99th, 99.9th and 99.99th percentile frame time of the last LOWS_FRAMES frames. */ fun lows(nowNs: Long): Lows { val copy: LongArray synchronized(this) { @@ -145,11 +132,6 @@ internal class FrameStats { } } -/** - * Feeds [stats] with the frame times gamescope shares (the ones MangoHud shows). Sessions without the - * shared file, or where gamescope stopped sending, fall back to the frames DroidDeck's compositor shows, - * which stop at the screen's refresh rate. - */ internal class GameFrameFeed(private val file: File, val stats: FrameStats = FrameStats()) { private var handle = 0L private var nextOpenNs = 0L @@ -160,7 +142,7 @@ internal class GameFrameFeed(private val file: File, val stats: FrameStats = Fra private val info = IntArray(5) @Volatile private var fromGamescope = false private var lastCompositorNs = 0L - @Volatile private var compositorFrames = 0L // written by the compositor thread only + @Volatile private var compositorFrames = 0L private var seenCompositorFrames = 0L @Volatile var engine = "" @@ -176,7 +158,6 @@ internal class GameFrameFeed(private val file: File, val stats: FrameStats = Fra @Volatile var refreshMilliHz = 0 private set - /** Main thread, once per display frame. True when new frames arrived. */ fun poll(nowNs: Long): Boolean { val compositorAdded = compositorFrames != seenCompositorFrames seenCompositorFrames = compositorFrames @@ -185,7 +166,6 @@ internal class GameFrameFeed(private val file: File, val stats: FrameStats = Fra nextOpenNs = nowNs + REOPEN_NS handle = runCatching { HudFrames.nativeOpen(file.path) }.getOrDefault(0L) if (handle == 0L) return compositorAdded - // Frames already in the file were written before this reader started. HudFrames.nativePoll(handle, frametimes, times) } val n = HudFrames.nativePoll(handle, frametimes, times) @@ -199,7 +179,6 @@ internal class GameFrameFeed(private val file: File, val stats: FrameStats = Fra } else if (lastRingFrameNs != 0L && nowNs - lastRingFrameNs > LIVE_NS) { fromGamescope = false } - // A new session writes the file again from the start; reopen when gamescope has gone quiet. if (n <= 0 && nowNs - maxOf(lastRingFrameNs, nextOpenNs - REOPEN_NS) > STALE_NS) close() if (nowNs - lastInfoNs >= INFO_NS) { lastInfoNs = nowNs @@ -208,7 +187,6 @@ internal class GameFrameFeed(private val file: File, val stats: FrameStats = Fra return n > 0 || compositorAdded } - /** Compositor thread: one frame shown on the panel. */ fun onCompositorFrame(nowNs: Long) { if (fromGamescope) { lastCompositorNs = 0L @@ -247,8 +225,6 @@ internal class GameFrameFeed(private val file: File, val stats: FrameStats = Fra private const val STALE_NS = 5_000_000_000L private const val INFO_NS = 500_000_000L - /** DroidDeck's mangoapp's names for gamescope's engine field (Valve's 20260902.1 build, app/main.cpp - * engine_type_from_str and overlay.h engines); gamescope sends "gamescope" when it knows none. */ fun engineLabel(name: String): String = when (name) { "DXVK" -> "DXVK" "vkd3d" -> "VKD3D" diff --git a/app/src/main/java/com/droiddeck/launcher/hud/HudFrames.kt b/app/src/main/java/com/droiddeck/launcher/hud/HudFrames.kt index 63a9a7945..ca13e7a3e 100644 --- a/app/src/main/java/com/droiddeck/launcher/hud/HudFrames.kt +++ b/app/src/main/java/com/droiddeck/launcher/hud/HudFrames.kt @@ -1,12 +1,10 @@ package com.droiddeck.launcher.hud -/** Reader for the frame-times file gamescope patch 0125 writes (hudframes.c). */ internal object HudFrames { init { System.loadLibrary("droiddeckwayland") } - /** Under the Linux root: the session exports /run/droiddeck-hud/frames to gamescope. */ const val PATH = "run/droiddeck-hud/frames" const val CAPACITY = 1024 diff --git a/app/src/main/java/com/droiddeck/launcher/hud/HudLockController.java b/app/src/main/java/com/droiddeck/launcher/hud/HudLockController.java index f739ec8b6..98c34df7c 100644 --- a/app/src/main/java/com/droiddeck/launcher/hud/HudLockController.java +++ b/app/src/main/java/com/droiddeck/launcher/hud/HudLockController.java @@ -8,7 +8,7 @@ * link to https://github.com/The412Banner/FusionHUD in the project documentation * AND in the in-app credits/about screen. * - * Ported to DroidDeck from FusionHUD (Bannerlator's HUD); DroidDeck changes are marked in the code. + * Ported to DroidDeck from FusionHUD (Bannerlator's HUD) and modified for DroidDeck. */ package com.droiddeck.launcher.hud; @@ -28,48 +28,18 @@ import android.view.View; import java.util.function.BiConsumer; import java.util.function.Consumer; -/** - * Shared touch + lock behaviour for every in-game performance HUD (classic FrameRating(+Horizontal), - * GameHub PerfHudView, GameNative PerformanceHudView, and the DroidDeck HUD). One place so the four - * overlays can't drift: - * - *
    - *
  • tap → {@link Callbacks#onTap()} (orientation-toggle for the first three; size-cycle for - * the DroidDeck HUD) — a no-op that only re-flashes the lock badge while locked.
  • - *
  • drag → moves the host view and, on release, {@link Callbacks#onMoved} to persist the - * position — disabled (frozen) while locked.
  • - *
  • long-press → toggles {@code hudLocked}, persisted via {@link Callbacks#onLockChanged} - * and kept in sync with the "Lock in place" menu chip. Works locked or unlocked (so a long-press - * always unlocks); a locked {@code DroidDeckHudView} passes touches on only over on-screen controls.
  • - *
- * - *

On every lock/unlock (and on a blocked gesture while locked) a small padlock badge fades in → - * holds → fades out in a corner of the panel. The fade is driven by a self-reposting Runnable that - * invalidates only while visible — nothing runs on the present path, nothing animates at rest. - * - *

All methods are Main-thread only (touch + draw), matching the epoll-thread rule: the views feed - * this from {@code onTouchEvent}/{@code dispatchDraw}, never from a present tick. - */ public class HudLockController { public interface Callbacks { - /** Unlocked single tap: orientation-toggle (classic/gamehub/gamenative) or size-cycle (DroidDeck HUD). */ void onTap(); - /** Drag released (unlocked): persist the new (x, y). */ void onMoved(float x, float y); - /** The view moved under a drag (unlocked), with the finger at screen point (rawX, rawY). */ default void onDragged(float rawX, float rawY) {} - /** The drag ended in a fling (unlocked), in pixels per second; comes just before {@link #onMoved}. */ default void onFlung(float velocityX, float velocityY) {} - /** Lock toggled by long-press: persist {@code hudLocked} + sync the menu chip. */ void onLockChanged(boolean locked); - /** Two-finger pinch (unlocked): the change since the previous event, above 1 when the fingers spread. */ default void onPinch(float factor) {} - /** The fingers came off a pinch: persist the new size. */ default void onPinchEnd() {} } - // Fade timing (ms): in → hold → out. Lock/unlock hold ~1.5 s; a blocked-gesture re-flash is shorter. private static final long FADE_IN_MS = 160; private static final long HOLD_TOGGLE_MS = 1500; private static final long HOLD_REFLASH_MS = 750; @@ -83,13 +53,10 @@ public class HudLockController { private boolean locked = false; - // Drag state private float downRawX, downRawY, startX, startY; private boolean dragging = false; - // A pinch ends the gesture's drag and tap: the finger left down after it must not move the HUD. private boolean pinched = false; - // Fade state private long fadeStart = 0; private long holdMs = HOLD_TOGGLE_MS; private boolean fadeShowLocked = false; @@ -110,14 +77,12 @@ public class HudLockController { iconStroke.setStrokeJoin(Paint.Join.ROUND); } - /** Set lock state from config (applyConfig) WITHOUT firing the persist callback. */ public void setLocked(boolean locked) { this.locked = locked; } public boolean isLocked() { return locked; } public boolean isDragging() { return dragging; } - /** Feed from the host view's onTouchEvent. Always consumes (mirrors the legacy manual handlers). */ public boolean onTouchEvent(MotionEvent event) { scaleDetector.onTouchEvent(event); detector.onTouchEvent(event); @@ -143,7 +108,6 @@ public class HudLockController { @Override public void onLongPress(MotionEvent e) { - // Long-press always toggles — this is the only gesture that still works while locked. locked = !locked; cb.onLockChanged(locked); flash(locked, HOLD_TOGGLE_MS); @@ -153,7 +117,7 @@ public class HudLockController { public boolean onSingleTapUp(MotionEvent e) { if (pinched) return true; if (locked) { - flash(true, HOLD_REFLASH_MS); // intentionally locked — remind the user + flash(true, HOLD_REFLASH_MS); } else { cb.onTap(); } @@ -171,7 +135,7 @@ public class HudLockController { if (pinched) return true; if (locked) { if (!dragging) flash(true, HOLD_REFLASH_MS); - dragging = true; // mark so the blocked drag doesn't fall through to a tap + dragging = true; return true; } view.setX(startX + (e2.getRawX() - downRawX)); @@ -203,7 +167,6 @@ public class HudLockController { } } - // ---- Fade badge ------------------------------------------------------- private void flash(boolean showLocked, long hold) { fadeShowLocked = showLocked; holdMs = hold; @@ -236,14 +199,6 @@ public class HudLockController { return Color.argb(Math.round(255 * a), Color.red(color), Color.green(color), Color.blue(color)); } - /** - * Draw the padlock badge in the top-right of the panel if a fade is in progress. Call from the - * host's onDraw (plain View) or dispatchDraw (ViewGroup), after the content. - * - *

No background box: the glyph is drawn twice — a slightly expanded, thicker semi-transparent - * black "halo" pass for legibility on any game frame, then the white glyph on top — so it reads as - * a floating icon, not an opaque tile. - */ public void drawBadge(Canvas canvas) { float a = currentAlpha(); if (a <= 0f) return; @@ -262,14 +217,12 @@ public class HudLockController { float r = size * 0.2f; float shackleTop = top + size * 0.08f; - // Pass 1: dark halo (expanded body, thicker shackle) for contrast; Pass 2: white glyph. drawLock(canvas, cx, bodyTop, bodyLeft, bodyRight, bottom, r, shackleTop, d, Color.argb(Math.round(150 * a), 0, 0, 0), 3.0f * d, 0.9f * d, false); drawLock(canvas, cx, bodyTop, bodyLeft, bodyRight, bottom, r, shackleTop, d, withAlpha(0xFFFFFFFF, a), 1.7f * d, 0f, true); } - /** One padlock pass. {@code halo} inflates the body/shackle in dark; the white pass punches the keyhole. */ private void drawLock(Canvas canvas, float cx, float bodyTop, float bodyLeft, float bodyRight, float bottom, float r, float shackleTop, float d, int color, float strokeW, float expand, boolean punchKeyhole) { @@ -277,7 +230,6 @@ public class HudLockController { iconStroke.setColor(color); iconStroke.setStrokeWidth(strokeW); - // Body canvas.drawRoundRect(bodyLeft - expand, bodyTop - expand, bodyRight + expand, bottom + expand, 2.4f * d, 2.4f * d, iconFill); if (punchKeyhole) { @@ -289,19 +241,16 @@ public class HudLockController { RectF arc = new RectF(cx - r, shackleTop, cx + r, shackleTop + 2f * r); if (fadeShowLocked) { - // Closed: both legs run straight down into the body. Path p = new Path(); p.addArc(arc, 180f, 180f); p.moveTo(cx - r, shackleTop + r); p.lineTo(cx - r, bodyTop); p.moveTo(cx + r, shackleTop + r); p.lineTo(cx + r, bodyTop); canvas.drawPath(p, iconStroke); } else { - // Open, clearly to the LEFT: the RIGHT leg stays seated in the body; the arc + LEFT leg - // hinge up-and-left off the right attachment so the left leg lifts well clear of the body. - canvas.drawLine(cx + r, shackleTop + r, cx + r, bodyTop, iconStroke); // fixed right leg + canvas.drawLine(cx + r, shackleTop + r, cx + r, bodyTop, iconStroke); Path p = new Path(); p.addArc(arc, 180f, 180f); - p.moveTo(cx - r, shackleTop + r); p.lineTo(cx - r, bodyTop); // full-length left leg + p.moveTo(cx - r, shackleTop + r); p.lineTo(cx - r, bodyTop); canvas.save(); canvas.rotate(-42f, cx + r, shackleTop + r); canvas.drawPath(p, iconStroke); diff --git a/app/src/main/java/com/droiddeck/launcher/hud/HudMetrics.java b/app/src/main/java/com/droiddeck/launcher/hud/HudMetrics.java index a98aa8535..759e01351 100644 --- a/app/src/main/java/com/droiddeck/launcher/hud/HudMetrics.java +++ b/app/src/main/java/com/droiddeck/launcher/hud/HudMetrics.java @@ -8,7 +8,8 @@ * link to https://github.com/The412Banner/FusionHUD in the project documentation * AND in the in-app credits/about screen. * - * Ported to DroidDeck from FusionHUD (Bannerlator's HUD); DroidDeck changes are marked in the code. + * Ported to DroidDeck from FusionHUD (Bannerlator's HUD) and modified for DroidDeck. + * Metric readers from @xXJSONDeruloXx's work. */ package com.droiddeck.launcher.hud; @@ -32,50 +33,17 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; -/** - * Shared live-metric collector for every performance HUD overlay. This is the single, device-complete - * implementation of the sysfs / Android readers — no overlay should re-read sysfs itself, so the GPU - * discovery / thermal / battery logic never diverges. - * - *

Ported and hardened from GameNative's {@code PerformanceHudView} readers, which are far more - * device-complete than the two-fixed-path versions previously inlined in the classic overlays: - *

    - *
  • GPU load — ~10 static candidates plus a dynamic {@code /sys/class/devfreq} + - * {@code /sys/devices/virtual/devfreq} walk for gpu/mali/g3d/kgsl nodes; handles gpubusy - * (busy/total), Mali gpuinfo (delta-ms/wall-ms, needs state between calls) and generic percent - * nodes. Discovery cached.
  • - *
  • CPU usage — {@code /proc/stat} aggregate delta with a scaling-frequency fallback for devices - * where {@code /proc/stat} is restricted.
  • - *
  • Thermal — prioritized CPU zone discovery (cpu-silicon > cpu-0 > cpu > soc > - * s5p-tmu > cputop > tsens > cluster > big/little) and a separate prioritized GPU zone - * discovery plus kgsl/mali direct paths. milli-°C normalization + 1..150 sanity clamp.
  • - *
  • Battery — % capacity, power W, runtime-left estimate (charge_counter / current_now, smoothed), - * battery temperature, plus the dual-battery current-sum for the classic overlays.
  • - *
- * - *

Every getter is null / absent-safe: nullable getters return {@code null} when a metric cannot be - * read (the overlay hides that row, matching GameNative behavior); the legacy non-null getters return - * 0 in that case. Not thread-safe; call from a single refresh thread per instance. - */ public class HudMetrics { private final Context context; public HudMetrics(Context context) { this.context = context; } - // DroidDeck: the readings Steam's performance overlay shows come from MangoReadings, so both agree. private MangoReadings mango; public void setMangoReadings(MangoReadings readings) { mango = readings; } - // ======================================================================= - // CPU usage - // ======================================================================= private Long lastCpuTotal = null; private Long lastCpuIdle = null; - /** - * Overall device CPU usage 0..100 computed from the /proc/stat delta since the last call, with a - * scaling-frequency fallback. Returns null only when neither source is readable. - */ public Integer getCpuUsagePercent() { String line = readFirstLine("/proc/stat"); if (line != null) { @@ -104,8 +72,6 @@ public class HudMetrics { return clampPercent((int) usage); } } - // First sample seeds the delta — fall through to the frequency estimate so the - // very first read still returns something. } } } @@ -127,20 +93,10 @@ public class HudMetrics { return clampPercent((int) ((currentTotal * 100L) / maxTotal)); } - /** Legacy non-null accessor (0 when unavailable). Kept for {@code PerfHudView}. */ - public float getCPUUsage() { - Integer p = getCpuUsagePercent(); - return p == null ? 0f : p; - } - - // ======================================================================= - // GPU load - // ======================================================================= private List gpuUsagePathsCache = null; private Long lastMaliGpuInfoMs = null; private long lastMaliGpuInfoWallMs = 0L; - /** GPU utilisation 0..100, or null when no readable source is found on this device. */ public Integer getGpuUsagePercent() { for (String path : discoverGpuUsagePaths()) { Integer p = readGpuUsageSample(path); @@ -149,12 +105,6 @@ public class HudMetrics { return null; } - /** Legacy non-null accessor (0 when unavailable). Kept for {@code PerfHudView}. */ - public int getGPULoad() { - Integer p = getGpuUsagePercent(); - return p == null ? 0 : p; - } - private List discoverGpuUsagePaths() { if (gpuUsagePathsCache != null) return gpuUsagePathsCache; LinkedHashSet candidates = new LinkedHashSet<>(); @@ -163,8 +113,6 @@ public class HudMetrics { if (new File(p).canRead()) candidates.add(p); } - // Vendor platform nodes with hashed/addressed names we can't hard-code (e.g. - // /sys/devices/platform/13000000.mali/utilisation, .../gpusysfs/gpu_busy, panfrost, sgpu). File platformDir = new File("/sys/devices/platform"); File[] platformNodes = platformDir.listFiles(File::isDirectory); if (platformNodes != null) { @@ -211,9 +159,6 @@ public class HudMetrics { return gpuUsagePathsCache; } - // Ordered, world-readable-when-present GPU utilisation nodes across the vendors we've seen: - // Adreno KGSL, Mali (misc + Exynos vendor), PowerVR, and amdgpu-style sysfs (Samsung Xclipse / - // "sgpu", AMD-on-Exynos). readGpuUsageSample() knows how to parse each filename shape. private static final String[] GPU_USAGE_STATIC_PATHS = { "/sys/class/kgsl/kgsl-3d0/gpubusy", "/sys/class/kgsl/kgsl-3d0/gpu_busy_percentage", @@ -223,23 +168,19 @@ public class HudMetrics { "/sys/class/misc/mali0/device/gpuinfo", "/sys/devices/platform/mali/utilization", "/sys/kernel/gpu/gpu_busy", - "/sys/devices/platform/gpusysfs/gpu_busy", // Exynos Mali vendor node + "/sys/devices/platform/gpusysfs/gpu_busy", "/sys/class/misc/pvrsrvkm/device/utilisation", - "/sys/class/pvr/utilisation", // PowerVR (Rogue) sysfs + "/sys/class/pvr/utilisation", "/sys/class/pvr/gpu_utilisation", - "/sys/class/drm/card0/device/gpu_busy_percent", // amdgpu / Xclipse (sgpu), 0..100 + "/sys/class/drm/card0/device/gpu_busy_percent", "/sys/class/devfreq/gpu/load", - // ── Additive cross-vendor fallbacks (appended LAST: tried only after every path above - // misses, so a device that already resolves — e.g. Adreno kgsl — never reaches these). ── - "/sys/kernel/ged/hal/gpu_utilization", // MediaTek GED: " " → loading% - "/sys/module/ged/parameters/gpu_loading", // MediaTek GED module param, plain percent + "/sys/kernel/ged/hal/gpu_utilization", + "/sys/module/ged/parameters/gpu_loading", }; - // Percent-bearing filenames scanned under each devfreq / platform GPU node. private static final String[] GPU_USAGE_FILES = { "gpu_busy_percentage", "gpu_busy_percent", "gpu_load", "utilisation", "utilization", "load", "gpu_busy", "gpuinfo", }; - // Substrings that mark a devfreq / platform node as the GPU, across vendors. private static final String[] GPU_NODE_TOKENS = { "gpu", "mali", "g3d", "kgsl", "panfrost", "pvr", "powervr", "xclipse", "sgpu", }; @@ -258,8 +199,6 @@ public class HudMetrics { return clampPercent((int) ((busy * 100L) / total)); } case "gpuinfo": { - // Mali multi-line node: the GPU busy time (ms) is the last token of line 1. Utilisation - // is the delta over wall-clock delta between two reads, so it needs state. String line = readNthLine(path, 1); if (line == null) return null; String[] toks = line.trim().split("\\s+"); @@ -294,46 +233,12 @@ public class HudMetrics { return null; } - // ======================================================================= - // RAM - // ======================================================================= - public float getRAMPercent() { - ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo(); - am.getMemoryInfo(mi); - if (mi.totalMem <= 0) return 0; - return (mi.totalMem - mi.availMem) * 100f / mi.totalMem; - } + private List thermalZonesCache = null; - /** Used RAM as "x.xGB" (or "yMB" below 1 GB), matching GameNative's HUD text. */ - public String getUsedRamText() { - ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - if (am == null) return "—"; - ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo(); - am.getMemoryInfo(mi); - long usedBytes = Math.max(0L, mi.totalMem - mi.availMem); - double usedGb = usedBytes / (1024.0 * 1024.0 * 1024.0); - if (usedGb >= 1.0) return String.format(Locale.US, "%.1fGB", usedGb); - return (usedBytes / (1024L * 1024L)) + "MB"; - } - - // ======================================================================= - // Temperature - // ======================================================================= - private List thermalZonesCache = null; // each entry: {type, tempPath} - - /** Legacy non-null CPU-temperature accessor in °C (0 when unavailable). Kept for {@code PerfHudView}. */ - public float getTemperature() { - Integer t = getCpuTempC(); - return t == null ? 0f : t; - } - - /** Hottest representative CPU temperature in °C, or null when no CPU zone is readable. */ public Integer getCpuTempC() { return readTemperatureC(discoverPrioritizedCpuTempPaths()); } - /** GPU temperature in °C, or null when no GPU sensor is readable. */ public Integer getGpuTempC() { ArrayList paths = new ArrayList<>(); paths.add("/sys/class/kgsl/kgsl-3d0/temp"); @@ -346,16 +251,16 @@ public class HudMetrics { private List discoverPrioritizedCpuTempPaths() { return prioritizePaths(type -> { - if (type.contains("gpu")) return null; // never mistake a GPU zone for CPU + if (type.contains("gpu")) return null; if (type.contains("cpu-silicon")) return 0; if (type.contains("cpu-0")) return 1; - if (type.contains("cpuss")) return 2; // Qualcomm cpuss composite - if (type.contains("mtktscpu")) return 2; // MediaTek CPU thermal - if (type.contains("cpu")) return 2; // cpu, cpu-*-usr, cpu_thermal, cpu_center - if (type.contains("s5p-tmu")) return 3; // Exynos TMU + if (type.contains("cpuss")) return 2; + if (type.contains("mtktscpu")) return 2; + if (type.contains("cpu")) return 2; + if (type.contains("s5p-tmu")) return 3; if (type.contains("soc")) return 4; if (type.contains("cputop")) return 5; - if (type.contains("tsens")) return 6; // Qualcomm tsens aggregate + if (type.contains("tsens")) return 6; if (type.contains("cluster")) return 7; if (type.contains("big") || type.contains("little")) return 8; return null; @@ -365,11 +270,11 @@ public class HudMetrics { private List discoverPrioritizedGpuTempPaths() { return prioritizePaths(type -> { if (type.contains("gpu-silicon")) return 0; - if (type.contains("gpuss")) return 1; // Qualcomm gpuss - if (type.contains("mtktsgpu")) return 1; // MediaTek GPU thermal + if (type.contains("gpuss")) return 1; + if (type.contains("mtktsgpu")) return 1; if (type.contains("gpu-virt")) return 2; if (type.contains("gpu")) return 2; - if (type.contains("g3d")) return 3; // Exynos Mali (g3d) + if (type.contains("g3d")) return 3; if (type.contains("kgsl")) return 4; if (type.contains("mali")) return 5; if (type.contains("xclipse") || type.contains("sgpu")) return 6; @@ -381,12 +286,11 @@ public class HudMetrics { private List prioritizePaths(Ranker ranker) { List zones = discoverAllThermalZones(); - ArrayList order = new ArrayList<>(); // {index, rank} + ArrayList order = new ArrayList<>(); for (int i = 0; i < zones.size(); i++) { Integer r = ranker.rank(zones.get(i)[0]); if (r != null) order.add(new int[]{i, r}); } - // Sort by rank, then by path for determinism. order.sort((a, b) -> { if (a[1] != b[1]) return Integer.compare(a[1], b[1]); return zones.get(a[0])[1].compareTo(zones.get(b[0])[1]); @@ -426,14 +330,12 @@ public class HudMetrics { if (!seen.add(path)) continue; Integer raw = readIntFromLine(path); if (raw == null) continue; - // Round to nearest degree for milli-°C sources; sanity-clamp to reject offline sensors. int celsius = raw > 1000 ? (raw + 500) / 1000 : raw; if (celsius >= 1 && celsius <= 150) return celsius; } return null; } - /** The first path in {@code paths} that yields a plausible reading, or null. */ private String resolveTempPath(List paths) { LinkedHashSet seen = new LinkedHashSet<>(); for (String path : paths) { @@ -446,13 +348,8 @@ public class HudMetrics { return null; } - // ======================================================================= - // Temperature danger bands (green / amber / red) - // ======================================================================= - public enum TempSensor { CPU, GPU, BATTERY } - /** Amber and red onset in °C. Always °C internally — conversion happens at format time only. */ public static final class Thresholds { public final int amberC; public final int redC; @@ -463,19 +360,14 @@ public class HudMetrics { public static final int TEMP_COLOR_AMBER = 0xFFFFB74D; public static final int TEMP_COLOR_RED = 0xFFEF5350; - // Tier-2 fallbacks, used when the device declares no usable trip points. Battery is the only one - // with a well-grounded number (Li-ion); CPU/GPU vary enough between parts that these are a - // deliberately conservative guess — which is exactly why trip points are preferred. private static final int DEFAULT_RED_CPU = 90; private static final int DEFAULT_RED_GPU = 90; private static final int DEFAULT_RED_BAT = 48; - /** Amber sits a proportional step below red, which scales sensibly across both CPU/GPU and battery ranges. */ public static int amberForRed(int redC) { return Math.round(redC * 0.88f); } - /** Parsed temperature display options, shared by every overlay so they can't disagree. */ public static final class TempDisplay { public final boolean fahrenheit; public final boolean colorBands; @@ -518,17 +410,11 @@ public class HudMetrics { } } - // Trip points are a device constant — resolve once per sensor and keep it. private final java.util.EnumMap autoThresholdCache = new java.util.EnumMap<>(TempSensor.class); private final java.util.EnumSet autoThresholdResolved = java.util.EnumSet.noneOf(TempSensor.class); - /** - * Bands for a sensor. Manual mode uses the user's red point; Auto prefers the device's OWN - * declared thermal trip points (per-device truth — a handheld with active cooling and a phone on - * the same SoC throttle at very different points) and falls back to {@link #DEFAULT_RED_CPU} etc. - */ public Thresholds resolveThresholds(TempSensor sensor, TempDisplay display) { if (display != null && !display.auto) { int red = display.manualRedFor(sensor); @@ -547,15 +433,6 @@ public class HudMetrics { return new Thresholds(amberForRed(red), red); } - /** - * Bands from the kernel's declared trip points for the zone we actually read this sensor from: - * amber ← the lowest sane {@code passive} trip, red ← the lowest sane {@code hot}/{@code critical}. - * - *

Returns null unless a usable red point is found. Two real traps this guards against: some - * zones aren't temperature sensors at all (a Snapdragon's {@code pm8550-bcl-lvl0} battery-current - * limiter reports {@code trip_point_0_temp = 1} with type {@code passive}), and the GPU is often - * read from a non-thermal_zone node ({@code /sys/class/kgsl/...}) that has no trip points. - */ private Thresholds readTripPointThresholds(TempSensor sensor) { String tempPath; switch (sensor) { @@ -563,7 +440,7 @@ public class HudMetrics { tempPath = resolveTempPath(discoverPrioritizedGpuTempPaths()); break; case BATTERY: - return null; // battery temp comes from BatteryManager, not a thermal zone + return null; default: tempPath = resolveTempPath(discoverPrioritizedCpuTempPaths()); } @@ -580,8 +457,6 @@ public class HudMetrics { Integer raw = readIntFromLine(tripTemp.getPath()); if (raw == null) continue; int celsius = raw > 1000 ? (raw + 500) / 1000 : raw; - // Reject anything that isn't plausibly a thermal trip — this is what filters out the - // battery-current-limiter zones whose "trip point" is a level index, not a temperature. if (celsius < 20 || celsius > 120) continue; String typePath = tripTemp.getPath().replaceAll("_temp$", "_type"); @@ -600,7 +475,6 @@ public class HudMetrics { return new Thresholds(amber, red); } - /** Band colour for a reading, or {@code fallbackColor} when banding is off or the value is unusable. */ public static int tempColor(Float celsius, Thresholds t, TempDisplay display, int fallbackColor) { if (celsius == null || t == null || display == null || !display.colorBands) return fallbackColor; if (celsius >= t.redC) return TEMP_COLOR_RED; @@ -608,7 +482,6 @@ public class HudMetrics { return TEMP_COLOR_OK; } - /** Formats a °C reading in the user's unit. Thresholds stay °C; only display converts. */ public static String formatTemp(float celsius, TempDisplay display, boolean oneDecimal) { boolean f = display != null && display.fahrenheit; float value = f ? celsius * 9f / 5f + 32f : celsius; @@ -618,15 +491,12 @@ public class HudMetrics { : String.format(Locale.ENGLISH, "%d%s", Math.round(value), unit); } - // ======================================================================= - // Battery - // ======================================================================= public static final class Battery { public final float watts; public final boolean charging; - public final Integer percent; // 0..100 or null - public final Integer tempC; // rounded °C or null - public final String runtimeText; // "LEFT 2h 5m" / "LEFT CHG" / null + public final Integer percent; + public final Integer tempC; + public final String runtimeText; Battery(float watts, boolean charging) { this(watts, charging, null, null, null); @@ -640,29 +510,23 @@ public class HudMetrics { } } - /** power_supply current_now channels (µA) for the dual-battery sum. */ private static final String[] CURRENT_CHANNELS = { "/sys/class/power_supply/battery/current_now", "/sys/class/power_supply/bms/current_now", "/sys/class/power_supply/main/current_now", }; - /** power_supply voltage_now channels (µV) — fallback when {@code EXTRA_VOLTAGE} reads 0 on some - * OEM firmwares (e.g. HONOR Magic), which otherwise leaves wattage stuck at 0.0W. */ private static final String[] VOLTAGE_CHANNELS = { "/sys/class/power_supply/battery/voltage_now", "/sys/class/power_supply/bms/voltage_now", "/sys/class/power_supply/main/voltage_now", }; - /** power_supply power_now channels (µW) — last-resort direct power reading when neither the - * BatteryManager voltage nor {@code voltage_now} is available. */ private static final String[] POWER_CHANNELS = { "/sys/class/power_supply/battery/power_now", "/sys/class/power_supply/bms/power_now", "/sys/class/power_supply/main/power_now", }; - /** First readable long across a channel list, or null. */ private static Long readLongFromChannels(String[] paths) { for (String p : paths) { Long v = readLongFromLine(p); @@ -671,15 +535,6 @@ public class HudMetrics { return null; } - /** - * ADDITIVE charging fallback from {@code /sys/class/power_supply/*}, consulted ONLY when the - * framework gave no usable charge state ({@code EXTRA_STATUS == UNKNOWN} or a null battery intent). - * A charger/input supply reporting {@code online == 1} — or the battery's own {@code status} reading - * "Charging"/"Full" — means charging. Returns TRUE when charging is positively detected, FALSE when a - * status/online node is readable but not charging, or null when nothing usable exists (so the caller - * keeps its own default). Never reached on devices whose BatteryManager reports a real status - * (e.g. Adreno phones/handhelds), so it cannot change their reading. - */ private static Boolean readChargingFromPowerSupply() { File[] supplies = new File("/sys/class/power_supply").listFiles(); if (supplies == null || supplies.length == 0) return null; @@ -690,14 +545,12 @@ public class HudMetrics { type = type == null ? "" : type.trim().toLowerCase(Locale.US); boolean isBattery = type.equals("battery") || name.equals("battery") || name.equals("bms"); if (isBattery) { - // The battery's own status string is the most direct charge-state signal. String st = readFirstLine(new File(supply, "status").getPath()); if (st == null) continue; st = st.trim().toLowerCase(Locale.US); - sawSignal = Boolean.FALSE; // "discharging"/"not charging" both land here + sawSignal = Boolean.FALSE; if (st.equals("charging") || st.equals("full")) return Boolean.TRUE; } else { - // A charger / power-input supply (ac/usb/wireless/mains/dc): online==1 → external power in. Long online = readLongFromLine(new File(supply, "online").getPath()); if (online == null) continue; sawSignal = Boolean.FALSE; @@ -712,55 +565,6 @@ public class HudMetrics { private static final double RUNTIME_SMOOTHING_OLD_WEIGHT = 0.65; private static final double RUNTIME_SMOOTHING_NEW_WEIGHT = 0.35; - /** - * Legacy discharge-only power reading used by the classic overlays + {@code PerfHudView}: - * watts are reported only while discharging (0 when charging). When {@code dualBattery} is set the - * per-cell current channels are summed to correct devices that report only one cell's current. - */ - public Battery getBattery(boolean dualBattery) { - Intent status = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); - boolean charging = false; - int voltageMv = 0; - if (status != null) { - charging = status.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0; - voltageMv = status.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0); - } else { - // ADDITIVE: no battery intent → best-effort power_supply charge probe (default stays false). - // Never reached on devices that deliver ACTION_BATTERY_CHANGED (e.g. Adreno). - Boolean sysCharging = readChargingFromPowerSupply(); - if (sysCharging != null) charging = sysCharging; - } - long microAmps; - if (dualBattery) { - long sum = 0; int n = 0; - for (String path : CURRENT_CHANNELS) { - Long v = readLongFromLine(path); - if (v != null) { sum += Math.abs(v); n++; } - } - microAmps = n > 0 ? -sum : readCurrentNowFallback(); - } else { - microAmps = readCurrentNowFallback(); - } - // current_now's SIGN convention is device-dependent — many OEMs (Xiaomi/Poco especially) - // report discharge as POSITIVE, the opposite of the AOSP convention. Gating wattage on the - // sign therefore reads 0W on battery on those devices (and only shows a value while charging). - // Use the magnitude for the power figure; the reliable charge DIRECTION is EXTRA_PLUGGED (the - // `charging` flag), which the HUD already uses for the PWR/CHG label. - // EXTRA_VOLTAGE reads 0 on some OEM firmwares even though current is fine — fall back to the - // power_supply voltage_now (µV → mV) so wattage isn't stuck at 0. - if (voltageMv <= 0) { - Long uv = readLongFromChannels(VOLTAGE_CHANNELS); - if (uv != null && uv > 0) voltageMv = (int) (Math.abs(uv) / 1000L); - } - // current unit auto-detected (mA vs µA) — see currentRawToAmps. - float watts = (voltageMv / 1000f) * currentRawToAmps(microAmps); - return new Battery(watts, charging); - } - - /** - * Full GameNative-parity battery snapshot for the GameNative-style HUD: %, power W (magnitude, - * regardless of charge/discharge), smoothed runtime-left estimate, and temperature. - */ public Battery collectBattery() { BatteryManager bm = (BatteryManager) context.getSystemService(Context.BATTERY_SERVICE); if (bm == null) return new Battery(0f, false); @@ -771,15 +575,12 @@ public class HudMetrics { Intent status = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); if (status == null) { - // No battery intent at all → additive power_supply charge probe (default stays false). Boolean sysCharging = readChargingFromPowerSupply(); return new Battery(0f, sysCharging != null && sysCharging, percent, null, null); } int st = status.getIntExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN); boolean charging = st == BatteryManager.BATTERY_STATUS_CHARGING || st == BatteryManager.BATTERY_STATUS_FULL; - // ADDITIVE: only when the framework reports no usable charge state do we consult power_supply. - // A device that reports CHARGING/DISCHARGING/FULL/NOT_CHARGING (e.g. Adreno) never enters this. if (st == BatteryManager.BATTERY_STATUS_UNKNOWN) { Boolean sysCharging = readChargingFromPowerSupply(); if (sysCharging != null) charging = sysCharging; @@ -792,23 +593,16 @@ public class HudMetrics { int rawTemp = status.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0); if (rawTemp > 0) tempC = Math.round(rawTemp / 10f); - // EXTRA_VOLTAGE reads 0 on some OEM firmwares (e.g. HONOR Magic) even though capacity, temp - // and current all work — which left wattage stuck at 0.0W. Fall back to the power_supply - // voltage_now (µV → mV); if voltage is still unavailable, use power_now (µW) directly. if (voltageMv <= 0) { Long uv = readLongFromChannels(VOLTAGE_CHANNELS); if (uv != null && uv > 0) voltageMv = (int) (Math.abs(uv) / 1000L); } - // Wattage = current × voltage, with current UNIT auto-detected (CURRENT_NOW is nominally µA but - // many OEMs — e.g. this HONOR — report mA; see batteryCurrentAmps). currentMicroAmps above stays - // as-is for the runtime estimate (a ratio, so unit-independent). If neither current nor voltage - // resolves, fall back to power_now (µW) directly. float amps = batteryCurrentAmps(bm); float watts = 0f; if (amps > 0f && voltageMv > 0) { watts = (voltageMv / 1000f) * amps; } else { - Long uw = readLongFromChannels(POWER_CHANNELS); // µW + Long uw = readLongFromChannels(POWER_CHANNELS); if (uw != null && uw != 0L) watts = (float) (Math.abs(uw) / 1_000_000.0); } @@ -842,24 +636,12 @@ public class HudMetrics { return minutes + "m"; } - private long readCurrentNowFallback() { - BatteryManager bm = (BatteryManager) context.getSystemService(Context.BATTERY_SERVICE); - return bm.getLongProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW); - } - - /** Raw battery current reading → AMPS, with unit AUTO-DETECT. {@code BATTERY_PROPERTY_CURRENT_NOW} - * (and {@code current_now}) are nominally µA, but many OEMs report mA — a small number, whereas a - * real µA reading is always huge. So a magnitude < 20000 means the device gave mA. Without this, - * wattage on mA-reporting devices came out ~1000× too small → 0.0W even though current & voltage are - * fine. Sentinel/zero → 0. (Heuristic mirrors Ludashi-plus WinlatorHUD.getBatteryCurrentAmps().) */ private static float currentRawToAmps(long raw) { if (raw == 0L || raw == Long.MIN_VALUE) return 0f; long a = Math.abs(raw); - return a < 20000L ? a / 1000f : a / 1_000_000f; // mA → A, else µA → A + return a < 20000L ? a / 1000f : a / 1_000_000f; } - /** Battery current in AMPS for the wattage calc: framework property first, then power_supply - * {@code current_now} sysfs (battery/bms/main) when it's unsupported ({@code Long.MIN_VALUE}) or 0. */ private float batteryCurrentAmps(BatteryManager bm) { long raw = (bm != null) ? bm.getLongProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW) : 0L; if (raw == 0L || raw == Long.MIN_VALUE) { @@ -869,11 +651,6 @@ public class HudMetrics { return currentRawToAmps(raw); } - // ======================================================================= - // Clock frequencies (best-effort, nullable) - // ======================================================================= - - /** Peak CPU core clock in MHz (max scaling_cur_freq across cores), or null when unreadable. */ public Integer getCpuClockMhz() { int cores = Runtime.getRuntime().availableProcessors(); long maxKhz = 0; @@ -882,29 +659,26 @@ public class HudMetrics { if (cur != null && cur > maxKhz) maxKhz = cur; } if (maxKhz <= 0) return null; - return (int) (maxKhz / 1000L); // kHz → MHz + return (int) (maxKhz / 1000L); } private List gpuClockPathsCache = null; private static final String[] GPU_CLOCK_PATHS = { - "/sys/class/kgsl/kgsl-3d0/gpuclk", // Adreno, Hz - "/sys/class/kgsl/kgsl-3d0/devfreq/cur_freq", // Adreno devfreq, Hz - "/sys/class/kgsl/kgsl-3d0/clock_mhz", // some kernels, already MHz - "/sys/kernel/gpu/gpu_clock", // Mali vendor node, MHz - "/sys/devices/platform/gpusysfs/gpu_clock", // Exynos Mali vendor node, MHz - "/sys/class/misc/mali0/device/clock", // Mali, kHz/MHz - "/sys/class/devfreq/gpu/cur_freq", // generic devfreq gpu, Hz - // ── Additive cross-vendor fallbacks (appended LAST: only reached when every node above - // is unreadable, so Adreno/Mali devices that already resolve are unaffected). ── - "/sys/kernel/ged/hal/current_freqency", // MediaTek GED (vendor's spelling), kHz - "/sys/kernel/ged/hal/gpu_clock", // MediaTek GED alt, MHz + "/sys/class/kgsl/kgsl-3d0/gpuclk", + "/sys/class/kgsl/kgsl-3d0/devfreq/cur_freq", + "/sys/class/kgsl/kgsl-3d0/clock_mhz", + "/sys/kernel/gpu/gpu_clock", + "/sys/devices/platform/gpusysfs/gpu_clock", + "/sys/class/misc/mali0/device/clock", + "/sys/class/devfreq/gpu/cur_freq", + "/sys/kernel/ged/hal/current_freqency", + "/sys/kernel/ged/hal/gpu_clock", }; private List discoverGpuClockPaths() { if (gpuClockPathsCache != null) return gpuClockPathsCache; LinkedHashSet found = new LinkedHashSet<>(); for (String p : GPU_CLOCK_PATHS) if (new File(p).canRead()) found.add(p); - // Vendor-agnostic devfreq walk: any gpu/mali/panfrost/xclipse/sgpu node's cur_freq (Hz). File[] devfreqRoots = { new File("/sys/class/devfreq"), new File("/sys/devices/virtual/devfreq"), @@ -928,49 +702,31 @@ public class HudMetrics { return gpuClockPathsCache; } - /** GPU clock in MHz (Adreno KGSL / devfreq / Mali nodes), or null when no source reports it. */ public Integer getGpuClockMhz() { for (String p : discoverGpuClockPaths()) { Long raw = readLongFromLine(p); if (raw == null || raw <= 0) continue; long mhz; - if (raw >= 1_000_000L) mhz = raw / 1_000_000L; // Hz - else if (raw >= 10_000L) mhz = raw / 1_000L; // kHz (defensive) - else mhz = raw; // already MHz + if (raw >= 1_000_000L) mhz = raw / 1_000_000L; + else if (raw >= 10_000L) mhz = raw / 1_000L; + else mhz = raw; if (mhz >= 1 && mhz <= 3000) return (int) mhz; } return null; } - // ======================================================================= - // VRAM (best-effort, Java-only sysfs — no native rebuild) - // ======================================================================= - // Covered here in pure Java: Adreno KGSL (bytes mapped/allocated) and amdgpu-style drm nodes - // (Samsung Xclipse / "sgpu", AMD-on-Exynos), which expose mem_info_vram_used/_total directly. - // TODO(native): VK_EXT_memory_budget as a vendor-agnostic fallback for Mali/PowerVR, which expose - // no world-readable VRAM node. FEASIBILITY (checked 2026-07): the compositor's Vulkan layer - // (app/src/main/cpp/winlator/vulkan.c) creates an EPHEMERAL VkInstance/VkPhysicalDevice inside - // getRenderer() and destroys it every call — it holds no reusable device and loads only the - // properties/extension entrypoints, not vkGetPhysicalDeviceMemoryProperties2. Wiring the budget - // query therefore means a new JNI method + enabling VK_KHR_get_physical_device_properties2 / - // VK_EXT_memory_budget at instance create + an NDK rebuild — out of scope for a source-only pass. - // CAVEAT to document if/when added: this HUD is an app-side overlay, NOT in the game's Vulkan - // device, so an app-created instance reports the DEVICE-WIDE heap budget (a GPU-memory-pressure - // proxy), not the game's per-process VRAM. Devices with no readable node return null → HUD hides VRAM. private List vramUsedPathsCache = null; private static final String[] VRAM_USED_PATHS = { - "/sys/class/kgsl/kgsl-3d0/gpumem_mapped", // bytes currently mapped to the GPU - "/sys/class/kgsl/kgsl-3d0/page_alloc", // bytes allocated to the KGSL page pool - "/sys/class/kgsl/kgsl-3d0/mapped", // alt name on some kernels - "/sys/class/drm/card0/device/mem_info_vram_used",// amdgpu / Xclipse (sgpu), bytes - "/sys/class/kgsl/kgsl-3d0/page_alloc_max", // high-water fallback + "/sys/class/kgsl/kgsl-3d0/gpumem_mapped", + "/sys/class/kgsl/kgsl-3d0/page_alloc", + "/sys/class/kgsl/kgsl-3d0/mapped", + "/sys/class/drm/card0/device/mem_info_vram_used", + "/sys/class/kgsl/kgsl-3d0/page_alloc_max", }; - // Total-VRAM companions (amdgpu exposes one; used only by the diagnostics report). private static final String[] VRAM_TOTAL_PATHS = { "/sys/class/drm/card0/device/mem_info_vram_total", }; - /** Used GPU memory in bytes from Adreno KGSL sysfs, or null when this device exposes nothing readable. */ public Long getVramUsedBytes() { if (vramUsedPathsCache == null) { ArrayList found = new ArrayList<>(); @@ -986,50 +742,33 @@ public class HudMetrics { private static Long sanitizeVramBytes(Long v) { if (v == null) return null; - if (v < (1L << 20)) return null; // < 1 MiB → idle-zero / not a byte count - if (v > 64L * (1L << 30)) return null; // > 64 GiB → not bytes + if (v < (1L << 20)) return null; + if (v > 64L * (1L << 30)) return null; return v; } - /** Used GPU memory as "x.xGiB" (or "yMiB"), or null when no VRAM source is readable. */ - public String getVramUsedText() { - Long b = getVramUsedBytes(); - return b == null ? null : formatGiB(b); - } - private static String formatGiB(long bytes) { return String.format(Locale.US, "%.1fGiB", bytes / (1024.0 * 1024.0 * 1024.0)); } - // ======================================================================= - // Single cached snapshot — read EVERYTHING once per HUD refresh - // ======================================================================= - - /** - * Immutable point-in-time reading of every metric the DroidDeck HUD draws. Collected in one pass by - * {@link #snapshot()} so the view never touches sysfs / BatteryManager during draw (the user's - * explicit ask: one collection pass, cached, so gameplay isn't strained). All numeric fields are - * raw (nullable when unreadable); the view formats + colours them. - */ public static final class Snapshot { - public final Integer cpuPercent; // 0..100 or null - public final Integer gpuPercent; // 0..100 or null - public final Integer cpuClockMhz; // MHz or null - public final Integer gpuClockMhz; // MHz or null - public final Integer cpuTempC; // °C or null - public final Integer gpuTempC; // °C or null + public final Integer cpuPercent; + public final Integer gpuPercent; + public final Integer cpuClockMhz; + public final Integer gpuClockMhz; + public final Integer cpuTempC; + public final Integer gpuTempC; public final long ramUsedBytes; public final long ramTotalBytes; - public final float ramPercent; // 0..100 - public final Long vramUsedBytes; // bytes or null - public final Battery battery; // never null - // ---- Mega-only extras ---- - public final int[] perCorePercent; // per-core 0..100, -1 unknown (never null; may be empty) - public final int[] perCoreClockMhz; // per-core MHz, 0 unknown (never null; may be empty) - public final Long swapUsedBytes; // bytes or null (no swap) - public final Long swapTotalBytes; // bytes or null - public final Long netDownBps; // bytes/sec down, or null (first sample / unreadable) - public final Long netUpBps; // bytes/sec up, or null + public final float ramPercent; + public final Long vramUsedBytes; + public final Battery battery; + public final int[] perCorePercent; + public final int[] perCoreClockMhz; + public final Long swapUsedBytes; + public final Long swapTotalBytes; + public final Long netDownBps; + public final Long netUpBps; Snapshot(Integer cpuPercent, Integer gpuPercent, Integer cpuClockMhz, Integer gpuClockMhz, Integer cpuTempC, Integer gpuTempC, long ramUsedBytes, long ramTotalBytes, @@ -1066,7 +805,6 @@ public class HudMetrics { } } - /** One collection pass over every metric. Call from the HUD's own ~1 s refresh thread, never draw. */ public Snapshot snapshot() { if (mango != null) return mangoSnapshot(mango); Integer cpu = getCpuUsagePercent(); @@ -1102,7 +840,6 @@ public class HudMetrics { net == null ? null : net[0], net == null ? null : net[1]); } - // DroidDeck: the same snapshot with MangoHud's sources and rounding (see MangoReadings). private Snapshot mangoSnapshot(MangoReadings m) { MangoReadings.Cpu cpu = m.cpu(); MangoReadings.Memory mem = m.memory(); @@ -1121,11 +858,6 @@ public class HudMetrics { net == null ? null : net[0], net == null ? null : net[1]); } - // ======================================================================= - // Mega-view extras: per-core CPU, swap, network (best-effort, nullable) - // ======================================================================= - - /** Per-core clock in MHz (each core's {@code scaling_cur_freq}); 0 for a core that can't be read. */ public int[] getPerCoreClockMhz() { int cores = Runtime.getRuntime().availableProcessors(); int[] out = new int[Math.max(0, cores)]; @@ -1136,11 +868,9 @@ public class HudMetrics { return out; } - // Per-core /proc/stat deltas — one prev sample per core, seeded on the first read. private long[] lastCoreTotal = null; private long[] lastCoreIdle = null; - /** Per-core CPU usage 0..100 from the {@code cpuN} /proc/stat deltas; -1 for a core not yet sampled. */ public int[] readPerCorePercent() { int cores = Runtime.getRuntime().availableProcessors(); int[] out = new int[Math.max(0, cores)]; @@ -1153,8 +883,8 @@ public class HudMetrics { try (BufferedReader r = new BufferedReader(new FileReader("/proc/stat"))) { String line; while ((line = r.readLine()) != null) { - if (!line.startsWith("cpu")) break; // cpu lines are first; stop at the block end - if (line.length() < 4 || !Character.isDigit(line.charAt(3))) continue; // skip aggregate "cpu " + if (!line.startsWith("cpu")) break; + if (line.length() < 4 || !Character.isDigit(line.charAt(3))) continue; String[] parts = line.trim().split("\\s+"); int core; try { core = Integer.parseInt(parts[0].substring(3)); } catch (Exception e) { continue; } @@ -1165,8 +895,8 @@ public class HudMetrics { try { long v = Long.parseLong(parts[i]); total += v; - if (i == 4) idle = v; // idle - else if (i == 5) idle += v; // + iowait + if (i == 4) idle = v; + else if (i == 5) idle += v; } catch (NumberFormatException e) { ok = false; break; } } if (!ok) continue; @@ -1179,10 +909,6 @@ public class HudMetrics { } } } catch (Exception ignored) {} - // Frequency fallback for any core /proc/stat couldn't give us — restricted on some firmwares - // (e.g. HONOR / Android 16), which is also why the aggregate getCpuUsagePercent() falls back. - // scaling_cur_freq / cpuinfo_max_freq is a load proxy (per-core clocks ARE readable there), and - // it also seeds the very first sample. Cores with a real /proc/stat delta keep it (no regression). for (int i = 0; i < out.length; i++) { if (out[i] >= 0) continue; Long cur = readLongFromLine("/sys/devices/system/cpu/cpu" + i + "/cpufreq/scaling_cur_freq"); @@ -1193,7 +919,6 @@ public class HudMetrics { return out; } - /** Swap {used, total} in bytes from /proc/meminfo, or null when there is no swap. */ public long[] getSwapBytes() { long totalKb = -1, freeKb = -1; try (BufferedReader r = new BufferedReader(new FileReader("/proc/meminfo"))) { @@ -1215,10 +940,8 @@ public class HudMetrics { return v == null ? -1 : v; } - // Net rate: cumulative rx/tx bytes + the wall-clock of the previous read → bytes/sec. private long lastNetRx = -1, lastNetTx = -1, lastNetWallMs = 0; - /** Network {down, up} in bytes/sec (all interfaces except loopback), or null on the first/failed read. */ public long[] readNetRateBps() { long rx = 0, tx = 0; boolean any = false; @@ -1241,7 +964,7 @@ public class HudMetrics { long now = SystemClock.elapsedRealtime(); long prevRx = lastNetRx, prevTx = lastNetTx, prevWall = lastNetWallMs; lastNetRx = rx; lastNetTx = tx; lastNetWallMs = now; - if (prevRx < 0 || prevWall <= 0) return null; // seed + if (prevRx < 0 || prevWall <= 0) return null; long dt = now - prevWall; if (dt <= 0) return null; long down = Math.max(0L, (rx - prevRx) * 1000L / dt); @@ -1249,20 +972,8 @@ public class HudMetrics { return new long[]{down, up}; } - // ======================================================================= - // Diagnostics report (invoked ONLY on user tap — never on the HUD refresh path) - // ======================================================================= - - /** - * One-tap plain-text device report for the "Export HUD diagnostics" action: for every metric it - * lists the resolved source path/API + current value, or MISS + the exact candidates it tried, - * followed by an "exists on this device" dump (thermal zone types, devfreq/kgsl/mali nodes) so a - * device owner can send back the real path for anything that missed. Does a short prime+sample so - * delta-based readers (CPU%/GPU%/net) report a live value. Run OFF the main thread. - */ public String buildDiagnosticsReport(Context ctxIn) { final Context ctx = ctxIn != null ? ctxIn : this.context; - // Prime the delta-based readers, wait, then take a live sample. snapshot(); try { Thread.sleep(350L); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); } Snapshot s = snapshot(); @@ -1281,7 +992,6 @@ public class HudMetrics { sb.append("\n== Metrics ==\n"); - // ---- CPU ---- boolean procStat = new File("/proc/stat").canRead(); if (s.cpuPercent != null) { metricHit(sb, "CPU %", s.cpuPercent + "%", @@ -1292,7 +1002,6 @@ public class HudMetrics { "/sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq"); } - // Per-core % StringBuilder cores = new StringBuilder(); for (int i = 0; i < s.perCorePercent.length; i++) { if (i > 0) cores.append(' '); @@ -1304,7 +1013,6 @@ public class HudMetrics { else metricMiss(sb, "Per-core %", "/proc/stat"); - // Per-core clock StringBuilder coreClk = new StringBuilder(); for (int i = 0; i < s.perCoreClockMhz.length; i++) { if (i > 0) coreClk.append(' '); @@ -1317,7 +1025,6 @@ public class HudMetrics { else metricMiss(sb, "CPU clock", "/sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq"); - // CPU temp String cpuTempPath = resolveTempPath(discoverPrioritizedCpuTempPaths()); if (s.cpuTempC != null && cpuTempPath != null) metricHit(sb, "CPU temp", s.cpuTempC + "°C", cpuTempPath); @@ -1325,7 +1032,6 @@ public class HudMetrics { metricMiss(sb, "CPU temp", "/sys/class/thermal/thermal_zone*/temp (types: cpu*/cpuss/tsens/mtktscpu/s5p-tmu/soc)"); - // ---- GPU ---- String gpuUsagePath = firstReadableGpuUsagePath(); if (s.gpuPercent != null && gpuUsagePath != null) metricHit(sb, "GPU %", s.gpuPercent + "%", gpuUsagePath); @@ -1347,7 +1053,6 @@ public class HudMetrics { "/sys/class/kgsl/kgsl-3d0/temp", "/sys/class/misc/mali0/device/temp", "/sys/class/thermal/thermal_zone*/temp (types: gpu*/gpuss/g3d/kgsl/mali/mtktsgpu)"); - // ---- Memory ---- metricHit(sb, "RAM", s.ramUsedText() + " / " + s.ramTotalText() + " (" + Math.round(s.ramPercent) + "%)", "ActivityManager.MemoryInfo"); if (s.swapUsedBytes != null) @@ -1355,7 +1060,6 @@ public class HudMetrics { else metricMiss(sb, "SWAP", "/proc/meminfo (SwapTotal=0 or unreadable)"); - // VRAM String vramPath = firstReadableVramUsedPath(); if (s.vramUsedBytes != null && vramPath != null) { Long total = readFirstReadableLong(VRAM_TOTAL_PATHS); @@ -1366,14 +1070,12 @@ public class HudMetrics { new ArrayList<>(), VRAM_USED_PATHS); } - // ---- Net ---- if (s.netDownBps != null) metricHit(sb, "NET", formatRate(s.netDownBps) + " down / " + formatRate(s.netUpBps) + " up", "/proc/net/dev"); else metricMiss(sb, "NET", "/proc/net/dev (first sample or unreadable)"); - // ---- Battery ---- Battery b = s.battery; StringBuilder bat = new StringBuilder(); bat.append(b.percent != null ? b.percent + "%" : "?%") @@ -1383,7 +1085,6 @@ public class HudMetrics { .append(b.runtimeText != null ? ", " + b.runtimeText : ""); metricHit(sb, "Battery", bat.toString(), "BatteryManager + /sys/class/power_supply"); - // ---- Exists-on-device dump ---- appendExistsDump(sb); return sb.toString(); } @@ -1415,7 +1116,7 @@ public class HudMetrics { } private String firstReadableVramUsedPath() { - if (vramUsedPathsCache == null) getVramUsedBytes(); // builds the cache + if (vramUsedPathsCache == null) getVramUsedBytes(); if (vramUsedPathsCache == null) return null; for (String p : vramUsedPathsCache) if (sanitizeVramBytes(readLongFromLine(p)) != null) return p; return null; @@ -1479,12 +1180,6 @@ public class HudMetrics { appendGpuNoRootProbes(sb); } - /** Probes for a ROOT-FREE GPU-load path on locked-sysfs Adreno devices (SM8750/HONOR etc.): - * (1) can the app open the KGSL device node? The sysfs mirror (/sys/class/kgsl) is SELinux-blocked - * (vendor_sysfs_kgsl), but /dev/kgsl-3d0 is labeled gpu_device and is normally app-openable — - * that's the door for reading GPU busy via ioctl perf-counters instead of sysfs. - * (2) is there an app-readable devfreq node under /sys/devices/platform that bypasses the blocked - * /sys/class/kgsl symlink (a free win for at least the clock). */ private void appendGpuNoRootProbes(StringBuilder sb) { sb.append("-- GPU no-root reachability probes --\n"); sb.append(" /dev/kgsl-3d0 open O_RDWR: ") @@ -1516,8 +1211,6 @@ public class HudMetrics { if (!found) sb.append(" devfreq (platform kgsl): (no app-readable node found)\n"); } - /** open()+close() a path with the given flags, reporting OK or the errno class — used to test - * whether the app's sandbox can reach a device node without root. */ private static String probeOpen(String path, int flags) { java.io.FileDescriptor fd = null; try { @@ -1530,8 +1223,6 @@ public class HudMetrics { } } - /** Battery-wattage inputs: which power_supply nodes this app can actually READ, and their raw - * values. Confirms the voltage_now / power_now fallback for devices where EXTRA_VOLTAGE is 0. */ private void appendPowerSupplyDump(StringBuilder sb) { sb.append("-- /sys/class/power_supply (battery watts inputs) --\n"); File[] nodes = new File("/sys/class/power_supply").listFiles(); @@ -1582,9 +1273,6 @@ public class HudMetrics { return names.length() == 0 ? "(no readable files)" : names.toString(); } - // ======================================================================= - // Low-level helpers - // ======================================================================= private static int clampPercent(int v) { return v < 0 ? 0 : (v > 100 ? 100 : v); } private static String readFirstLine(String path) { diff --git a/app/src/main/java/com/droiddeck/launcher/hud/KeyValueSet.java b/app/src/main/java/com/droiddeck/launcher/hud/KeyValueSet.java index 1c3a1ba9b..f455d8525 100644 --- a/app/src/main/java/com/droiddeck/launcher/hud/KeyValueSet.java +++ b/app/src/main/java/com/droiddeck/launcher/hud/KeyValueSet.java @@ -8,7 +8,7 @@ * link to https://github.com/The412Banner/FusionHUD in the project documentation * AND in the in-app credits/about screen. * - * Ported to DroidDeck from FusionHUD (Bannerlator's HUD); DroidDeck changes are marked in the code. + * Ported to DroidDeck from FusionHUD (Bannerlator's HUD) and modified for DroidDeck. */ package com.droiddeck.launcher.hud; @@ -20,17 +20,14 @@ import java.util.Iterator; public class KeyValueSet implements Iterable { private String data = ""; - // Default constructor for an empty KeyValueSet public KeyValueSet() { this.data = ""; } - // Constructor to initialize with a string representation of key-value pairs public KeyValueSet(String data) { this.data = data != null && !data.isEmpty() ? data : ""; } - // Helper method to find the start and end indices of a key in the data string private int[] indexOfKey(String key) { int start = 0; int end = data.indexOf(","); @@ -48,37 +45,31 @@ public class KeyValueSet implements Iterable { return null; } - // Method to get the value associated with a key, or an empty string if the key is not present public String get(String key) { for (String[] keyValue : this) if (keyValue[0].equals(key)) return keyValue[1]; return ""; } - // Method to get the value associated with a key, or a fallback value if the key is not present public String get(String key, String fallback) { for (String[] keyValue : this) if (keyValue[0].equals(key)) return keyValue[1]; return fallback; } - // Method to get a boolean value associated with a key, or a fallback value if the key is not present public boolean getBoolean(String key, boolean fallback) { String value = get(key); if (value.isEmpty()) return fallback; return value.equals("1") || value.equals("t") || value.equalsIgnoreCase("true"); } - // Method to get a float value associated with a key, or a fallback value if the key is not present public float getFloat(String key, float fallback) { String value = get(key); try { if (!value.isEmpty()) return Float.parseFloat(value); } catch (NumberFormatException e) { - // Ignore exception and return fallback } return fallback; } - // Method to set a value for a key in the key-value data public void put(String key, Object value) { int[] range = indexOfKey(key); if (range != null) { @@ -88,21 +79,15 @@ public class KeyValueSet implements Iterable { } } - // Method to set a float value for a key in the key-value data public void put(String key, float value) { put(key, String.valueOf(value)); } - // Iterator implementation for key-value pairs in the format [key, value] @NonNull @Override public Iterator iterator() { final int[] start = {0}; final int[] end = {data.indexOf(",")}; - // A single-item config (no comma) gives indexOf == -1, making hasNext() (start < end) - // immediately false so the sole pair was never iterated (get() then returned the - // fallback). Guard it exactly as next() already does. An empty string still yields - // end == 0 → no iteration, which is correct. if (end[0] == -1) end[0] = data.length(); final String[] item = new String[2]; return new Iterator() { diff --git a/app/src/main/java/com/droiddeck/launcher/hud/MangoReadings.kt b/app/src/main/java/com/droiddeck/launcher/hud/MangoReadings.kt index 828bcc0f5..ade7aa656 100644 --- a/app/src/main/java/com/droiddeck/launcher/hud/MangoReadings.kt +++ b/app/src/main/java/com/droiddeck/launcher/hud/MangoReadings.kt @@ -3,12 +3,6 @@ package com.droiddeck.launcher.hud import com.droiddeck.launcher.runtime.LinuxRuntime import java.io.File -/** - * The readings Steam's performance overlay shows in a DroidDeck session (Valve's mangohud 20260902.1, - * tools/mangoapp), taken from the files the session hands it and computed the same way, so the two - * overlays agree. Sources and formulas follow that build: cpu.cpp calculateCPUData, deckard_temp.cpp, - * msm.cpp getQcomGpuInfo, memory.cpp update_meminfo and battery.cpp getPercent / getPower. - */ internal class MangoReadings(private val sessionDir: File, private val rootDir: File) { class Cpu(val percent: Int, val perCore: IntArray) class Memory(val usedBytes: Long, val totalBytes: Long, val swapUsedBytes: Long, val swapTotalBytes: Long) @@ -17,7 +11,6 @@ internal class MangoReadings(private val sessionDir: File, private val rootDir: private val cpuTimes = HashMap() private val cpuPercents = HashMap() - /** Total and per-core load from the session's /proc/stat (DroidDeck's stand-in when the app can't read it). */ @JvmOverloads fun cpu(stat: File = statFile()): Cpu? { val lines = runCatching { stat.readLines() }.getOrNull() ?: return null @@ -38,7 +31,6 @@ internal class MangoReadings(private val sessionDir: File, private val rootDir: File(sessionDir, "proc-stat").takeIf { !File("/proc/stat").canRead() && it.isFile } ?: File("/proc/stat") private fun cpuPercent(key: String, v: LongArray): Int { - // user nice system idle iowait irq softirq steal guest guest_nice; guest time is already in user. val user = v[0] - v[8] val nice = v[1] - v[9] val systemAll = v[2] + v[5] + v[6] @@ -55,7 +47,6 @@ internal class MangoReadings(private val sessionDir: File, private val rootDir: return busy.coerceIn(0f, 100f).toInt().also { cpuPercents[key] = it } } - /** Average of the zones the session types cpuN-thermal, truncated. */ fun cpuTempC(): Int? { var sum = 0f var count = 0 @@ -85,7 +76,6 @@ internal class MangoReadings(private val sessionDir: File, private val rootDir: return (milli / 1000.0).toInt() } - /** MemTotal - MemAvailable and SwapTotal - SwapFree from /proc/meminfo. */ fun memory(): Memory? { val kb = HashMap() runCatching { @@ -102,10 +92,8 @@ internal class MangoReadings(private val sessionDir: File, private val rootDir: return Memory((total - available) * 1024, total * 1024, (swapTotal - swapFree) * 1024, swapTotal * 1024) } - /** The KGSL total DroidDeck shares with the session (GpuMemComponent). */ fun vramBytes(): Long? = fresh(File(rootDir, VRAM_FILE), VRAM_FRESH_MS)?.let(::readLong) - /** BAT* under the session's power_supply (BatteryComponent): no watts while charging, full or unknown. */ fun battery(): Battery? { val dirs = File(sessionDir, "sys/power_supply").listFiles { f -> f.name.contains("BAT") }?.sortedBy { it.name } if (dirs.isNullOrEmpty()) return null @@ -149,7 +137,6 @@ internal class MangoReadings(private val sessionDir: File, private val rootDir: private fun readLong(file: File): Long? = runCatching { file.readText().trim().toLong() }.getOrNull() private fun readFloat(file: File): Float? = runCatching { file.readLines().first().trim().toFloat() }.getOrNull() - /** std::stod on the first line: the leading number, "6 %" -> 6. */ fun leadingNumber(file: File): Double? = runCatching { Regex("^\\s*-?\\d+(\\.\\d+)?").find(file.readLines().first())?.value?.trim()?.toDouble() }.getOrNull() 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 46ce99798..8cbe135e7 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/FakeInputWriter.java +++ b/app/src/main/java/com/droiddeck/launcher/input/FakeInputWriter.java @@ -612,7 +612,6 @@ public class FakeInputWriter { for (int i = 0; i < length; i++) output[9 + i] = ring.get(TRITON_OUTPUT_OFFSET + i); nativeStoreFence(); if (seq != ring.getLong(TRITON_OUTPUT_SEQ_OFFSET)) return null; - // Acknowledge the exact report sequence taken so a concurrent timeout overwrite cannot lose its length. ring.putInt(TRITON_OUTPUT_ACK_OFFSET, (int) seq); nativeStoreFence(); return output; diff --git a/app/src/main/java/com/droiddeck/launcher/input/GesturePrefs.kt b/app/src/main/java/com/droiddeck/launcher/input/GesturePrefs.kt index b8be5bf54..55588d6fd 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/GesturePrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/GesturePrefs.kt @@ -2,7 +2,6 @@ package com.droiddeck.launcher.input import android.content.Context -/** Global defaults with optional overrides for the currently focused game. */ object GesturePrefs { private fun prefs(context: Context) = context.getSharedPreferences("touch_gestures", Context.MODE_PRIVATE) diff --git a/app/src/main/java/com/droiddeck/launcher/input/GestureTouchStreamGate.kt b/app/src/main/java/com/droiddeck/launcher/input/GestureTouchStreamGate.kt index 68d5722d0..4d69f68c5 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/GestureTouchStreamGate.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/GestureTouchStreamGate.kt @@ -2,7 +2,6 @@ package com.droiddeck.launcher.input import android.view.MotionEvent -/** Keeps split Android touch targets from restarting one shared gesture recognizer. */ internal class GestureTouchStreamGate { enum class Source { SURFACE, SCREEN_KEYS, ON_SCREEN_CONTROLS } diff --git a/app/src/main/java/com/droiddeck/launcher/input/HomeButtonService.kt b/app/src/main/java/com/droiddeck/launcher/input/HomeButtonService.kt index 21ca4717d..61dfe32c4 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/HomeButtonService.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/HomeButtonService.kt @@ -37,7 +37,6 @@ object HomeButtonShortcuts { } fun onKeyEvent(context: Context, event: KeyEvent): Boolean { - // AYN maps its system-assistant key (gpio-keys KEY_F24) to HOME too; only the pad's Home is Guide. if (!HomeGuideInput.isHome(event.keyCode) || !PadBridge.isControllerKeyEvent(event)) return false return input.onKeyEvent(event, enabled() && HomeApp.isDefault(context)) } diff --git a/app/src/main/java/com/droiddeck/launcher/input/HomeGuideInput.kt b/app/src/main/java/com/droiddeck/launcher/input/HomeGuideInput.kt index fd289f734..e4d712044 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/HomeGuideInput.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/HomeGuideInput.kt @@ -46,7 +46,6 @@ class HomeGuideInput(private val setPressed: (Boolean) -> Unit, private val onCa } companion object { - // The Retroid Nova pad falls back to Generic.kl, which maps its Home scan code to MOVE_HOME. fun isHome(keyCode: Int) = keyCode == KeyEvent.KEYCODE_HOME || keyCode == KeyEvent.KEYCODE_MOVE_HOME private const val DUO_QUICK_MENU_SCAN_CODE = 194 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 b7fced710..8f546e8b0 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/OnScreenControls.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/OnScreenControls.kt @@ -672,7 +672,6 @@ class OnScreenControls( private fun closestSnap(position: Float, targets: List): Float? = targets.minByOrNull { abs(position - it) }?.takeIf { abs(position - it) <= dp(SNAP_DP) } - /** Whether a finger going down at ([x], [y]) lands on something here, as [onTouchEvent] decides it. */ fun takesTouchAt(x: Float, y: Float): Boolean = editing || keyboardContains(x, y) || (!buttonsOnly && quickContains(x, y)) || (!quickHidden && (controlAt(x, y) ?: adaptiveStickAt(x, y)) != null) 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 7dd234daf..edb95fbe6 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/PadBridge.java +++ b/app/src/main/java/com/droiddeck/launcher/input/PadBridge.java @@ -250,11 +250,6 @@ public final class PadBridge { || device.supportsSource(InputDevice.SOURCE_JOYSTICK)); } - public static boolean isControllerKey(KeyEvent event) { - return isFromController(event.getDevice()) - && isPadKey(event.getKeyCode(), event.getDevice().getKeyboardType()); - } - public static boolean isPadKey(int keyCode, int keyboardType) { if ((keyCode >= KeyEvent.KEYCODE_BUTTON_A && keyCode <= KeyEvent.KEYCODE_BUTTON_MODE) || (keyCode >= KeyEvent.KEYCODE_BUTTON_1 && keyCode <= KeyEvent.KEYCODE_BUTTON_16)) return true; @@ -616,7 +611,6 @@ public final class PadBridge { private void writePad(PadState report) { if (backQamAxesBlocked && !hasAxes(state)) backQamAxesBlocked = false; long at = android.os.SystemClock.uptimeMillis(); - // Steam must see the modifier before a partner; preserve quick press/release pairs in order. if (backQamPressed || homeGuidePressed || !backQamWrites.isEmpty()) at += backQamDelayMs; writeBackQamReport(report, at); } diff --git a/app/src/main/java/com/droiddeck/launcher/input/ScreenKey.kt b/app/src/main/java/com/droiddeck/launcher/input/ScreenKey.kt index 2967a06f6..422375432 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/ScreenKey.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/ScreenKey.kt @@ -36,12 +36,6 @@ fun screenKeyDirections(shape: Int): List = when (shape) { else -> emptyList() } -val SCREEN_KEY_SHAPES = - listOf( - SCREEN_KEY_PILL, SCREEN_KEY_ROUND, SCREEN_KEY_CIRCLE, SCREEN_KEY_RECT, - SCREEN_KEY_TRIGGER, SCREEN_KEY_DPAD, SCREEN_KEY_STICK, - ) - data class ScreenKey( val codes: List, val x: Float, @@ -55,9 +49,4 @@ data class ScreenKey( val toggle: Boolean = false, val labels: Boolean = true, ) { - fun shownLabel(context: Context): String = when { - text.isNotEmpty() -> text - glyph.isNotEmpty() -> glyph - else -> screenKeyLabel(context, codes) - } } diff --git a/app/src/main/java/com/droiddeck/launcher/input/ScreenKeys.kt b/app/src/main/java/com/droiddeck/launcher/input/ScreenKeys.kt index df129888c..be1f658f3 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/ScreenKeys.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/ScreenKeys.kt @@ -609,7 +609,6 @@ class ScreenKeys(context: Context, private val sendKey: (Int, Boolean) -> Unit) else -> entry.halfH } - /** Whether a finger going down at ([x], [y]) lands on something here, as [onTouchEvent] decides it. */ fun takesTouchAt(x: Float, y: Float): Boolean = editing || entryAt(x, y) != null private fun entryAt(x: Float, y: Float): Entry? = entries.lastOrNull { entry -> 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 38f7f76d9..0f18132ef 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SecondScreenMode.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SecondScreenMode.kt @@ -19,7 +19,6 @@ enum class SecondScreenMode(val id: String, @StringRes val label: Int) { it != NONE && it.id == remembered && (it != DECK_CONTROLS || deckPad) && (it != DASHBOARD || dashboard) && (it != DESKTOP || desktop) }?.let { return it } - // No pick yet: the dashboard. A pick that can't open right now keeps the old fallback. val picked = entries.any { it != NONE && it.id == remembered } return when { dashboard && !picked -> DASHBOARD 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 68eed5ede..672cf9b47 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchControls.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchControls.kt @@ -112,7 +112,6 @@ class SteamTouchControls( private var selected = -1 private var dragOffset = 0f to 0f private var pinchStart = 0f - private var pinchScale = 1f private var saving = false private val pendingBindings = HashMap() private var pendingColor: FloatArray? = null @@ -244,7 +243,6 @@ class SteamTouchControls( private fun hit(list: List, x: Float, y: Float): Element? = list.filter { reach(it, x, y) <= 1.15f }.minByOrNull { reach(it, x, y) } - /** Whether a finger going down at ([x], [y]) lands on something here, as [onTouchEvent] decides it. */ fun takesTouchAt(x: Float, y: Float): Boolean = editing || menuOpen || hit(live(), x, y) != null private fun live(): List = @@ -350,17 +348,6 @@ class SteamTouchControls( canvas.restore() } - 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 - canvas.drawCircle(x + r * 0.28f, y - r * 0.18f, r * 0.27f, p) - canvas.drawCircle(x + r * 0.28f, y - r * 0.18f, r * 0.1f, Paint(p).apply { style = Paint.Style.FILL }) - canvas.drawCircle(x - r * 0.3f, y + r * 0.3f, r * 0.17f, p) - p.strokeWidth = r * 0.2f - canvas.drawLine(x + r * 0.1f, y - r * 0.02f, x - r * 0.2f, y + r * 0.22f, p) - canvas.drawLine(x - r * 0.45f, y + r * 0.28f, x - r * 0.95f, y + r * 0.1f, p) - } - private fun drawFace(canvas: Canvas, type: Int, binding: SteamTouchBindings.Binding?, x: Float, y: Float, r: Float, alpha: Int, pressed: Boolean) { val square = isSquare(type) @@ -855,19 +842,6 @@ class SteamTouchControls( invalidate() } - private fun controlName(type: Int) = when (type) { - 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 -> context.getString(R.string.touch_menu) - SteamTouchConfig.KEYBOARD -> context.getString(R.string.touch_keyboard) - else -> label(type) - } - private fun moreMenu() { val host = parent as? android.view.ViewGroup ?: return val density = resources.displayMetrics.density 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 2d63358d3..560bff4f0 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchDevice.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchDevice.kt @@ -74,8 +74,6 @@ class SteamTouchDevice private constructor(private val buffer: ByteBuffer) { Log.i(TAG, "steam touch: " + if (value) "plugged in" else "unplugged") } - fun setBattery(percent: Int) = buffer.putInt(OFF_BATTERY, percent.coerceIn(0, 100)) - fun state(): State { repeat(8) { val seq = buffer.getLong(OFF_STATE_SEQ) diff --git a/app/src/main/java/com/droiddeck/launcher/input/SteamVolumeKeys.kt b/app/src/main/java/com/droiddeck/launcher/input/SteamVolumeKeys.kt index 3e0ca925a..062bfbfdc 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SteamVolumeKeys.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SteamVolumeKeys.kt @@ -3,7 +3,6 @@ package com.droiddeck.launcher.input import android.media.AudioManager import android.view.KeyEvent -/** Use Steam's PulseAudio bridge for feedback instead of requesting Android's volume UI. */ internal object SteamVolumeKeys { fun dispatch(event: KeyEvent, enabled: Boolean, adjust: (Int) -> Unit): Boolean { val direction = when (event.keyCode) { diff --git a/app/src/main/java/com/droiddeck/launcher/input/TouchscreenGestures.java b/app/src/main/java/com/droiddeck/launcher/input/TouchscreenGestures.java index 140b4ace4..819226764 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/TouchscreenGestures.java +++ b/app/src/main/java/com/droiddeck/launcher/input/TouchscreenGestures.java @@ -28,7 +28,6 @@ public final class TouchscreenGestures { void key(int code, boolean down); void wheel(int steps); void showKeyboard(); - /** Establish the pointer origin before a relative middle-mouse pan begins. */ default void prepareRelativePan() {} default void gesture(Gesture gesture) {} } @@ -97,12 +96,10 @@ public final class TouchscreenGestures { final int code; MouseButton(int code) { this.code = code; } } - // Long-press detection private Runnable longPressRunnable; private boolean longPressTriggered; private boolean longPressActionHeld, twoFingerHoldActionHeld, threeFingerHoldActionHeld; - // Double-tap detection private long lastTapTime; private float lastTapX; private float lastTapY; @@ -110,76 +107,54 @@ public final class TouchscreenGestures { private float doubleTapAnchorX, doubleTapAnchorY; private long singleFingerDownTime; - // Drag tracking (box selection) private boolean isDragging; private boolean dragButtonPressed; - // Two-finger tracking private boolean twoFingerDragging; private float twoFingerLastX0, twoFingerLastY0; private float twoFingerLastX1, twoFingerLastY1; private boolean twoFingerMiddleButtonDown; - // Pinch tracking private float pinchLastDistance; private float pinchZoomRemainder; private float keyPanRemainderX, keyPanRemainderY; private static final float PINCH_STEP_PX = 10f; - // Two-finger tap detection private boolean twoFingerTapPossible; private boolean twoFingerTapFired; - // Track which WASD/arrow keys are currently held private boolean panKeyUp, panKeyDown, panKeyLeft, panKeyRight; - // True when finger has moved beyond tap tolerance (prevents spurious tap on lift) private boolean movedBeyondTapThreshold; - // True after a multi-finger gesture has been active — prevents the remaining - // finger from starting a spurious single-finger drag on lift sequence. private boolean multiFingerGestureUsed; - // Remember which keycodes were actually pressed so releasePanKeys releases only those private Integer panLeftKey, panRightKey, panUpKey, panDownKey; - // Long-press movement tolerance (pixels of raw finger movement before cancelling long-press) private static final float LONG_PRESS_MOVE_TOLERANCE = 50f; - // Raw touch-down coordinates for measuring finger travel private float touchDownRawX, touchDownRawY; - // Last raw coordinates for single-finger drag delta private float singleFingerLastRawX, singleFingerLastRawY; - // Two-finger gesture mutual exclusion (only pan OR pinch, not both) private static final int TWO_FINGER_GESTURE_NONE = 0; private static final int TWO_FINGER_GESTURE_PAN = 1; private static final int TWO_FINGER_GESTURE_ZOOM = 2; private int twoFingerGestureMode = TWO_FINGER_GESTURE_NONE; - // Accumulated deltas for deciding which two-finger gesture to lock into private float accumulatedPinchDelta, accumulatedPanDelta; - // Settle window: ignore movement for first N ms after second finger lands (#1) - // Also serves as a grace period for a third finger to arrive before 2F locks in private static final long TWO_FINGER_SETTLE_MS = 120; private static final long THREE_FINGER_SETTLE_MS = 50; private long twoFingerDownTime; private long threeFingerDownTime; - // Velocity gating for two-finger hold vs drag (#2) private long twoFingerLastMoveTime; - private static final float MIN_DRAG_VELOCITY = 0.3f; // px/ms to commit to drag + private static final float MIN_DRAG_VELOCITY = 0.3f; - // Pinch vs drag ratio: pinch delta must exceed pan by this factor to classify as pinch (#4) private static final float PINCH_RATIO_THRESHOLD = 1.5f; - // Cancel-and-reclassify: allow reclassification within this window after lock-in (#5) private static final long RECLASSIFY_WINDOW_MS = 100; private long twoFingerLockTime; private float reclassifyPinchDelta, reclassifyPanDelta; - // Gesture pointer ownership tracking (prevents d-pad/button pointer conflicts) private int gestureFingerCount = 0; private final ArrayList gestureOwnedPointerIds = new ArrayList<>(); private final int[] multiFingerStartIds = new int[3]; private final float[] multiFingerStartX = new float[3], multiFingerStartY = new float[3]; private int multiFingerStartCount; - // Two-finger hold detection private Runnable twoFingerHoldRunnable; private boolean twoFingerHoldTriggered; - // Three-finger tracking private boolean threeFingerTapPossible; private boolean threeFingerTapFired; private boolean threeFingerDragging; @@ -199,9 +174,7 @@ public final class TouchscreenGestures { switch (action) { case MotionEvent.ACTION_DOWN: - // A new touch must not shorten or discard an already recognized click. handleTsCancel(true); - // First pointer overall — gesture system claims it gestureOwnedPointerIds.clear(); gestureOwnedPointerIds.add(pointerId); gestureFingerCount = 1; @@ -209,13 +182,11 @@ public final class TouchscreenGestures { break; case MotionEvent.ACTION_POINTER_DOWN: - // New pointer arrived — gesture system claims it if (!gestureOwnedPointerIds.contains(pointerId)) { gestureOwnedPointerIds.add(pointerId); gestureFingerCount++; } if (gestureFingerCount == 1) { - // First gesture finger (controls own earlier pointers) handleTsDown(event, actionIndex); } else if (gestureFingerCount == 2) { handleTsPointerDown(event); @@ -242,15 +213,12 @@ public final class TouchscreenGestures { gestureOwnedPointerIds.remove(Integer.valueOf(pointerId)); gestureFingerCount = Math.max(0, gestureFingerCount - 1); if (prevCount == 3 && gestureFingerCount == 2) { - // Three-finger gesture ending handleTsThreeFingerUp(event); } else if (prevCount == 2 && gestureFingerCount == 1) { - // Two-finger gesture ending (2→1) handleTsPointerUp(event); } else if (prevCount >= 4) { if (removedIndex < 3) rebaseThreeFingerMovement(event); } else if (prevCount >= 1 && gestureFingerCount == 0) { - // Last gesture finger lifted while controls still hold pointers handleTsUp(event, actionIndex); } } @@ -261,9 +229,6 @@ public final class TouchscreenGestures { int prevCount = gestureFingerCount; gestureOwnedPointerIds.remove(Integer.valueOf(pointerId)); gestureFingerCount = Math.max(0, gestureFingerCount - 1); - // Clean up multi-finger state if needed, routing by previous - // gesture mode so a 3-finger gesture isn't ended via the - // two-finger cleanup path. if (prevCount >= 3) { handleTsThreeFingerUp(event); } else if (prevCount == 2) { @@ -271,7 +236,6 @@ public final class TouchscreenGestures { } handleTsUp(event, actionIndex); } - // ACTION_UP = last pointer overall — full cleanup gestureOwnedPointerIds.clear(); gestureFingerCount = 0; break; @@ -309,15 +273,12 @@ public final class TouchscreenGestures { threeFingerHoldTriggered = false; accumulatedThreeFingerDelta = 0; - // Store raw touch coordinates for long-press movement tolerance touchDownRawX = event.getX(actionIndex); touchDownRawY = event.getY(actionIndex); singleFingerDownTime = SystemClock.uptimeMillis(); singleFingerLastRawX = touchDownRawX; singleFingerLastRawY = touchDownRawY; - // Keep a nearby re-tap undecided until UP: it can still become a drag, - // long press, or multi-finger gesture instead of a double click. if (isDoubleTap(event, actionIndex)) { secondTapCandidate = true; doubleTapAnchorX = lastTapX; @@ -327,7 +288,6 @@ public final class TouchscreenGestures { if (!secondTapCandidate && delayedPress == null && pendingTapPress == null) moveCursorTo(x, y); - // Schedule long-press if (gestureConfig.getLongPressEnabled()) { longPressRunnable = () -> { secondTapCandidate = false; @@ -342,7 +302,6 @@ public final class TouchscreenGestures { postDelayed(longPressRunnable, gestureConfig.getLongPressDelay()); } - // Tap: immediate cursor move (press is deferred to UP for click, or MOVE for drag) } private void handleTsPointerDown(MotionEvent event) { @@ -351,11 +310,6 @@ public final class TouchscreenGestures { flushPendingTapRelease(); lastTapTime = 0; flushPendingHoldClickRelease(); - // Cancel any single-finger long-press. cancelLongPressTimer only - // clears the pending runnable; if the long-press has already fired - // we must also release the held action before we transition into a - // multi-finger gesture, otherwise the long-press button/key stays - // down for the rest of the touch sequence. if (longPressTriggered) { longPressActionHeld = releaseHoldAction(longPressActionHeld, gestureConfig.getLongPressAction()); longPressTriggered = false; @@ -388,7 +342,6 @@ public final class TouchscreenGestures { event.getY(idx0) - event.getY(idx1)); recordMultiFingerOrigins(event, 2); } - // Schedule two-finger hold if (gestureConfig.getTwoFingerHoldEnabled()) { twoFingerHoldRunnable = () -> { twoFingerHoldTriggered = true; @@ -404,33 +357,26 @@ public final class TouchscreenGestures { } private void handleTsMove(MotionEvent event, int pointerIndex) { - // After a multi-finger gesture, ignore single-finger movement until all fingers lift if (multiFingerGestureUsed) return; int x = (int) event.getX(pointerIndex); int y = (int) event.getY(pointerIndex); - // If long press already triggered, ignore movement (right-click already sent) if (longPressTriggered) return; - // Measure raw finger travel from initial touch point float rawTravel = maxFingerTravel(event, pointerIndex, touchDownRawX, touchDownRawY); if (secondTapCandidate && (rawTravel > doubleTapSlop || maxFingerTravel(event, pointerIndex, doubleTapAnchorX, doubleTapAnchorY) >= doubleTapSlop)) { secondTapCandidate = false; } - // The threshold decides when a drag starts. An active drag must keep - // following the finger even when it returns through the touch origin. if (!isDragging && rawTravel <= LONG_PRESS_MOVE_TOLERANCE) { return; } - // Beyond tolerance — cancel long press cancelLongPressTimer(); movedBeyondTapThreshold = true; - // Detect drag start if (!isDragging && gestureConfig.getDragEnabled()) { flushPendingTapRelease(); String dragAction = gestureConfig.getDragAction(); @@ -438,14 +384,7 @@ public final class TouchscreenGestures { dragButtonPressed = true; singleFingerLastRawX = event.getX(pointerIndex); singleFingerLastRawY = event.getY(pointerIndex); - // Send the configured drag press immediately so that a short - // move followed straight by UP still produces a press+release - // pair. A zero-delta performPanAction just presses the button - // (or is a no-op for the key-pan variants, which press per - // direction on their next move frame). if (isClickDragAction(dragAction)) { - // The candidate's cursor stayed at the previous click. Start - // a drag at this touch's own origin before pressing its button. moveCursorTo((int) touchDownRawX, (int) touchDownRawY); if (useRelativeMouseDragMovement()) { pressClickDragAction(dragAction); @@ -456,7 +395,6 @@ public final class TouchscreenGestures { performPanAction(0f, 0f, dragAction); } notifyGesture(Gesture.DRAG); - // Non-click pan actions continue with real deltas on the next frame. return; } @@ -481,14 +419,11 @@ public final class TouchscreenGestures { return; } - // Move cursor moveCursorTo(x, y); } private void handleTsTwoFingerMove(MotionEvent event) { if (multiFingerGestureUsed) return; - // If a two-finger hold has already fired, ignore subsequent jitter so - // it can't lock into a pan/zoom gesture mid-hold. if (twoFingerHoldTriggered) return; if (gestureFingerCount < 2 || gestureOwnedPointerIds.size() < 2) return; @@ -496,20 +431,15 @@ public final class TouchscreenGestures { int idx1 = event.findPointerIndex(gestureOwnedPointerIds.get(1)); if (idx0 < 0 || idx1 < 0) return; - // Settling delays recognition, not movement tracking. A swipe or pinch - // must stop being a tap/hold even when it finishes inside that delay. if (multiFingerMovementExceeded(event)) { twoFingerTapPossible = false; cancelTwoFingerHoldTimer(); } - // Slow deliberate travel can cross touch slop before reaching the - // pan/zoom threshold. It must not turn into a stationary hold first. if (multiFingerMovementExceeded(event, true)) cancelTwoFingerHoldTimer(); float x0 = event.getX(idx0), y0 = event.getY(idx0); float x1 = event.getX(idx1), y1 = event.getY(idx1); - // Compute per-frame pan and pinch deltas float currDist = (float) Math.hypot(x0 - x1, y0 - y1); float framePinchDelta = Math.abs(currDist - pinchLastDistance); @@ -523,7 +453,6 @@ public final class TouchscreenGestures { long now = SystemClock.uptimeMillis(); if (now - twoFingerDownTime < TWO_FINGER_SETTLE_MS) { - // Still settling — update stored positions but don't accumulate deltas twoFingerLastX0 = x0; twoFingerLastY0 = y0; twoFingerLastX1 = x1; twoFingerLastY1 = y1; pinchLastDistance = currDist; @@ -531,14 +460,12 @@ public final class TouchscreenGestures { } long frameTime = now - twoFingerLastMoveTime; twoFingerLastMoveTime = now; - float velocity = (frameTime > 0) ? framePanDelta / frameTime : 0; // px/ms + float velocity = (frameTime > 0) ? framePanDelta / frameTime : 0; - // Accumulate deltas until we have enough movement to decide gesture type if (twoFingerGestureMode == TWO_FINGER_GESTURE_NONE) { accumulatedPinchDelta += framePinchDelta; accumulatedPanDelta += framePanDelta; if (accumulatedPinchDelta + accumulatedPanDelta > (float) gestureConfig.getGestureThreshold()) { - // Enough movement to lock into a gesture — no longer a tap or hold twoFingerTapPossible = false; cancelTwoFingerHoldTimer(); if (accumulatedPinchDelta > accumulatedPanDelta * PINCH_RATIO_THRESHOLD @@ -551,7 +478,6 @@ public final class TouchscreenGestures { twoFingerLockTime = now; notifyGesture(Gesture.TWO_FINGER_DRAG); } else if (accumulatedPanDelta >= accumulatedPinchDelta && gestureConfig.getTwoFingerDragEnabled()) { - // Fallback: pan is dominant but velocity was too low — still drag twoFingerGestureMode = TWO_FINGER_GESTURE_PAN; twoFingerLockTime = now; notifyGesture(Gesture.TWO_FINGER_DRAG); @@ -571,7 +497,7 @@ public final class TouchscreenGestures { reclassifyPinchDelta += framePinchDelta; reclassifyPanDelta += framePanDelta; float reclassifyTotal = reclassifyPinchDelta + reclassifyPanDelta; - if (reclassifyTotal > 20f) { // enough post-lock data to reclassify + if (reclassifyTotal > 20f) { boolean shouldBeZoom = reclassifyPinchDelta > reclassifyPanDelta * PINCH_RATIO_THRESHOLD; if (shouldBeZoom && twoFingerGestureMode == TWO_FINGER_GESTURE_PAN && gestureConfig.getPinchEnabled()) { @@ -585,7 +511,7 @@ public final class TouchscreenGestures { resetSmallMovement(); notifyGesture(Gesture.TWO_FINGER_DRAG); } - twoFingerLockTime = 0; // done reclassifying + twoFingerLockTime = 0; reclassifyPinchDelta = 0; reclassifyPanDelta = 0; } @@ -595,12 +521,10 @@ public final class TouchscreenGestures { pinchZoomRemainder = accumulateSignedMovement(pinchZoomRemainder, delta); if (Math.abs(pinchZoomRemainder) >= PINCH_STEP_PX) { performZoomAction(pinchZoomRemainder > 0); - // Keep the fractional travel, without queuing a burst of old - // zoom actions after a large move or a direction reversal. pinchZoomRemainder %= PINCH_STEP_PX; } } - pinchLastDistance = currDist; // Always update baseline to avoid stale deltas + pinchLastDistance = currDist; if (twoFingerGestureMode == TWO_FINGER_GESTURE_PAN) { String twoFingerDragAction = gestureConfig.getTwoFingerDragAction(); if (isClickDragAction(twoFingerDragAction)) { @@ -627,14 +551,11 @@ public final class TouchscreenGestures { private void handleTsPointerUp(MotionEvent event) { cancelTwoFingerHoldTimer(); if (multiFingerMovementExceeded(event)) twoFingerTapPossible = false; - // Two-finger hold release if (twoFingerHoldTriggered) { twoFingerHoldActionHeld = releaseHoldAction(twoFingerHoldActionHeld, gestureConfig.getTwoFingerHoldAction()); twoFingerHoldTriggered = false; } - // Two-finger tap detection else if (twoFingerTapPossible && !twoFingerDragging && gestureConfig.getTwoFingerTapEnabled()) { - // Move cursor to midpoint between the two fingers float midX = (twoFingerLastX0 + twoFingerLastX1) / 2f; float midY = (twoFingerLastY0 + twoFingerLastY1) / 2f; queueTapClick(gestureConfig.getTwoFingerTapAction(), midX, midY); @@ -652,7 +573,6 @@ public final class TouchscreenGestures { reclassifyPinchDelta = 0; reclassifyPanDelta = 0; twoFingerLockTime = 0; - // Suppress single-finger tap/drag when last finger lifts after any 2F gesture movedBeyondTapThreshold = true; multiFingerGestureUsed = true; } @@ -661,7 +581,6 @@ public final class TouchscreenGestures { cancelLongPressTimer(); if (longPressTriggered) { - // Release the long-press action longPressActionHeld = releaseHoldAction(longPressActionHeld, gestureConfig.getLongPressAction()); longPressTriggered = false; return; @@ -671,7 +590,6 @@ public final class TouchscreenGestures { float y = event.getY(actionIndex); if (isDragging && dragButtonPressed) { - // End drag (release any held drag buttons/keys) releaseDragInput(); dragButtonPressed = false; isDragging = false; @@ -685,8 +603,6 @@ public final class TouchscreenGestures { ? gestureConfig.getLongPressDelay() : ViewConfiguration.getLongPressTimeout(); if (SystemClock.uptimeMillis() - singleFingerDownTime < tapLimit && Math.hypot(x - doubleTapAnchorX, y - doubleTapAnchorY) < doubleTapSlop) { - // A normal left tap already supplied the first click. Other - // single-tap mappings need a complete synthesized double click. boolean firstTapClicked = gestureConfig.getTapEnabled() && TouchGestureConfig.ACTION_LEFT_CLICK.equals(gestureConfig.getTapAction()); int clicks = firstTapClicked ? 1 : 2; @@ -697,26 +613,21 @@ public final class TouchscreenGestures { } } - // Two-finger tap already sent its click on POINTER_UP — skip the tap on UP if (twoFingerTapFired) { twoFingerTapFired = false; return; } - // Three-finger tap already sent its click — skip the tap on UP if (threeFingerTapFired) { threeFingerTapFired = false; return; } - // Simple tap — only if finger stayed within tap tolerance if (gestureConfig.getTapEnabled() && !movedBeyondTapThreshold) { queueTapClick(gestureConfig.getTapAction(), x, y); notifyGesture(Gesture.TAP); } - // Record for double-tap detection (even if tap itself is disabled, - // so double-tap can still prime from single touch-ups) if (!movedBeyondTapThreshold && (gestureConfig.getTapEnabled() || gestureConfig.getDoubleTapEnabled())) { lastTapTime = SystemClock.uptimeMillis(); lastTapX = x; @@ -776,13 +687,10 @@ public final class TouchscreenGestures { cancelLongPressTimer(); cancelDrag(); cancelTwoFingerHoldTimer(); - // If a two-finger hold already injected its action, release it before - // transitioning so the held button/key doesn't stay down. if (twoFingerHoldTriggered) { twoFingerHoldActionHeld = releaseHoldAction(twoFingerHoldActionHeld, gestureConfig.getTwoFingerHoldAction()); twoFingerHoldTriggered = false; } - // Clean up two-finger state releaseDragInput(); resetSmallMovement(); twoFingerDragging = false; @@ -797,7 +705,6 @@ public final class TouchscreenGestures { threeFingerDragOffsetX = threeFingerDragOffsetY = 0f; threeFingerDownTime = SystemClock.uptimeMillis(); - // Compute midpoint of three gesture fingers if (gestureOwnedPointerIds.size() >= 3) { float sumX = 0, sumY = 0; int validCount = 0; @@ -809,13 +716,12 @@ public final class TouchscreenGestures { validCount++; } } - if (validCount < 3) return; // Can't proceed without all 3 pointers + if (validCount < 3) return; threeFingerLastMidX = sumX / 3f; threeFingerLastMidY = sumY / 3f; recordMultiFingerOrigins(event, 3); } - // Schedule three-finger hold if (gestureConfig.getThreeFingerHoldEnabled()) { threeFingerHoldRunnable = () -> { threeFingerHoldTriggered = true; @@ -831,8 +737,6 @@ public final class TouchscreenGestures { private void handleTsThreeFingerMove(MotionEvent event) { if (gestureFingerCount < 3 || gestureOwnedPointerIds.size() < 3) return; - // If a three-finger hold has already fired, ignore subsequent jitter so - // it can't lock into a pan gesture mid-hold. if (threeFingerHoldTriggered) return; if (multiFingerMovementExceeded(event)) { threeFingerTapPossible = false; @@ -877,8 +781,6 @@ public final class TouchscreenGestures { } if (threeFingerGestureMode == THREE_FINGER_GESTURE_PAN) { - // Once recognized, retain small mouse movements. Keyboard panning - // accumulates its own direction threshold in performKeyPan. String threeFingerDragAction = gestureConfig.getThreeFingerDragAction(); if (isClickDragAction(threeFingerDragAction)) { if (useRelativeMouseDragMovement()) { @@ -919,7 +821,6 @@ public final class TouchscreenGestures { multiFingerStartCount = 0; resetSmallMovement(); threeFingerGestureMode = THREE_FINGER_GESTURE_NONE; - // Suppress single-finger tap/drag when last finger lifts after any 3F gesture movedBeyondTapThreshold = true; multiFingerGestureUsed = true; } @@ -966,7 +867,6 @@ public final class TouchscreenGestures { } x /= 3f; y /= 3f; - // Replacing a tracked finger changes the centroid without moving the cursor. if (threeFingerDragging) { threeFingerDragOffsetX += threeFingerLastMidX - x; threeFingerDragOffsetY += threeFingerLastMidY - y; @@ -1030,9 +930,6 @@ public final class TouchscreenGestures { private void startPendingTapClick() { if (delayedPress != null || pendingTapPress != null || pendingTapClicks.isEmpty()) return; TapClick click = pendingTapClicks.peekFirst(); - // Games that poll button state need an observable UP interval too. - // Reuse the first tap's location: normal finger drift must not turn - // this into two clicks on different targets or a tiny drag. long delay = Math.max(0, CLICK_DELAYED_TIME - (SystemClock.uptimeMillis() - lastInputReleaseTime)); pendingTapPress = () -> { pendingTapPress = null; @@ -1224,9 +1121,6 @@ public final class TouchscreenGestures { pressButton(MouseButton.BUTTON_MIDDLE); twoFingerMiddleButtonDown = true; } - // Pan from the client's current pointer. Games can recenter the mouse - // when the middle button goes down; replaying our old absolute touch - // anchor after that warp causes a large, unintended camera movement. moveCursorByRelativeDelta(dx, dy); } @@ -1287,10 +1181,6 @@ public final class TouchscreenGestures { } private void performKeyPan(float dx, float dy, Integer leftKey, Integer rightKey, Integer upKey, Integer downKey) { - // When fingers are actively moving, update key states to match the - // swipe direction. When fingers stop moving but stay on screen, - // keep the current keys held — movement continues until finger-up - // calls releasePanKeys(). keyPanRemainderX = accumulateSignedMovement(keyPanRemainderX, dx); keyPanRemainderY = accumulateSignedMovement(keyPanRemainderY, dy); @@ -1300,9 +1190,6 @@ public final class TouchscreenGestures { dy = keyPanRemainderY; keyPanRemainderX = keyPanRemainderY = 0f; - // --- Significant movement detected: update each axis --- - - // Horizontal: determine desired state from this frame's delta boolean wantLeft = dx < -3f; boolean wantRight = dx > 3f; @@ -1311,7 +1198,6 @@ public final class TouchscreenGestures { if (wantLeft && !panKeyLeft) { pressKey(leftKey); panKeyLeft = true; panLeftKey = leftKey; } if (wantRight && !panKeyRight) { pressKey(rightKey); panKeyRight = true; panRightKey = rightKey; } - // Vertical: determine desired state from this frame's delta boolean wantUp = dy < -3f; boolean wantDown = dy > 3f; diff --git a/app/src/main/java/com/droiddeck/launcher/input/VrControllers.kt b/app/src/main/java/com/droiddeck/launcher/input/VrControllers.kt index 343d18bdd..0734710a3 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/VrControllers.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/VrControllers.kt @@ -6,7 +6,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import java.io.File -/** What SteamVR sees for the game pad. The driver reads the file once, when SteamVR starts. */ object VrControllers { enum class Emulate(val key: String) { OFF("off"), INDEX("index"), TOUCH("touch"), VIVE("vive") } @@ -46,7 +45,6 @@ object VrControllers { fun encode(settings: Settings): String = "emulate=${settings.emulate.key}\nsticks=${if (settings.sticksMoveHands) "hands" else "game"}\n" - /** Writes into the session's runtime dir, which the guest sees as /run/droiddeck. */ fun publish(context: Context) { runCatching { val dir = File(context.filesDir, ".wayland-rt").apply { mkdirs() } 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 5c5687c96..c45c74821 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/DeckyManager.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/DeckyManager.kt @@ -23,7 +23,6 @@ object DeckyManager { private const val PATH = "root/homebrew/services/PluginLoader" private const val VERSION = "root/homebrew/services/.droiddeck-decky-version" private const val SETTINGS = "root/homebrew/settings/loader.json" - // droiddeck-session reads the marker from the root of the client it runs. private val CEF_REMOTE_DEBUG_MARKERS = listOf( "root/.local/share/Steam/.cef-enable-remote-debugging", "root/.droiddeck-x64/.local/share/Steam/.cef-enable-remote-debugging", @@ -58,7 +57,6 @@ object DeckyManager { check(staged.renameTo(file)) { "cannot write ${file.path}" } } - // Mirrors Decky's updater: Stable takes the newest non-prerelease tag, Prerelease the newest tag of either kind. fun latest(channels: ReleaseChannels, branch: Int): Release? = if (branch == BRANCH_STABLE) channels.stable.firstOrNull() else channels.all.firstOrNull() 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 4a4857634..2b976796f 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/DesktopCatalog.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/DesktopCatalog.kt @@ -51,7 +51,6 @@ object DesktopCatalog { private fun journal(root: File, id: String) = File(root, ".droiddeck-pkg-$id$JOURNAL_SUFFIX") - /** Packages whose extraction over the runtime started and never finished. */ fun interrupted(context: Context): List = interrupted(LinuxRuntime.rootDir(context)) fun interrupted(root: File): List = @@ -63,7 +62,6 @@ object DesktopCatalog { private fun downloadFile(context: Context, entry: Entry) = File(context.cacheDir, "pkg-${entry.id}-${entry.sha256.take(12)}.download") - /** Drops downloads kept for installs that no longer need finishing, such as after the runtime was reinstalled. */ fun pruneDownloads(context: Context) = synchronized(installLock) { val keep = interrupted(context).map { downloadFile(context, it).name }.toSet() context.cacheDir.listFiles { f -> f.name.startsWith("pkg-") && f.name.removeSuffix(".resume.properties").endsWith(".download") } @@ -102,7 +100,6 @@ object DesktopCatalog { o.optString("icon", "applications-games"), o.optString("category", "Game"), ) - /** A finished download is kept until its extraction completes, so resuming does not fetch it again. */ private fun downloaded(file: File, entry: Entry): Boolean = file.isFile && file.length() > 0 && !File(file.path + ".resume.properties").exists() && (entry.size <= 0 || file.length() == entry.size) && entry.sha256.equals(Hashes.sha256(file), ignoreCase = true) diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/DeviceControlsComponent.kt b/app/src/main/java/com/droiddeck/launcher/runtime/DeviceControlsComponent.kt index cf0d3d7f6..334bce3df 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/DeviceControlsComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/DeviceControlsComponent.kt @@ -189,7 +189,6 @@ class DeviceControlsComponent(context: Context, root: File, private val activity companion object { private const val TAG = "DeviceControls" - // AudioService sends these on every stream step; the Settings observer and 250 ms poll lag a held volume key. private const val VOLUME_CHANGED_ACTION = "android.media.VOLUME_CHANGED_ACTION" private const val STREAM_MUTE_CHANGED_ACTION = "android.media.STREAM_MUTE_CHANGED_ACTION" private const val PERMISSION_CHANNEL = "brightness_permission" 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 7db52a9e2..8e5769c31 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/GuestCommand.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/GuestCommand.kt @@ -39,7 +39,6 @@ object GuestCommand { if (linkDir != null) cmd.add(1, "--link2symlink") val binds = LinuxRuntime.binds(context, null, runtimeDir, Environment.getExternalStorageDirectory(), extraBinds) binds.forEach { cmd.add("-b"); cmd.add(it) } - // Fake root and link2symlink are emulated by proot, so their syscalls must stay on the traced path. val fastPathKey = if (fastPath && !fakeRoot && linkDir == null && ProotFastPath.enabled(context)) ProotFastPath.key(root, binds) else null cmd += listOf( "/usr/bin/env", "-i", "HOME=/root", "USER=root", "LANG=C.UTF-8", @@ -48,8 +47,6 @@ object GuestCommand { "DROIDDECK_BWRAP_SOCKET=${BwrapSpawner.socket(context)}", "XDG_DATA_DIRS=/root/.local/share/flatpak/exports/share:/usr/local/share:/usr/share", ) - // The Steam client installer and the package tools inside the guest fetch over plain curl, - // so the app proxy has to reach them through the environment proot starts them with. DownloadSource.proxyHostPort(context).takeIf { it.isNotEmpty() }?.let { hostPort -> cmd += listOf( "http_proxy=http://$hostPort", "https_proxy=http://$hostPort", @@ -77,7 +74,6 @@ object GuestCommand { try { while (process.isAlive && !flag.get()) Thread.sleep(200) if (flag.get()) { - // PRoot ignores SIGTERM; SIGQUIT makes it kill its tracees and exit cleanly. if (pid > 0) { runCatching { android.system.Os.kill(pid, android.system.OsConstants.SIGQUIT) } } else { 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 04926193a..ab5a58737 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java +++ b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java @@ -50,7 +50,6 @@ public final class LinuxRuntime { } public static File liveDir(Context context) { - // Android may evict cache files even while guest processes are using them. File dir = new File(context.getNoBackupFilesDir(), "guest-runtime"); dir.mkdirs(); return dir; @@ -63,9 +62,7 @@ public final class LinuxRuntime { } private static final String HOST_DIR = "opt/android-host"; - /** The downloaded add-on pack. The APK does not ship this binary. */ public static final String PROOT_PACK = "add-on pack"; - /** Termux's unpatched proot inside the rootfs. Developer switch only. */ public static final String PROOT_ROOTFS = "rootfs fallback"; public static final String PROOT_MISSING = "missing"; @@ -81,7 +78,6 @@ public final class LinuxRuntime { return rootfsProot(context).isFile() && rootfsLoader(context).isFile(); } - /** Patched proot is the add-on pack. The rootfs copy is not that binary. */ public static boolean patchedProot(Context context) { return AddonPack.PROOT.installed(context); } @@ -98,7 +94,6 @@ public final class LinuxRuntime { return AddonPack.PROOT.file(context, "libproot-loader.so"); } - /** Where {@link #prootBinary} came from: {@link #PROOT_PACK}, {@link #PROOT_ROOTFS} or {@link #PROOT_MISSING}. */ public static String prootSource(Context context) { File bin = prootBinary(context); if (!bin.isFile()) return PROOT_MISSING; @@ -116,7 +111,6 @@ public final class LinuxRuntime { return hasRootfs(context) && (patchedProot(context) || rootfsProotReady(context)); } - /** The rootfs alone: enough to read the game library, while the proot pack can still be downloading. */ public static boolean hasRootfs(Context context) { if (LinuxRuntimeInstaller.isRemoving() || LinuxRuntimeInstaller.hasRemovalPending(context)) return false; File root = rootDir(context); @@ -392,9 +386,6 @@ public final class LinuxRuntime { return thermalZone("ddr"); } - // MangoHud reads the GPU and DDR temperatures from these zone numbers (bindAdrenoStats). On phones - // where they are CPU zones, their type follows the temperature put there, so they stay out of the - // CPU average. private static java.util.Map replacedZoneTypes() { java.util.Map replaced = new java.util.HashMap<>(); if (gpuTempSource() != null) { @@ -405,7 +396,6 @@ public final class LinuxRuntime { return replaced; } - /** The zones the session's MangoHud averages for the CPU temperature (all typed cpuN-thermal there). */ public static List cpuTempZones() { java.util.Map replaced = replacedZoneTypes(); List zones = new ArrayList<>(); 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 ee3349405..50fb07355 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntimeInstaller.java +++ b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntimeInstaller.java @@ -244,10 +244,6 @@ public final class LinuxRuntimeInstaller { } } - /** - * The download is named after the release, so a partial file is only ever resumed for the same - * version and checksum, never for a different runtime that happens to share a fixed name. - */ static String archiveName(Release release) { String version = release.version.replaceAll("[^A-Za-z0-9._-]", "_"); String sum = release.sha256.replaceAll("[^A-Za-z0-9]", ""); @@ -255,7 +251,6 @@ public final class LinuxRuntimeInstaller { return "linuxfs-" + version + "-" + sum + ".tar.zst"; } - /** Removes archives left by other releases and by builds that used the fixed name. */ private static void deleteOtherArchives(File dir, String keep) { File[] files = dir.listFiles((d, name) -> name.startsWith("linuxfs") && name.endsWith(".tar.zst") && !name.equals(keep)); if (files == null) return; @@ -264,8 +259,6 @@ public final class LinuxRuntimeInstaller { private static boolean installOnce(Context context, Release release, ProgressListener listener) { File archive = new File(context.getCacheDir(), archiveName(release)); - // A failed download keeps what it fetched so the next attempt resumes it; anything that - // reached verification and failed is not trusted and is removed. boolean keepPartial = false; try { deleteOtherArchives(context.getCacheDir(), archive.getName()); @@ -526,8 +519,6 @@ public final class LinuxRuntimeInstaller { } } - // Packages extract over the live runtime and their archives list a link before the file it - // names, so a kill mid-way must leave neither a truncated file nor a link to a missing one. public static boolean extract(Context context, File archive, File destination, ProgressListener listener) { return extract(context, archive, destination, destination, listener); } 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 974b6fe16..e9a9f4a72 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/ProotFastPath.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/ProotFastPath.kt @@ -12,10 +12,6 @@ object ProotFastPath { const val LIBRARY = "/usr/local/lib/libblfastpath.so" const val MAX_BINDS = 256 - /** - * The fast path's trampoline is let through only by the proot built with patches/0014. - * The rootfs copy (Termux's stock proot) is not that binary, so the library stays out. - */ fun allows(preference: Boolean, noSeccomp: Boolean, source: String): Boolean = preference && !noSeccomp && source == LinuxRuntime.PROOT_PACK 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 3b2a39ee8..fbbc5ddae 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeFileTree.java +++ b/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeFileTree.java @@ -74,11 +74,6 @@ public final class RuntimeFileTree { throw new IOException("Cannot make directory removable: " + dir); } - /** - * Paths in a runtime tree that belong to the user: the home folder, imported AppImages and script apps, and catalog - * AppImages with their menu entries and markers. Tar-kind catalog packages (the desktop) unpack over /usr and cannot - * be carried. - */ static java.util.List userPaths(File tree) throws IOException { java.util.List paths = new java.util.ArrayList<>(java.util.Arrays.asList("root", "opt/appimages/user", "opt/droiddeck-apps", "opt/droiddeck/wincomponents")); for (String n : children(new File(tree, "usr/local/share/applications"))) { @@ -168,13 +163,6 @@ public final class RuntimeFileTree { return false; } - /** - * Moves each user path that exists in {@code from} into {@code to}, replacing what {@code to} ships there. With - * {@code onlyMissing}, a path already present in {@code to} is left alone. On failure the paths already moved go back - * before the exception is thrown. Rollback failures are suppressed on that exception; callers must retain the source - * of any incomplete rollback. - * Returns the moved paths. - */ static java.util.List carryUserData(File from, File to, boolean onlyMissing) throws IOException { return carryUserData(from, to, onlyMissing, new java.util.ArrayList<>(userPaths(from, to))); } @@ -200,7 +188,6 @@ public final class RuntimeFileTree { return moved; } - /** Puts the given paths back from {@code from} into {@code to}; failures are collected, not stopped on. */ static void carryBack(File from, File to, java.util.List paths) throws IOException { IOException failed = null; for (int i = paths.size() - 1; i >= 0; i--) { diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeHealth.kt b/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeHealth.kt index f334e6cc5..94d7f443f 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeHealth.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeHealth.kt @@ -10,7 +10,6 @@ object RuntimeHealth { fun brokenLibraryLinks(context: Context): List = brokenLibraryLinks(LinuxRuntime.rootDir(context)) - /** A complete runtime has no library link in usr/lib whose target is missing. */ fun brokenLibraryLinks(root: File): List = listOf("usr/lib", "usr/lib/aarch64-linux-gnu").flatMap { dir -> val lib = File(root, dir) 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 aa53092fe..a641cf1f2 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeInstallService.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeInstallService.kt @@ -117,7 +117,6 @@ class RuntimeInstallService : Service() { if (Build.VERSION.SDK_INT >= 26) context.startForegroundService(intent) else context.startService(intent) } - /** Keeps the process in the foreground until the returned release runs, so closing the app does not stop the install. */ fun holdForPackage(context: Context, title: String): () -> Unit { packageTitle = title packageStage = title 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 517fa6c74..ef64bfde0 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/BatteryComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/BatteryComponent.kt @@ -56,7 +56,6 @@ class BatteryComponent(val dir: File, private val vpowerDir: File) : SessionPart val timeToEmptyS = if (discharging && chargeNowUah > 0 && abs(currentAvgUa) > 0) chargeNowUah * 3600L / abs(currentAvgUa) else 0L val charging = status == BatteryManager.BATTERY_STATUS_CHARGING val timeToFullS = if (charging && chargeFullUah > chargeNowUah && abs(currentAvgUa) > 0) (chargeFullUah - chargeNowUah) * 3600L / abs(currentAvgUa) else 0L - // Android can keep reporting charging at 100%; Linux reports Full there, and Steam shows 99% until it does. val statusText = when (status) { BatteryManager.BATTERY_STATUS_CHARGING -> if (pct >= 100) "Full" else "Charging" BatteryManager.BATTERY_STATUS_DISCHARGING -> "Discharging" @@ -127,9 +126,6 @@ class BatteryComponent(val dir: File, private val vpowerDir: File) : SessionPart companion object { private const val TAG = "SessionService" - // CURRENT_NOW/CURRENT_AVERAGE are documented as µA, but Samsung and other vendors report mA, - // which made every lifetime estimate 1000x too long. No real µA reading under load is below 10 mA, - // and no mA reading is above 10 A, so anything smaller than 10_000 is taken as mA. private const val MILLIAMP_CEILING = 10_000L internal fun toMicroamps(raw: Long): Long = if (abs(raw) < MILLIAMP_CEILING) raw * 1000L else raw diff --git a/app/src/main/java/com/droiddeck/launcher/session/BootMovie.kt b/app/src/main/java/com/droiddeck/launcher/session/BootMovie.kt index 953196900..a525bed37 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/BootMovie.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/BootMovie.kt @@ -24,7 +24,6 @@ object BootMovie { DROIDDECK_60 to "droiddeck-startup-60.webm", ) - // Steam's CEF player only takes WebM codecs; MediaMuxer's WebM writer cannot take AV1. internal val VIDEO_CODECS = setOf(MediaFormat.MIMETYPE_VIDEO_VP8, MediaFormat.MIMETYPE_VIDEO_VP9, MediaFormat.MIMETYPE_VIDEO_AV1) private val REMUX_CODECS = setOf(MediaFormat.MIMETYPE_VIDEO_VP8, MediaFormat.MIMETYPE_VIDEO_VP9) internal val AUDIO_CODECS = setOf(MediaFormat.MIMETYPE_AUDIO_OPUS, MediaFormat.MIMETYPE_AUDIO_VORBIS) @@ -39,18 +38,14 @@ object BootMovie { private fun dir(context: Context) = File(context.filesDir, "boot-movie") private fun customFile(context: Context) = File(dir(context), "custom.webm") - /** A movie imported before movies were kept in Steam's own folder. */ internal fun legacyCustomFile(context: Context) = customFile(context) - /** Unpacks one of DroidDeck's own movies on demand; null for anything else. */ internal fun assetFile(context: Context, id: String): File? = ASSETS[id]?.let { extractAsset(context, it) } - /** Picking a movie here makes it Steam's startup movie at the next session, over whatever Steam had picked. */ fun select(context: Context, id: String) { SessionPrefs.setBootMovie(context, id) SessionPrefs.setBootMovieApplyNext(context, true) BootMovieReport.note(context, "picked $id here; Steam names ${SessionFiles.startupMoviePath(BootMovieLibrary.steamConfig(context))}") - // Whatever Steam names right now is about to be replaced, so it is not a pick to adopt later. SessionFiles.startupMovieLocalPath(BootMovieLibrary.steamConfig(context))?.let { SessionPrefs.setBootMovieSteamSeen(context, it) } } @@ -67,7 +62,6 @@ object BootMovie { } } - /** The file the current choice plays from, before any volume or length change. */ fun currentFile(context: Context): File? = sourceOf(context, choice(context)) private fun sourceOf(context: Context, choice: String): File? = when { @@ -84,7 +78,6 @@ object BootMovie { return variant(context, source) } - /** The movie as Steam should get it: with the chosen volume and cut off after the chosen number of seconds. */ private fun variant(context: Context, source: File): File? { val volume = if (SessionPrefs.bootMovieSound(context)) SessionPrefs.bootMovieVolume(context) else 0 val seconds = SessionPrefs.bootMovieSeconds(context) @@ -124,21 +117,15 @@ object BootMovie { internal enum class Step { REMUX_SILENT, REMUX_AUDIO, TRANSCODE_SILENT, TRANSCODE_AUDIO, REGAIN } - /** - * How a movie is processed: the steps tried in order until one works, whether the source goes out untouched, and - * whether it still plays as it is when every step fails. - */ internal class Plan(val passthrough: Boolean, val steps: List, val sourceOnFailure: Boolean) internal fun plan(volume: Int, seconds: Int): Plan = when { volume == 100 && seconds == 0 -> Plan(true, emptyList(), true) volume == 0 -> Plan(false, listOf(Step.REMUX_SILENT, Step.TRANSCODE_SILENT), false) - // Only a length that could not be applied plays as it is; a volume that could not be applied plays no louder. volume == 100 -> Plan(false, listOf(Step.REMUX_AUDIO, Step.TRANSCODE_AUDIO), true) else -> Plan(false, listOf(Step.REGAIN, Step.REMUX_SILENT, Step.TRANSCODE_AUDIO), false) } - /** Frames beyond 1920x1080 (4K and the like) have made Steam crash while it starts, so such a movie is always scaled down. */ internal const val MAX_LONG_SIDE = 1920 internal const val MAX_SHORT_SIDE = 1080 @@ -150,22 +137,16 @@ object BootMovie { private fun isOversized(source: File): Boolean = probe(source)?.first?.let { oversized(it) } == true - /** A movie that is too large never goes out as it is: it is converted at the chosen volume, and plays nothing if that fails. */ internal fun oversizedPlan(volume: Int): Plan = Plan(false, listOf(if (volume == 0) Step.TRANSCODE_SILENT else Step.TRANSCODE_AUDIO), false) private fun transcodeSilent(source: File, target: File, limitUs: Long) = BootMovieTranscoder.transcode(source, target, keepAudio = false, limitUs = limitUs) {} != null - /** Keeps at most the one processed copy in use, so trying settings never leaves movies piling up. */ private fun prune(folder: File, keep: String?) { folder.listFiles()?.forEach { if (it.name != keep) it.delete() } } - /** - * Takes over a startup movie chosen in Steam's own settings: when Steam's config names a movie it did not name before, - * that movie becomes the choice here. Steam already plays it, so nothing is staged over it. True when the choice changed. - */ fun syncFromSteam(context: Context): Boolean { val named = SessionFiles.startupMovieLocalPath(BootMovieLibrary.steamConfig(context)) ?: return false val seen = SessionPrefs.bootMovieSteamSeen(context) @@ -174,7 +155,6 @@ object BootMovie { if (named == SessionFiles.BOOT_MOVIE_PATH || (named.isEmpty() && seen == null)) return false val path = named.ifEmpty { "/movies/steam_os_startup.webm" } val prefix = "/uioverrides/movies/" - // The list names a bought movie from the Steam folder (/config/communityitemscache/...), Steam's config from its config folder. val id = if (path.startsWith(prefix)) BootMovieLibrary.idOf(path.removePrefix(prefix)) else { val listed = BootMovieLibrary.list(context).firstOrNull { it.id.startsWith(BootMovieLibrary.STEAM_PREFIX) && it.id.endsWith(path) } listed?.id ?: BootMovieLibrary.STEAM_PREFIX + path diff --git a/app/src/main/java/com/droiddeck/launcher/session/BootMovieLibrary.kt b/app/src/main/java/com/droiddeck/launcher/session/BootMovieLibrary.kt index 0dd7a0fad..6fb1f56a9 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/BootMovieLibrary.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/BootMovieLibrary.kt @@ -9,22 +9,14 @@ import com.droiddeck.launcher.runtime.LinuxRuntime import org.json.JSONObject import java.io.File -/** - * The boot movies a person can pick from: DroidDeck's own two, a movie imported before this list existed, every `.webm` in - * Steam's custom movies folder (imports, SteamDeckRepo and movies put there by hand; all of them can be removed), Steam's own - * three startup movies and the movies bought in Steam's points shop (neither can be removed). - */ object BootMovieLibrary { private const val TAG = "BootMovieLibrary" const val PREFIX = "file:" - /** A movie Steam itself keeps outside its custom movies folder, such as one bought in its points shop: `steam:`. */ const val STEAM_PREFIX = "steam:" - /** Held while the movie list is read and changed, so two imports at once cannot overwrite each other's entries. */ private val lock = Any() - /** Names DroidDeck stages the chosen movie under; they are copies, not movies of their own. */ internal val STAGED = setOf("bigpicture_startup.webm", "steam_os_startup.webm", "droiddeck-startup.webm") const val SOURCE_DROIDDECK = "droiddeck" @@ -37,28 +29,21 @@ object BootMovieLibrary { internal fun moviesDir(context: Context) = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/config/uioverrides/movies") - /** - * The folders whose custom movies are listed, found and removed: [moviesDir], where imports are put and managed, then the - * x86-64 client's. The order never depends on which client is running, so a saved choice keeps meaning the same file. - */ internal fun customMovieDirs(context: Context): List = listOf(moviesDir(context), File(SessionPrefs.steamRoot(context, "x86_64"), "config/uioverrides/movies")).distinctBy { it.path } - /** Where imported and installed movies are kept for good; Steam's folder holds a copy, which is put back if it goes missing. */ internal fun libraryDir(context: Context) = File(context.filesDir, "boot-movie/library") private fun metaFile(context: Context) = File(context.filesDir, "boot-movie/movies.json") internal fun steamRoot(context: Context) = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam") - /** The running (else selected) Steam client's config, the one whose startup movie choice is read and recorded. */ internal fun steamConfig(context: Context) = File(SessionPrefs.activeSteamRoot(context), "config/config.vdf") private fun thumbsDir(context: Context) = File(context.filesDir, "boot-movie/thumbs") fun idOf(name: String) = PREFIX + name - /** Every Steam client folder: the usual one and the x86-64 client's. */ internal fun steamRoots(context: Context): List = listOf( steamRoot(context), File(LinuxRuntime.rootDir(context), "root/.droiddeck-x64/.local/share/Steam"), ).filter { it.isDirectory } @@ -86,7 +71,6 @@ object BootMovieLibrary { private fun readMeta(context: Context): JSONObject = runCatching { JSONObject(metaFile(context).readText()) }.getOrDefault(JSONObject()) - /** Changes the movie list one writer at a time; [change] gets the list and returns whether to save it. */ private fun editMeta(context: Context, change: (JSONObject) -> Boolean) = synchronized(lock) { val meta = readMeta(context) if (change(meta)) { @@ -100,7 +84,6 @@ object BootMovieLibrary { } } - /** The post a movie was installed from, if it came from SteamDeckRepo. */ fun repoId(context: Context, name: String): String? = readMeta(context).optJSONObject(name)?.optString("repo")?.takeIf { it.isNotEmpty() } @@ -128,7 +111,6 @@ object BootMovieLibrary { val info = meta.optJSONObject(f.name) val source = info?.optString("source")?.takeIf { it.isNotEmpty() } ?: SOURCE_STEAM val title = info?.optString("title")?.takeIf { it.isNotEmpty() } ?: f.nameWithoutExtension - // Everything in Steam's custom movies folder is the person's own (imported, from SteamDeckRepo or put there by hand). out.add(Entry(idOf(f.name), title, if (source == SOURCE_STEAM) SOURCE_IMPORT else source, f, removable = true)) } val steam = LinkedHashMap() @@ -147,14 +129,12 @@ object BootMovieLibrary { for ((id, file) in steam) { val native = file.nameWithoutExtension.lowercase() in NATIVE if ((file.name in STAGED && !native) || !seen.add(file.canonicalPath)) continue - // Steam names a bought movie by an item number and a hash; the points shop's own title is shown when Steam has it. val title = if (native) NATIVE[file.nameWithoutExtension.lowercase()] ?: file.nameWithoutExtension else bought[file.path] ?: boughtId(file)?.let { "#$it" } ?: file.nameWithoutExtension out.add(Entry(id, title, if (native) SOURCE_STEAM else SOURCE_POINTSHOP, file, removable = false)) } return out } - /** The four startup movies Steam ships with; Steam's movies folder holds many more videos that are not startup movies. */ private val NATIVE = mapOf( "deck_startup" to "Deck startup", "oled_startup" to "Oled startup", "startup_machine" to "Machine startup", "steam_os_startup" to "SteamOS startup", @@ -167,13 +147,8 @@ object BootMovieLibrary { private val scans = HashMap() - /** Forgets the last search for Steam's own movies. */ internal fun forgetScan() = synchronized(scanLock) { scans.clear() } - /** - * Movies Steam keeps outside its custom movies folder, such as ones bought in its points shop: every `.webm` below the - * client's config, cache and user folders. The search is repeated at most every 15 seconds for each client folder. - */ private fun pointShopMovies(root: File): Scan = synchronized(scanLock) { val now = System.currentTimeMillis() scans[root.path]?.let { if (now - it.at < 15_000L) return it } @@ -191,10 +166,8 @@ object BootMovieLibrary { scan } - /** The item number in the name Steam gives a bought movie, `_.webm`; null for any other name. */ private fun boughtId(file: File) = Regex("^(\\d+)_").find(file.nameWithoutExtension)?.groupValues?.get(1) - /** What the points shop calls a bought movie, from the list of startup movies Steam keeps in each user's local config. */ private fun boughtTitle(root: File, id: String): String? { val key = "communityitemid\\\":\\\"$id\\\"" val title = Regex("item_title\\\\\":\\\\\"([^\\\\\"]*)") @@ -209,7 +182,6 @@ object BootMovieLibrary { private const val OVERRIDES = "/uioverrides/movies/" - /** Puts back, in Steam's folder, any movie of DroidDeck's that went missing there, such as after the runtime was reset. */ internal fun restore(context: Context) = synchronized(lock) { migrateLegacyMeta(context) val meta = readMeta(context) @@ -222,7 +194,6 @@ object BootMovieLibrary { } } - /** Takes in the movie notes an earlier version kept inside Steam's movies folder, so those movies keep their title and source. */ private fun migrateLegacyMeta(context: Context) { val legacy = File(moviesDir(context), ".droiddeck-movies.json") if (!legacy.isFile) return @@ -238,7 +209,6 @@ object BootMovieLibrary { legacy.delete() } - /** Copies [kept] to [copy] through a staging name, so Steam never sees half a movie. */ private fun mirror(kept: File, copy: File): Boolean { val staged = File(copy.parentFile, copy.name + ".${System.nanoTime()}.staged") return try { @@ -255,10 +225,8 @@ object BootMovieLibrary { fun title(context: Context, id: String): String = list(context).firstOrNull { it.id == id }?.title ?: id - /** The file a movie id plays from; DroidDeck's own movies live in the app and are unpacked on demand. */ fun fileOf(context: Context, entry: Entry): File? = entry.file ?: BootMovie.assetFile(context, entry.id) - /** A free `.webm` name for a movie called [title]; the same title twice gets " (2)", " (3)"… */ fun freeName(context: Context, title: String): String { val base = title.replace(Regex("[^\\p{L}\\p{N} ._()\\-]"), "").replace(Regex("\\s+"), " ").trim().trim('.').take(60).ifEmpty { "movie" } val dirs = customMovieDirs(context) @@ -269,10 +237,6 @@ object BootMovieLibrary { return name } - /** - * Keeps the finished movie [staged] (a file in [libraryDir]) under a free name, copies it into Steam's folder and notes its - * title, source and cover. Returns the name, or null when it could not be kept. Runs one movie at a time. - */ internal fun commit(context: Context, staged: File, title: String, source: String, repoId: String? = null, cover: ByteArray? = null): String? = synchronized(lock) { val name = freeName(context, title) @@ -283,7 +247,6 @@ object BootMovieLibrary { name } - /** Notes a movie that was just put into Steam's folder: what to call it, where it came from and its cover. */ fun register(context: Context, name: String, title: String, source: String, repoId: String? = null, cover: ByteArray? = null) { editMeta(context) { meta -> val info = JSONObject().put("title", title).put("source", source) @@ -301,12 +264,10 @@ object BootMovieLibrary { val removed = when { id == BootMovie.CUSTOM -> { BootMovie.removeCustom(context); true } else -> { - // Only movies in Steam's custom movies folder are ever deleted; Steam's own and points shop movies are left alone. if (!id.startsWith(PREFIX)) return false val name = id.removePrefix(PREFIX) if (name.isEmpty() || name.contains('/') || name.contains('\\') || name in STAGED) return false val copies = customMovieDirs(context).map { File(it, name) } - // A name can exist in more than one client's folder; the entry is the first one found (see customMovieDirs), and only that file is deleted. val target = copies.firstOrNull { it.exists() } val managed = File(libraryDir(context), name) if (readMeta(context).optJSONObject(name) == null && target == null && !managed.isFile) return false @@ -335,14 +296,12 @@ object BootMovieLibrary { return removed } - /** What every processed copy of [source] starts with; the hash of its full path keeps same-named files in different places apart. */ internal fun variantPrefix(source: File) = safe(source.name) + "." + Integer.toHexString(source.absolutePath.hashCode()) + "-" private fun safe(name: String) = name.replace(Regex("[^A-Za-z0-9._-]"), "_") private fun coverName(name: String) = "cover-" + safe(name) - /** How long a movie runs, in whole seconds; 0 when it cannot be read. */ fun durationSeconds(file: File?): Int { if (file == null || !file.isFile) return 0 val retriever = MediaMetadataRetriever() @@ -357,7 +316,6 @@ object BootMovieLibrary { } } - /** The first frame that is not nearly black out of several taken through the movie, else the brightest; a movie may open dark. */ private fun brightestFrame(retriever: MediaMetadataRetriever, durationMs: Long): Bitmap? { var best: Bitmap? = null var bestLuma = -1 @@ -382,11 +340,9 @@ object BootMovieLibrary { private const val MIN_COVER_LUMA = 24 - /** A cover for a movie: the picture SteamDeckRepo gave it, or one frame taken from the video and remembered. */ fun cover(context: Context, entry: Entry): File? { val file = fileOf(context, entry) ?: return null val name = if (entry.id.startsWith(PREFIX)) file.name else entry.id - // Points Shop covers once kept a picture that was not always the movie's own; those files are not used. val given = File(thumbsDir(context), if (entry.source == SOURCE_POINTSHOP) "v2-" + coverName(name) else coverName(name)) if (given.isFile && given.length() > 0L) return given val key = "frame-" + safe(name) + "-v2-" + file.length() + "-" + file.lastModified() + ".jpg" diff --git a/app/src/main/java/com/droiddeck/launcher/session/BootMovieReport.kt b/app/src/main/java/com/droiddeck/launcher/session/BootMovieReport.kt index 3a672dcfd..18ff31169 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/BootMovieReport.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/BootMovieReport.kt @@ -8,10 +8,6 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -/** - * What DroidDeck and Steam know about boot movies, in one text file: Steam's startup movie setting, every movie file and - * where it lives, DroidDeck's own state, and a short trace of what each session start did to Steam's config. - */ object BootMovieReport { private const val TAG = "BootMovieReport" private const val MAX_TRACE_LINES = 200 @@ -23,7 +19,6 @@ object BootMovieReport { private fun traceFile(context: Context) = File(context.filesDir, "boot-movie/apply.log") - /** Adds a line to the short trace kept for the report; only the latest lines are kept. */ fun note(context: Context, line: String) { synchronized(lock) { runCatching { @@ -36,7 +31,6 @@ object BootMovieReport { } } - /** Writes the report to Downloads/DroidDeck, or to the app's own folder when that is not possible; null when the report cannot be built or neither folder works. */ fun write(context: Context): File? { val text = runCatching { build(context) }.getOrElse { Log.w(TAG, "could not build the report", it) diff --git a/app/src/main/java/com/droiddeck/launcher/session/BootMovieTranscoder.kt b/app/src/main/java/com/droiddeck/launcher/session/BootMovieTranscoder.kt index 92bbbae4e..55f84c006 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/BootMovieTranscoder.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/BootMovieTranscoder.kt @@ -46,7 +46,6 @@ internal object BootMovieTranscoder { return Result(audioDropped = hadAudio && !muxedAudio) } - /** Copies the video of [source] untouched and gives it its sound again at [gain] of its level, cut to [limitUs]. */ fun regain(source: File, target: File, gain: Float, limitUs: Long): Boolean { val audio = runCatching { encodeAudio(source, gain, limitUs) } .onFailure { Log.w(TAG, "could not change the volume of ${source.name}", it) }.getOrNull() ?: return false @@ -121,7 +120,6 @@ internal object BootMovieTranscoder { internal class Layout(val width: Int, val height: Int, val x: Int, val y: Int, val contentWidth: Int, val contentHeight: Int) - // Steam fills the screen with its startup movie, so portrait clips are pillarboxed into 16:9 instead of cropped. internal fun layout(width: Int, height: Int, rotation: Int): Layout { val (w, h) = if (rotation % 180 != 0) height to width else width to height fun even(v: Double) = maxOf(2, (v.toInt() / 2) * 2) @@ -137,7 +135,6 @@ internal object BootMovieTranscoder { return Layout(canvas, ch, (canvas - cw) / 2, 0, cw, ch) } - /** [pcm] made quieter by [gain] and cut to its first [limitUs] microseconds. */ internal fun scale(pcm: ShortArray, channels: Int, gain: Float, limitUs: Long): ShortArray { val frames = pcm.size / channels val kept = if (limitUs == Long.MAX_VALUE) frames else minOf(frames.toLong(), limitUs * OPUS_RATE / 1_000_000L).toInt() @@ -164,7 +161,6 @@ internal object BootMovieTranscoder { return out } - /** Most decoded audio held in memory at once: about six minutes of 48 kHz stereo. */ private const val MAX_PCM_BYTES = 64 shl 20 private fun encodeAudio(source: File, gain: Float = 1f, limitUs: Long = Long.MAX_VALUE): Audio? { @@ -212,7 +208,6 @@ internal object BootMovieTranscoder { } dec.releaseOutputBuffer(index, false) if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) break - // Past the length that will be kept there is nothing more to decode; without a length, a track too big to hold is dropped. if (limitUs != Long.MAX_VALUE && info.presentationTimeUs > limitUs) break if (pcm.size() > MAX_PCM_BYTES) { if (limitUs == Long.MAX_VALUE) return null else break } } diff --git a/app/src/main/java/com/droiddeck/launcher/session/BottomNotifications.kt b/app/src/main/java/com/droiddeck/launcher/session/BottomNotifications.kt index d43b1f617..45e0890c5 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/BottomNotifications.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/BottomNotifications.kt @@ -7,13 +7,8 @@ import android.service.notification.StatusBarNotification import androidx.compose.runtime.mutableStateListOf import androidx.core.app.NotificationManagerCompat -/** One notification for the bottom-screen list. Kept in memory only; never written to disk or logs. */ data class BottomNotification(val key: String, val app: String, val title: String, val text: String, val postedAt: Long) -/** - * Notifications from other apps for the bottom screen, so a message can be read without leaving the game. - * Works only after the user allows DroidDeck notification access in Android settings. - */ object BottomNotifications { val items = mutableStateListOf() diff --git a/app/src/main/java/com/droiddeck/launcher/session/BottomScreenshots.kt b/app/src/main/java/com/droiddeck/launcher/session/BottomScreenshots.kt index 90010a82c..905f22715 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/BottomScreenshots.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/BottomScreenshots.kt @@ -12,12 +12,10 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -/** A screenshot for the bottom-screen gallery: one DroidDeck took (a MediaStore [uri]) or one Steam saved (a [file]). */ data class BottomShot(val uri: Uri?, val file: File?, val takenAt: Long, val steam: Boolean) { val model: Any get() = uri ?: file!! } -/** Game frames DroidDeck saves to Pictures/DroidDeck, plus the screenshots Steam keeps under userdata. */ object BottomScreenshots { private const val FOLDER = "DroidDeck" private const val QUALITY = 95 @@ -58,7 +56,6 @@ object BottomScreenshots { } }.getOrDefault(false) - /** Newest first, at most [LIMIT]. */ fun list(context: Context, runtimeRoot: File): List = (own(context) + steam(runtimeRoot)).sortedByDescending { it.takenAt }.take(LIMIT) @@ -85,7 +82,6 @@ object BottomScreenshots { shots }.getOrDefault(emptyList()) - /** The .jpg files in userdata//760/remote//screenshots of each Steam home; the thumbnails folder is skipped. */ internal fun steam(runtimeRoot: File): List = SteamDownloadMonitor.STEAM_HOMES.flatMap { home -> File(runtimeRoot, "$home/userdata").listFiles { f -> f.isDirectory }.orEmpty().flatMap { account -> 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 8b7ad50f0..b1d70902a 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt @@ -34,7 +34,6 @@ object ComponentsManager { "Dxvk-gplasync-Linux" to "dxvk", "Vkd3d-proton-Linux" to "vkd3d", ) - // DD_NIGHTLY holds every component in one release; the file name says which. private val NIGHTLY_PREFIXES = linkedMapOf("DD-dxvk-" to "dxvk", "DD-vkd3d-proton-" to "vkd3d", "DD-FEXCore-" to "fex") private val SOURCES = RELEASES.keys.map { NIGHTLIES to it } + (DD_COMPONENTS to DD_NIGHTLY) private val DOWNLOAD_REPOS = listOf(NIGHTLIES, DD_COMPONENTS) @@ -976,12 +975,9 @@ object ComponentsManager { internal fun assetComp(release: String, name: String): String? = RELEASES[release] ?: if (release == DD_NIGHTLY) NIGHTLY_PREFIXES.entries.firstOrNull { name.startsWith(it.key) }?.value else null - // Per component: the stable packages newest first, then the nightlies. internal val CATALOG_ORDER: Comparator = compareBy { it.comp }.thenBy { it.release == DD_NIGHTLY }.then(newestFirst { it.file }) - // A nightly DD--[+N]-nightly.wcp pairs with the newest stable -[-]-linux.wcp - // in the same list, so the stable row can offer it behind a Stable/Nightly switch. Unpaired nightlies keep their own row. internal fun nightlyPairs(items: List): Map { val stable = items.filter { it.release != DD_NIGHTLY } return items.filter { it.release == DD_NIGHTLY }.mapNotNull { n -> diff --git a/app/src/main/java/com/droiddeck/launcher/session/FexCatalog.kt b/app/src/main/java/com/droiddeck/launcher/session/FexCatalog.kt index 92cd7652d..d3471283e 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/FexCatalog.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/FexCatalog.kt @@ -7,11 +7,6 @@ import com.droiddeck.launcher.core.Downloader import org.json.JSONObject import java.io.File -/** - * The FEX build DroidDeck-Components' fex.json names for x86 programs. A session uses the last copy read. - * Components' build-fex.yml replaces the archive on its single `fex` release and records the build's version; - * a release per build (fex-2610-r5) is still read. - */ object FexCatalog { private const val TAG = "FexCatalog" const val CATALOG_URL = "https://raw.githubusercontent.com/Droid-Deck/DroidDeck-Components/main/fex.json" @@ -43,7 +38,6 @@ object FexCatalog { fun cached(context: Context): Build? = runCatching { parse(file(context).readText()) }.getOrNull() - /** Reads fex.json straight from GitHub (a digest never comes from a mirror), at most every ten minutes. */ @Synchronized fun refresh(context: Context, now: Long = System.currentTimeMillis()): Build? { if (now - refreshedAt < REFRESH_MS) return cached(context) @@ -62,7 +56,6 @@ object FexCatalog { return build } - /** For droiddeck-fex-update: the build and every source its archive may come from (it checks the sha256). */ fun environment(build: Build, sources: List = DownloadSource.candidates(build.url)): List = listOf( "BL_FEX_TAG=${build.tag}", "BL_FEX_SHA256=${build.sha256}", 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 447fb254c..b4bd458a5 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GameStorage.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GameStorage.kt @@ -17,7 +17,6 @@ object GameStorage { val primary = context.getExternalFilesDir(null)?.absolutePath val primaryStorage = Environment.getExternalStorageDirectory()?.absolutePath return context.getExternalFilesDirs(null).orEmpty().filterNotNull().mapNotNull { dir -> - // Non-primary app dirs are usable even when Android reports the volume as non-removable (USB SSDs often do). if (primary != null && dir.absolutePath == primary) return@mapNotNull null if (!primaryStorage.isNullOrBlank() && dir.absolutePath.startsWith("$primaryStorage/")) return@mapNotNull null val volume = try { sm?.getStorageVolume(dir) } catch (e: Exception) { null } diff --git a/app/src/main/java/com/droiddeck/launcher/session/GameTelemetry.kt b/app/src/main/java/com/droiddeck/launcher/session/GameTelemetry.kt index 28777f6b0..2a32d817e 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GameTelemetry.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GameTelemetry.kt @@ -3,7 +3,6 @@ package com.droiddeck.launcher.session import java.nio.ByteBuffer import java.nio.ByteOrder -/** One reading from a racing game's telemetry. Fractions are 0..1; null means the game does not send it. */ data class Telemetry( val source: String, val speedKmh: Float? = null, @@ -19,33 +18,22 @@ data class Telemetry( val currentLapS: Float? = null, val lastLapS: Float? = null, val bestLapS: Float? = null, - /** G-force as the game sends it (lateral, longitudinal) and steering from -1 (left) to 1 (right). */ val gLat: Float? = null, val gLon: Float? = null, val steer: Float? = null, - /** Front left, front right, rear left, rear right. */ val tireTempsC: List? = null, val turboBar: Float? = null, val engineTempC: Float? = null, val oilTempC: Float? = null, - /** OutGauge dashboard lights that are on, as GameTelemetry.DL_* bits. */ val lights: Int? = null, ) -/** - * Racing telemetry games send over UDP to 127.0.0.1. Each format has its own port, the one the game or - * its common tools use by default: - * - [CODEMASTERS_PORT]: DiRT Rally 1/2, DiRT 4, GRID ("extradata 3" float array), DiRT Showdown (152-byte packet). - * - [FORZA_PORT]: Forza Motorsport 7/2023 and Forza Horizon 4/5 "Data Out" (Sled and Dash). - * - [OUTGAUGE_PORT]: BeamNG.drive and Live for Speed OutGauge. - */ object GameTelemetry { const val CODEMASTERS_PORT = 20777 const val FORZA_PORT = 5300 const val OUTGAUGE_PORT = 4444 val PORTS = listOf(CODEMASTERS_PORT, FORZA_PORT, OUTGAUGE_PORT) - // Games send to 127.0.0.1; on Android InetAddress.getLoopbackAddress() is ::1, which IPv4 packets never reach. val LISTEN_ADDRESS: java.net.InetAddress = java.net.InetAddress.getByAddress(byteArrayOf(127, 0, 0, 1)) fun parse(port: Int, data: ByteArray, length: Int): Telemetry? = runCatching { @@ -61,15 +49,12 @@ object GameTelemetry { private const val MPS_TO_KMH = 3.6f private const val GRAVITY = 9.81f - // OutGauge DashLights / ShowLights bits (Live for Speed InSim; BeamNG uses the same values). const val DL_SHIFT = 1 const val DL_HANDBRAKE = 4 const val DL_PITSPEED = 8 const val DL_TC = 16 const val DL_ABS = 1024 - // Float indexes of the Codemasters packet; engine rpm values are sent divided by 10. Older games - // (DiRT Showdown) send only the first 38 floats, so lap count, position, fuel and max rpm are missing there. private const val CM_LAP_TIME = 1 private const val CM_SPEED = 7 private const val CM_THROTTLE = 29 @@ -99,7 +84,6 @@ object GameTelemetry { speedKmh = f(CM_SPEED) * MPS_TO_KMH, rpm = f(CM_RPM) * 10f, maxRpm = (f(CM_MAX_RPM) * 10f).takeIf { it > 0f }, - // Neutral is 0; DiRT Rally 2.0 reports reverse as 10, older games as -1. gear = when { gear == 0 -> "N" gear < 0 || gear == 10 -> "R" @@ -119,8 +103,6 @@ object GameTelemetry { ) } - // Forza "Data Out": Sled is 232 bytes; the Dash block follows it (FM7 311, FM 2023 331). Horizon 4/5 send - // 324 bytes with 12 extra bytes before the Dash block. private const val FORZA_SLED = 232 private const val FORZA_FM7 = 311 private const val FORZA_HORIZON = 324 @@ -140,7 +122,6 @@ object GameTelemetry { if (dash < 0) return Telemetry(source = "Forza", rpm = rpm, maxRpm = maxRpm.takeIf { it > 0f }) if (!racing) return Telemetry(source = "Forza") val gear = buf.get(dash + 75).toInt() and 0xFF - // Tire temperatures are read as Fahrenheit, as community Data Out parsers do; not checked on a device. val tiresF = (0 until 4).map { buf.getFloat(dash + 24 + it * 4) } return Telemetry( source = "Forza", @@ -163,7 +144,6 @@ object GameTelemetry { ) } - // OutGauge: 92 bytes, or 96 with the optional id at the end. private const val OUTGAUGE_SIZE = 92 internal fun outGauge(buf: ByteBuffer, length: Int): Telemetry? { @@ -189,14 +169,12 @@ object GameTelemetry { } } -/** Games that publish their state locally without any setup: War Thunder's web page and Elite Dangerous' Status.json. */ object GameStatusFiles { const val WAR_THUNDER_STATE = "http://127.0.0.1:8111/state" private const val ELITE_APP = "359320" private const val ELITE_STATUS = "pfx/drive_c/users/steamuser/Saved Games/Frontier Developments/Elite Dangerous/Status.json" const val WAR_THUNDER_ROWS = 12 - /** War Thunder's own labels (for example "TAS, km/h") with their values, while a match runs. */ internal fun warThunder(json: String): List> { val state = org.json.JSONObject(json) if (!state.optBoolean("valid", false)) return emptyList() @@ -214,7 +192,6 @@ object GameStatusFiles { data class Elite(val fuelMain: Double?, val fuelReserve: Double?, val cargo: Double?, val pips: List?, val legal: String?, val balance: Long?) - /** Null while Elite is at its menus (Status.json then carries only the timestamp and flags). */ internal fun elite(json: String): Elite? { val s = org.json.JSONObject(json) val fuel = s.optJSONObject("Fuel") diff --git a/app/src/main/java/com/droiddeck/launcher/session/GameThreadHints.kt b/app/src/main/java/com/droiddeck/launcher/session/GameThreadHints.kt index 1b5b90db6..3c2c5098b 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GameThreadHints.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GameThreadHints.kt @@ -12,54 +12,26 @@ import com.droiddeck.launcher.wayland.WaylandCompositor import java.io.File import kotlin.math.abs -/** - * The game's busiest threads named to Android's performance hint service (ADPF) for a session. - * - * Qualcomm's WALT moves a thread to the prime cores only once it fills a mid core (the Odin 3's - * sched_upmigrate is 95), and a game thread that waits on its workers or the GPU for part of each - * frame never does: Factorio's update thread sat at ~68% on cores 0-5 with 6-7 idle, and ran 60% - * faster pinned to 6-7. An app may not raise a thread's uclamp (EPERM without CAP_SYS_NICE), but a - * hint session may name any thread of its UID - the guest's are - and one reported behind its - * target is boosted by the power HAL onto the prime cores: on the Odin 3 a 70%-busy thread of a - * child process went from cores 0-5 to 6-7 and ran 1.7x faster within a second. - * - * Once a second the busiest threads of this UID are read from /proc; one busy more than [ENTER] of - * the time (averaged with the second before, so a loading spike does not count) joins the session, - * and leaves it under [LEAVE] - well below, since it runs faster where it was moved. While the - * session has threads it reports them behind target, which is what asks for the boost: the reports - * carry no frame timing, the busy share is the signal (a capped game's frame interval looks the same - * as a slow one's). Nothing is boosted while the device is critically hot or saves power. - * - * Nor while the game holds the panel's rate or a fraction of it ([holdsPace]): there is nothing to - * gain, and it cost frame pacing - Serious Sam capped at 120 fps ran its boosted main thread on prime - * cores the governor kept at 2.2 GHz, slower than a 3.5 GHz mid core, and 23-25% of its frames fell - * under 90 fps against 2-7% unboosted. A game with no frames (Factorio's benchmark, a loading screen) - * or one falling behind the panel is boosted; one holding pace keeps what it has for [PACE_LEAVE_S] - * seconds and is then let go. - */ object GameThreadHints { private const val TAG = "GameThreadHints" private const val TARGET_NS = 10_000_000L private const val REPORT_MS = 50L - private const val SCAN_EVERY = 20 // reports, so once a second + private const val SCAN_EVERY = 20 private const val ENTER = 0.6 - private const val LEAVE = 0.15 // a capped game's main thread idles to ~30% on a prime core; under 0.3 it hopped back and forth - private const val MAX_THREADS = 2 // one per prime core; a third would be sped up in place instead + private const val LEAVE = 0.15 + private const val MAX_THREADS = 2 private const val TOP_PROCESSES = 3 private const val PACE_WINDOW_S = 3 private const val PACE_LEAVE_S = 10 private const val PACE_TOLERANCE = 0.08 - /** Busy, but not the game: the app itself and the Steam client, whose UI would otherwise hold the prime cores. */ private val NOT_GAMES = setOf("steam", "steamwebhelper") private var worker: Thread? = null - /** At a session's start. The line is for the log. */ @Synchronized fun start(context: Context): String { if (!SessionPrefs.gameThreadHints(context)) return "game thread hints: off" - // Measured on the Odin 3 (CQ8725S) only; another HAL may boost differently, or not at all. if (Build.VERSION.SDK_INT < 31 || Build.SOC_MODEL != "CQ8725S") return "game thread hints: Odin 3 only" val hm = context.getSystemService(PerformanceHintManager::class.java) ?: return "game thread hints: no PerformanceHintManager" @@ -69,7 +41,6 @@ object GameThreadHints { return "game thread hints: watching" } - /** At a session's end. */ @Synchronized fun stop() { worker?.interrupt() @@ -92,7 +63,6 @@ object GameThreadHints { val fps = (frames.last() - frames.first()).toDouble() / (frames.size - 1).coerceAtLeast(1) val paced = frames.size > PACE_WINDOW_S && holdsPace(fps, SessionState.refreshHz.toDouble()) pacedFor = if (paced) pacedFor + 1 else 0 - // Not SEVERE: the Odin 3 reports it once its skin reaches 50 C, a couple of minutes into any game. val spare = pm.isPowerSaveMode || pm.currentThermalStatus >= PowerManager.THERMAL_STATUS_CRITICAL val next = when { spare || pacedFor >= PACE_LEAVE_S -> emptySet() @@ -101,7 +71,6 @@ object GameThreadHints { } if (next != boosted) { session = update(hm, session, next) - // No session means a thread exited in between: tried again next second. boosted = if (session == null) emptySet() else next Log.i(TAG, (if (session == null && next.isNotEmpty()) "no session for " else "boosting ") + next.joinToString { "${readName("/proc/$it/comm")}/$it ${((busy[it] ?: 0.0) * 100).toInt()}%" }.ifEmpty { "nothing" } + @@ -119,7 +88,6 @@ object GameThreadHints { } } - /** A new session over [tids] in place of [session]; null for none. The set changes seldom, so no setThreads. */ private fun update(hm: PerformanceHintManager, session: PerformanceHintManager.Session?, tids: Set): PerformanceHintManager.Session? { session?.close() if (tids.isEmpty()) return null @@ -131,19 +99,9 @@ object GameThreadHints { } } - /** - * Whether a game presenting [fps] frames a second keeps up with a [refreshHz] panel: within - * [PACE_TOLERANCE] of the rate or of a half, a third or a quarter of it (a 60 fps limiter on 120 Hz). - * A game with next to no frames (a headless benchmark, a loading screen) does not. - */ - // ponytail: a CPU-bound game that happens to run at ~60 fps on a 120 Hz panel reads as limited; narrow the band if that shows up internal fun holdsPace(fps: Double, refreshHz: Double): Boolean = fps >= 10 && refreshHz > 0 && (1..4).any { k -> abs(fps - refreshHz / k) < PACE_TOLERANCE * refreshHz / k } - /** - * The threads to boost next: those already boosted that are still above [LEAVE], then new ones - * above [ENTER], busiest first, at most [MAX_THREADS]. - */ internal fun pick(boosted: Set, busy: Map): Set = busy.entries .filter { (tid, share) -> share >= if (tid in boosted) LEAVE else ENTER } @@ -154,7 +112,6 @@ object GameThreadHints { private fun readName(path: String) = try { File(path).readText().trim() } catch (_: Exception) { "?" } - /** Busy shares of this UID's threads since the last sample: /proc shows an app only its own processes. */ private class Scanner { private val self = Process.myPid() private val hz = Os.sysconf(OsConstants._SC_CLK_TCK).toDouble() @@ -163,7 +120,6 @@ object GameThreadHints { private var lastThread = HashMap() private var lastBusy = HashMap() - /** Only the busiest few processes have their threads read, so a second costs a few dozen small reads. */ fun sample(): Map { val now = System.nanoTime() val seconds = (now - lastNs) / 1e9 @@ -197,7 +153,6 @@ object GameThreadHints { return busy } - /** utime + stime of a /proc stat line; fields counted after the command's closing parenthesis. */ private fun cpuTicks(path: String): Long? = try { val line = File(path).readText() val fields = line.substring(line.lastIndexOf(')') + 2).split(' ') 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 8e10024fc..019d3b3df 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GpuStatsComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GpuStatsComponent.kt @@ -73,7 +73,6 @@ class GpuStatsComponent(val dir: File, private val root: File) : SessionPart() { }, "gpu-stats").apply { isDaemon = true; start() } } - // kgsl's gpubusy can read 0 0 straight after a read, so feeds that share a reader share one read. private fun refresh() { if (layerLoad != null || cycleClock != null || memstore != null) { shown = hudShown() @@ -288,8 +287,6 @@ class GpuStatsComponent(val dir: File, private val root: File) : SessionPart() { return pick("gpu_busy_percentage", LOAD_SOURCES, "%3d %%\n") { it.coerceIn(0, 100) } } - // kgsl publishes busy/total once per ~1 s of GPU-on time and zeroes it on any read while the GPU is - // power-collapsed, so direct pollers mostly see 0; latch each published window instead. private fun latchedBusyFeed(): Feed? { var load = 0L var windowAt = 0L 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 5632bd27e..f673f282b 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/OfflineMode.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/OfflineMode.kt @@ -21,7 +21,6 @@ object OfflineMode { fun account(context: Context): String? { val root = LinuxRuntime.rootDir(context) - // Until its first start the x86-64 client has no sign-in; droiddeck-steam-x64 --sync-login copies the arm64 one. val file = x64LoginUsers(root).takeIf { SessionPrefs.activeSteamClientArch(context) == "x86_64" && it.isFile } ?: loginUsers(root) val text = try { 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 5826db97a..39d99bfa6 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/RumbleComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/RumbleComponent.kt @@ -214,8 +214,6 @@ class RumbleComponent : SessionPart() { if (any) manager.vibrate(combined.combine()) else cancel() } - // Some vendors' input-device vibrator HALs (Samsung's) don't halt a Bluetooth pad's running - // effect on cancel(), but a new effect always replaces it. override fun cancel() { val supersede = CombinedVibration.startParallel() ids.forEach { supersede.addVibrator(it, VibrationEffect.createOneShot(1L, 1)) } @@ -255,7 +253,6 @@ class RumbleComponent : SessionPart() { private const val TAG = "SessionService" const val NAME = "droiddeck-rumble" const val MIN_GAP_MS = 50L - // Must match RUMBLE_HOLD_MS in fakeinput_steam.cpp. const val HOLD_MS = 0xFFFF const val HOLD_EFFECT_MS = 2000L const val KEEPALIVE_MS = 1500L 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 b4408c48b..4fc004304 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionArtifacts.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionArtifacts.kt @@ -31,7 +31,6 @@ object SessionArtifacts { private val collectingDone = collectingLock.newCondition() private val collecting = HashSet() - /** Recovery leaves [dir] alone from the stop until [collect] has finished it. */ fun collectionStarted(dir: File) { collectingLock.lock() try { collecting.add(dir) } finally { collectingLock.unlock() } @@ -317,7 +316,6 @@ object SessionArtifacts { runCatching { File(dir, CLIENT_FILE).writeText("$arch\n") } } - /** The Steam client a session ran, when it recorded one. */ internal fun recordedClient(dir: File): String? = runCatching { File(dir, CLIENT_FILE).readText().trim() }.getOrNull()?.takeIf { it == "arm64" || it == "x86_64" } 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 056fc205a..09122644d 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt @@ -33,7 +33,6 @@ object SessionFiles { "etc/xdg/autostart/droiddeck-clipboard.desktop" to "etc/xdg/autostart/droiddeck-clipboard.desktop", ) - /** Puts the session's files in place; [startsSession] is true only for the call that starts the Steam session. */ fun stage(context: Context, root: File, startsSession: Boolean = false) { if (DesktopCatalog.desktopInstalled(root) && !DesktopCatalog.desktopFromPack(root)) try { DesktopCatalog.cleanLegacy(context) } catch (e: Exception) { Log.w(TAG, "LXQt cleanup will retry next session", e) } @@ -171,7 +170,6 @@ object SessionFiles { } + listOf("libvulkan-thunk.so", "libwebkitinput.so").map { "x86_64/$it" to "usr/local/lib/droiddeck-fex/x86_64/$it" } val optional = (arrayOf( "usr/local/bin/gamescope" to "usr/local/bin/gamescope", - // The gamescope pack carries its own WSI layer (patch 0117); it replaces the runtime's stock copy. "usr/lib/libVkLayer_FROG_gamescope_wsi_aarch64.so" to "usr/lib/libVkLayer_FROG_gamescope_wsi_aarch64.so", "usr/local/lib/droiddeck/uruntime" to "usr/local/lib/droiddeck/uruntime", "usr/local/lib/droiddeck-pipewire.tzst" to "usr/local/lib/droiddeck-pipewire.tzst", @@ -282,7 +280,6 @@ object SessionFiles { val movieDir = File(home, "config/uioverrides/movies") val config = File(home, "config/config.vdf") if (startupMovie == null) { - // Nothing to play (a movie whose volume could not be set is never played at another volume), so no earlier copy plays either. for (name in listOf("bigpicture_startup.webm", "steam_os_startup.webm")) { File(movieDir, name).delete() File(movieDir, ".$name.source").delete() @@ -302,17 +299,10 @@ object SessionFiles { if (!bigPictureMovieInstalled) allInstalled = false if (done && home.path.contains(".droiddeck-x64") == activeIsX64) seen = if (bigPictureMovieInstalled) BOOT_MOVIE_PATH else "" } - // A pick made in the app is done once a session started with every Steam client's config having it; guest commands run - // before that, and a Steam that is already running writes its own choice back when it exits. if (force && allDone && allInstalled && startsSession) SessionPrefs.setBootMovieApplyNext(context, false) seen?.let { SessionPrefs.setBootMovieSteamSeen(context, it) } } - /** - * Points Steam's startup movie back at its own default when it names a movie that is gone: DroidDeck's staged copy that - * could not be prepared, or (when a pick here is waiting) a movie removed from Steam's folder. True when Steam's config - * no longer names a missing movie. - */ internal fun resetMissingStartupMovie(config: File, force: Boolean): Boolean { if (!config.isFile) return true val text = runCatching { config.readText() }.getOrElse { @@ -391,7 +381,6 @@ object SessionFiles { installed = staged.setReadable(true, false) && staged.renameTo(movie) if (installed) { FileUtils.writeString(stamp, identity) - // Steam's picker draws its preview from this picture and does not redraw it when the movie changes. File(directory, name.removeSuffix(".webm") + ".jpg").delete() } } catch (e: Exception) { @@ -408,10 +397,6 @@ object SessionFiles { internal const val BOOT_MOVIES_DIR = "/uioverrides/movies/" internal const val BOOT_MOVIE_PATH = "/uioverrides/movies/bigpicture_startup.webm" - /** - * Steam's startup movie setting is the `StartupMovie` section inside its `Customization` section; the one under the Steam - * settings is where older clients kept it, and DroidDeck still keeps it in step. - */ private fun customizationMovie(text: String): VdfBlock? { var from = 0 while (true) { @@ -421,7 +406,6 @@ object SessionFiles { } } - /** The older `StartupMovie` section: the first one that is not inside `Customization`. */ private fun olderMovie(text: String): VdfBlock? { val inside = customizationMovie(text) var from = 0 @@ -432,10 +416,8 @@ object SessionFiles { } } - /** The section Steam reads: the one under `Customization` when there is one. */ private fun activeStartupMovie(text: String): VdfBlock? = customizationMovie(text) ?: olderMovie(text) - /** The movie Steam's config selects, as a path like `/uioverrides/movies/x.webm`; null when none is named. */ internal fun startupMoviePath(config: File): String? = startupMovieLocalPath(config)?.takeIf { it.isNotEmpty() } internal fun startupMovieLocalPath(config: File): String? { @@ -444,7 +426,6 @@ object SessionFiles { return scalar(text.substring(movie.keyStart, movie.close + 1), "LocalPath") } - /** Both `StartupMovie` sections of Steam's config as written there, for the report; null when there is none. */ internal fun startupMovieBlock(config: File): String? { val text = runCatching { config.readText() }.getOrNull() ?: return null val parts = listOfNotNull( @@ -454,7 +435,6 @@ object SessionFiles { return parts.takeIf { it.isNotEmpty() }?.joinToString("\n") } - /** The `Customization` section of Steam's config with the lines before it, for the report; null when there is none. */ internal fun customizationBlock(config: File): String? { val text = runCatching { config.readText() }.getOrNull() ?: return null val customization = findObject(text, "Customization", 0, text.length) ?: return null @@ -467,7 +447,6 @@ object SessionFiles { return text.replaceRange(movie.keyStart, movie.close + 1, setScalar(setScalar(block, "MovieID", "0", newline), "LocalPath", path, newline)) } - /** True when Steam's config selects DroidDeck's staged movie once this returns. */ internal fun ensureStartupMovieDefault(config: File, force: Boolean = false): Boolean { val path = BOOT_MOVIE_PATH val legacyPath = "/uioverrides/movies/droiddeck-startup.webm" @@ -494,7 +473,6 @@ object SessionFiles { return false } } - // Later sections first, so the positions of earlier ones stay valid. val targets = listOfNotNull(customizationMovie(text), olderMovie(text)).sortedByDescending { it.keyStart } for (target in targets) text = pointAt(text, target, path, newline) if (olderMovie(text) == null) { @@ -538,11 +516,6 @@ object SessionFiles { private val IDLE_SLEEP_TIMERS = listOf("IdleSuspendBatterySeconds", "IdleSuspendACSeconds") - /** - * Sets Steam's Power > "Sleep after" (battery and plugged in) to Disabled at every session start: Android - * owns the device's sleep, and a client that suspends inside the session only shows a black screen. Dimming stays the user's. Runs - * only before the client starts, which rewrites its config on exit. - */ internal fun disableIdleSleep(steamHome: File): Boolean { val config = File(steamHome, "config/config.vdf") val text = runCatching { config.readText() }.getOrNull() ?: return false @@ -573,7 +546,6 @@ object SessionFiles { } } - /** Adds the `Customization` section Steam reads, next to the `Software` section, when the config has none. */ private fun withCustomization(text: String, path: String, newline: String): String { val root = findObject(text, "InstallConfigStore", 0, text.length) ?: return text val lineStart = text.lastIndexOf('\n', root.close).let { if (it < 0) 0 else it + 1 } @@ -643,13 +615,6 @@ object SessionFiles { fun logDirectory(context: Context): File = LinuxRuntime.logDir(context).apply { mkdirs() } - /** - * Registers the Android system fonts (which include CJK faces such as Noto Sans CJK / - * MiSans) with the container's fontconfig, so the Steam Deck UI (CEF/webkit renderer) - * resolves Chinese/Japanese glyphs instead of showing tofu / mojibake. - * The fonts themselves are bind-mounted by LinuxRuntime.bindSystemFonts() into - * /usr/local/share/fonts/android; here we only need to point fontconfig at them. - */ private val DESKTOP_STRINGS = listOf( "desktop_restore_title" to R.string.desktop_restore_title, "desktop_restore_body" to R.string.desktop_restore_body, 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 ded53162c..185550370 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionLogShare.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionLogShare.kt @@ -69,7 +69,6 @@ object SessionLogShare { return zip } - /** Every entry is scrubbed line by line, whatever its size: a raw copy can carry tokens out of the device. */ private fun addEntry(z: ZipOutputStream, name: String, f: File) { z.putNextEntry(ZipEntry(name)) val w = z.bufferedWriter() 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 f73609f44..6f335f231 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPaths.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPaths.kt @@ -44,7 +44,6 @@ object SessionPaths { fun current(): File? = dir - /** Whether the current session folder will survive teardown. No session means the next one can retain logs. */ fun currentSessionLogsRetained(context: Context): Boolean = dir?.let { it.parentFile == SessionFiles.logDirectory(context) } ?: true 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 9ed3b6d27..bf189a174 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -125,14 +125,12 @@ object SessionPrefs { prefs(context).edit().putString("statsDash", id).apply() } - /** In-game stats "Send dashboard data" Automatic: on unless the user switched it off. */ fun telemetryAuto(context: Context): Boolean = prefs(context).getBoolean("telemetryAuto", true) fun setTelemetryAuto(context: Context, on: Boolean) { prefs(context).edit().putBoolean("telemetryAuto", on).apply() } - /** Games the user switched off by hand, as TelemetryOutput keys; Automatic leaves them alone. */ fun telemetryOff(context: Context): Set = prefs(context).getStringSet("telemetryOff", null)?.toSet().orEmpty() fun setTelemetryOff(context: Context, keys: Set) { @@ -163,13 +161,6 @@ object SessionPrefs { prefs(context).edit().putString("gameStoresSpeedTier", tier).apply() } - /** - * 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 storesShowMature(context: Context): Boolean = prefs(context).getBoolean("storesShowMature", false) @@ -246,7 +237,6 @@ object SessionPrefs { prefs(context).edit().putString("storesInstallTarget", path).apply() } - /** Empty means follow the app/system language for GOG and Epic installs. */ fun storesInstallLanguage(context: Context): String = prefs(context).getString("storesInstallLanguage", "") ?: "" fun setStoresInstallLanguage(context: Context, code: String) { @@ -472,10 +462,6 @@ object SessionPrefs { position.takeIf { it in listOf("top", "bottom", "center") } ?: "center").apply() } - /** - * After a game launched from the front end (or another launcher's link) closes, the client - * exits cleanly and the session returns there, instead of staying in Big Picture. - */ fun returnAfterGame(context: Context): Boolean = prefs(context).getBoolean("returnAfterGame", true) fun setReturnAfterGame(context: Context, on: Boolean) { @@ -485,11 +471,6 @@ object SessionPrefs { private fun shaderPrecacheApplied(context: Context) = java.io.File(com.droiddeck.launcher.runtime.LinuxRuntime.rootDir(context), "root/.bl-shader-precache-applied") - /** - * Shader pre-caching as the launcher last set it, until a session reports it written into the - * client's config.vdf; after that the client's own toggle is the user's again. A client that - * has never written a config.vdf starts with pre-caching off. - */ fun pendingShaderPrecache(context: Context): Boolean? { val prefs = prefs(context) if (!prefs.contains("shaderPrecache")) { @@ -660,7 +641,6 @@ object SessionPrefs { prefs(context).edit().putBoolean("gpuClockPin", on).apply() } - /** Whether the game's busiest threads are named to the power HAL for a boost (GameThreadHints). */ fun gameThreadHints(context: Context): Boolean = prefs(context).getBoolean("gameThreadHints", true) fun prootNoSeccomp(context: Context): Boolean = prefs(context).getBoolean("prootNoSeccomp", false) @@ -675,18 +655,12 @@ object SessionPrefs { prefs(context).edit().putBoolean("prootFastPath", on).apply() } - /** gamescope draws the cursor itself. Off by default: on a phone that plane costs frames. */ fun cursorComposite(context: Context): Boolean = prefs(context).getBoolean("cursorComposite", false) fun setCursorComposite(context: Context, on: Boolean) { prefs(context).edit().putBoolean("cursorComposite", on).apply() } - /** - * The rootfs ships Termux's unpatched proot. A session uses it only when this file exists, - * so a missing add-on pack cannot silently fall back to it. - * Path: Android/data/<package>/files/droiddeck-stock-proot - */ @JvmStatic fun allowStockProot(context: Context): Boolean = context.getExternalFilesDir(null)?.let { File(it, "droiddeck-stock-proot").isFile } == true @@ -772,7 +746,6 @@ object SessionPrefs { ) } - /** Extra Proton, Wine and graphics diagnostics for the next game launch; off unless chosen. */ fun gameDebugLogsEnabled(context: Context): Boolean = prefs(context).getBoolean("gameDebugLogsEnabled", false) @@ -926,7 +899,6 @@ object SessionPrefs { prefs(context).edit().putString("steamClientArch", id).apply() } - /** The running session's client, else the selected one: a changed setting applies from the next session. */ fun activeSteamClientArch(context: Context): String = SessionState.steamClientArch?.takeIf { SessionState.running } ?: steamClientArch(context) @@ -944,13 +916,10 @@ object SessionPrefs { fun setBootMovieSound(context: Context, on: Boolean) { prefs(context).edit().putBoolean("bootMovieSound", on).apply() } fun bootMovieVolume(context: Context): Int = prefs(context).getInt("bootMovieVolume", 100).coerceIn(0, 100) fun setBootMovieVolume(context: Context, percent: Int) { prefs(context).edit().putInt("bootMovieVolume", percent.coerceIn(0, 100)).apply() } - /** How long the boot movie plays, in seconds; 0 plays all of it. */ fun bootMovieSeconds(context: Context): Int = prefs(context).getInt("bootMovieSeconds", 0).coerceAtLeast(0) fun setBootMovieSeconds(context: Context, seconds: Int) { prefs(context).edit().putInt("bootMovieSeconds", seconds.coerceAtLeast(0)).apply() } - /** Set when a movie was picked here, so the next session makes it Steam's startup movie even if Steam had another one. */ fun bootMovieApplyNext(context: Context): Boolean = prefs(context).getBoolean("bootMovieApplyNext", false) fun setBootMovieApplyNext(context: Context, on: Boolean) { prefs(context).edit().putBoolean("bootMovieApplyNext", on).apply() } - /** The startup movie path Steam's config held when it was last looked at, so only a change made in Steam is taken over. */ fun bootMovieSteamSeen(context: Context): String? = prefs(context).getString("bootMovieSteamSeen", null) fun setBootMovieSteamSeen(context: Context, path: String) { prefs(context).edit().putString("bootMovieSteamSeen", path).apply() } fun bootMovieCustomName(context: Context): String? = prefs(context).getString("bootMovieCustomName", null) @@ -1114,13 +1083,11 @@ object SessionPrefs { prefs(context).edit().putString("theme", id).apply() } - /** The custom Gradient theme's primary and secondary colours (ARGB). */ fun gradientColors(context: Context): Pair = prefs(context).let { it.getInt("gradientPrimary", com.droiddeck.launcher.ui.Themes.DEFAULT_PRIMARY) to it.getInt("gradientSecondary", com.droiddeck.launcher.ui.Themes.DEFAULT_SECONDARY) } - /** How fast the Inner Gamer theme cycles its colours, 1 to 10. */ fun rgbSpeed(context: Context): Int = prefs(context).getInt("innerGamerSpeed", com.droiddeck.launcher.ui.Themes.DEFAULT_RGB_SPEED).coerceIn(1, com.droiddeck.launcher.ui.Themes.RGB_SPEEDS) @@ -1150,18 +1117,11 @@ object SessionPrefs { prefs(context).edit().putBoolean("hdr.$mode", on).apply() } - /** Compositor frame limit that releases buffers to the game unpaced. */ const val FPS_UNPACED = -1 - /** The drawer's frame limit slider: its first stop is Off. */ const val FPS_LIMIT_OFF = 15 const val FPS_LIMIT_MAX = 165 - /** - * Buffers go back to the game as soon as they are replaced instead of once per refresh; - * Steam's Frame Limit in the Quick Access menu caps Steam games, and the drawer's limit caps games - * launched without Steam. Mirroring still follows the glasses' refresh rate. - */ const val PIP_FPS = 30 fun compositorFpsLimit(mirrorRefreshLimit: Int, sessionLimit: Int = 0, pip: Boolean = false): Int { 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 6a631341a..f43a7f55a 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt @@ -107,7 +107,6 @@ class SessionService : Service() { private var freezeCancelled = false private var pendingSteamGameId: String? = null private var pendingSteamGameGeneration = -1 - // Bumped to cancel a running game-exit watch (a new launch, or the session ending). @Volatile private var gameExitWatchToken = 0 private val flushSteamGame = object : Runnable { override fun run() = flushQueuedSteamGame() @@ -477,7 +476,6 @@ class SessionService : Service() { .onFailure { Log.w(TAG, "agent environment skipped", it) } .getOrDefault(emptyList()) SessionFiles.stage(this, root, startsSession = true) - // Earlier sessions' reports still read the Steam logs and VAC marker this session replaces. SessionArtifacts.awaitCollections() SessionArtifacts.finishAbandoned(this, beforeGuestStart = true) VacDiagnostics.arm(this) @@ -535,7 +533,6 @@ class SessionService : Service() { guest.add("DXVK_HDR=1") Log.i(TAG, "hdr: gamescope --hdr-enabled, DXVK_HDR=1") } - // gamescope's WSI layer and winex11 reuse their per-present window checks for this long. guest.add("DROIDDECK_WSI_BYPASS_CHECK_MS=100") guest.add("WINE_OFFSCREEN_CHECK_MS=100") guest.add("BL_REFRESH=" + Math.round(SessionState.refreshHz)) @@ -1111,7 +1108,6 @@ class SessionService : Service() { } } val x64Libraries = org.json.JSONArray() - // Desktop mode offers the x86-64 client either way; droiddeck-steam-x64 drops library folders it is not given. val x64Here = SessionPrefs.steamClientArch(this) == "x86_64" || (SessionState.mode == MODE_DESKTOP && com.droiddeck.launcher.storage.SteamStorage.x64ClientPresent(SessionPrefs.steamRoot(this, "x86_64"))) @@ -1315,13 +1311,6 @@ class SessionService : Service() { Log.i(TAG, "queued Steam game $gameId for the running client") } - /** - * A game launched from the front end (the Games tab, or another launcher's link): once it has - * started and then been gone for [GAME_EXIT_GRACE_MS], ask the client to exit through the - * session script's steam-stop request. The client quits cleanly, finishing its cloud sync, so - * the session ends on its own and the activity returns to where the launch came from instead - * of leaving the player in Big Picture. The grace covers launchers that restart the game. - */ private fun watchGameExit(gameId: String, gen: Int) { if (!SessionPrefs.returnAfterGame(this)) return val token = ++gameExitWatchToken @@ -1336,7 +1325,6 @@ class SessionService : Service() { seen = true goneSince = 0L } else if (!seen) { - // Never started (an install, an update or a failed launch): leave the session be. if (now > startBy) { Log.i(TAG, "game $gameId did not start; not watching for its exit") return@Thread @@ -1362,8 +1350,6 @@ class SessionService : Service() { Log.i(TAG, "game $gameId closed: asking the client to exit and returning to the launcher") } - /** A Steam game is running: the client runs each one under its reaper, and Proton's script - * waits on the game with waitforexitandrun. Read from the command lines, as ComponentsManager does. */ private fun steamGameRunning(): Boolean = SteamGameProcess.running() private fun resumeSession(panelVisible: Boolean = true) { @@ -1576,7 +1562,6 @@ class SessionService : Service() { if (suspendPolicy == SessionPrefs.SUSPEND_MANUAL && hidden && !downloadsBlockingSuspend) { manualPauseRequested = true } else if (!hidden && !SessionState.suspended) { - // Coming back before the freeze finished keeps the game running instead of pausing it a moment later. manualPauseRequested = false } val controller = suspendController ?: return @@ -1599,7 +1584,6 @@ class SessionService : Service() { SessionPrefs.SUSPEND_MANUAL -> manualPauseRequested && prepared && !downloadsBlockingSuspend else -> false } - // A frozen game never sends the stop for a held rumble, so the keepalive would run on. val quietRumble = !screenOn || shouldSuspend || SessionState.suspended components.forEach { (it as? RumbleComponent)?.quiet(quietRumble) } if (freezePending && !shouldSuspend && !freezeCancelled) { @@ -1919,7 +1903,7 @@ class SessionService : Service() { override fun onDestroy() { stopSession(0) - GameThreadHints.stop() // also when the session never got as far as running + GameThreadHints.stop() if (screenReceiverRegistered) { unregisterReceiver(screenReceiver) screenReceiverRegistered = false @@ -2158,9 +2142,7 @@ class SessionService : Service() { private const val DOWNLOAD_POLL_MS = 2_000L private const val STEAM_GAME_REQUEST = "steam-game" private const val GAME_URL_PREFIX = "steam://rungameid/" - /** How long a front-end launch may take to start (downloads, first-run installs) before the exit watch gives up. */ private const val GAME_START_TIMEOUT_MS = 20 * 60_000L - /** How long the game must stay gone before the client is asked to exit: launchers restart games. */ private const val GAME_EXIT_GRACE_MS = 10_000L private const val GAME_EXIT_POLL_MS = 2_000L private const val NO_PAD_SWITCH = "Download/droiddeck-no-pad" 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 c93a2758b..8e63d05c6 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionState.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionState.kt @@ -72,8 +72,6 @@ object SessionState { var steamUrl: String? = null @Volatile var steamClientArch: String? = null - // A Play SteamVR request can arrive while an existing session is visible. - // Wake the status/launch effect even if no glasses surface has attached yet. var steamVr by mutableStateOf(false) var relaunch: android.content.Intent? = null @JvmStatic var hdr = false @@ -96,7 +94,6 @@ object SessionState { @Volatile var secondScreenAutoOpened = false @Volatile var secondScreenProvisional = false - /** Guest library paths on removable storage and the host folders they are mounted from, set by SessionService. */ @Volatile var steamLibraryMounts: Map = emptyMap() @Volatile @@ -115,7 +112,6 @@ object SessionState { @Volatile var upscaleRatio: Float = 0f - /** Frame limit chosen in the drawer for a game launched without Steam; 0 is off. */ @Volatile var fpsLimit: Int = 0 diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionSuspendController.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionSuspendController.kt index adb792e71..b0a685214 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionSuspendController.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionSuspendController.kt @@ -26,9 +26,7 @@ internal class SessionSuspendController( private val nowNanos: () -> Long = System::nanoTime, private val pagedOut: (processes: Int, unacknowledged: Int, residentKb: Long, swapKb: Long, swapBeforeKb: Long, millis: Long) -> Unit = { _, _, _, _, _, _ -> }, - // False with a RAM cap set: the user asked for the session to use less memory, so nothing is read back. private val readBackAllowed: () -> Boolean = { true }, - // processes is 0 when the read-back was skipped: a RAM cap, or MemAvailable could not take it. private val pagedIn: (processes: Int, swapKb: Long, swapBeforeKb: Long, availableKb: Long, millis: Long) -> Unit = { _, _, _, _, _ -> }, ) { @@ -113,12 +111,10 @@ internal class SessionSuspendController( if (interrupted) Thread.currentThread().interrupt() } completion(complete) - // A cancelled or failed freeze leaves the game running with whatever the page-out already moved. if (!complete && frozen.isEmpty() && !interrupted) readBack() }, { completion(false) }) } - // A freeze still queued or paging out stops before its next SIGSTOP pass and rolls back. fun cancelFreeze() { cancelled = true } @@ -252,7 +248,6 @@ internal class SessionSuspendController( } private fun stoppedSnapshot(records: List, root: ProcessRecord): Map? { - // PRoot can leave the leader stopped while native worker threads keep running. val tasks = LinkedHashMap() var stopped = true for (record in records) { @@ -279,9 +274,6 @@ internal class SessionSuspendController( return tasks.takeIf { stopped && it.isNotEmpty() } } - // A stopped process cannot run libblsession's SIGURG handler, so the page-out has to finish - // before the stop. Android's low-memory killer otherwise finds the frozen game resident in RAM - // with most of zram still free. private fun pageOut(records: MutableMap, ProcessRecord>, root: Int, helpers: List) { val control = zramControl?.takeIf { it.isFile } ?: return try { @@ -304,8 +296,6 @@ internal class SessionSuspendController( val start = nowMillis() val before = memoryKb(targets) for (record in targets) send(record, SIGURG) - // A program can replace libblsession's SIGURG handler (PyInstaller's bootloader, the Go runtime); - // such a process never creates its ack and is not waited for once the start window has passed. val waiting = targets.toMutableList() while (!closing && !cancelled && nowMillis() - start < PAGE_OUT_TIMEOUT_MS) { val started = nowMillis() - start >= PAGE_OUT_START_MS @@ -319,13 +309,6 @@ internal class SessionSuspendController( pagedOut(targets.size, unacknowledged, after.first, after.second, before.second, nowMillis() - start) } - // Left in zram, the paged-out memory comes back one major fault at a time while the game runs; in a heavy - // game that is hundreds of faults per frame and stalls of a quarter second and more, long after the resume. - // libblsession's handler reads back only the swapped pages (mode 0, as the session script does for the client). - // The handler runs on whichever thread takes the signal, so that thread pauses for the read-back, - // once and right after the resume instead of mid-game. - // It signals even with nothing in zram yet: after a cancelled freeze a page-out handler may not have started, - // and it reads the mode only when it starts, so it reads back instead of paging out. private fun readBack() { val targets = pagedOutTargets.filterNot { isGone(it) } pagedOutTargets = emptyList() @@ -360,15 +343,10 @@ internal class SessionSuspendController( pagedIn(targets.size, swap, before, available, nowMillis() - start) } - // Reading back moves the swap into resident memory, out of MemAvailable. On a device short on memory that - // could push Android's low-memory killer onto DroidDeck itself, so the memory comes back only when - // MemAvailable is at least twice the swap and keeps 1 GB or a tenth of MemTotal, whichever is more, after it. - // Otherwise it stays in zram and faults back in as the game touches it, as before. private fun canTake(swapKb: Long, totalKb: Long, availableKb: Long): Boolean = availableKb >= swapKb * READ_BACK_AVAILABLE_FACTOR && availableKb - swapKb >= maxOf(READ_BACK_HEADROOM_KB, totalKb / READ_BACK_TOTAL_SHARE) - // MemTotal and MemAvailable, 0 when unreadable (which skips the read-back). private fun meminfoKb(): Pair = try { val values = HashMap() File(procRoot, "meminfo").forEachLine { line -> @@ -402,9 +380,6 @@ internal class SessionSuspendController( return resident to swap } - // PRoot traces every guest. A stop sent while it runs becomes a group stop that it restarts - // thread by thread; once it is stopped, each thread that takes a SIGSTOP stays parked in its - // signal-delivery stop. So the roots stop first and every guest thread gets its own SIGSTOP. private fun stop(records: List) { for (record in records.filter { it.role == Role.SESSION_ROOT }) send(record, SIGSTOP) for (record in records.filter { it.role == Role.HELPER_ROOT }) send(record, SIGSTOP) @@ -469,7 +444,6 @@ internal class SessionSuspendController( val current = try { readProcess(record.pid) } catch (_: Exception) { return false } if (current == null || current.started != record.started) return true if (current.state == 'T' || current.state == 't') return false - // A traced worker in 't' is usually PRoot handling a system call; 'T' is a job-control stop. return File(procRoot, "${record.pid}/task").listFiles().orEmpty().none { entry -> entry.name.toIntOrNull() != null && try { readStat(File(entry, "stat"))[0].first() == 'T' } catch (_: IOException) { false } diff --git a/app/src/main/java/com/droiddeck/launcher/session/StatsLayout.kt b/app/src/main/java/com/droiddeck/launcher/session/StatsLayout.kt index d0ff1486a..a8857c739 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/StatsLayout.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/StatsLayout.kt @@ -1,6 +1,5 @@ package com.droiddeck.launcher.session -/** The In-game stats page's top strip, picked on the page (tap or the Layout row) and remembered. */ enum class StatsStrip(val id: String) { PANEL("panel"), TILES("tiles"), RINGS("rings"); @@ -11,28 +10,23 @@ enum class StatsStrip(val id: String) { } } -/** Speeds on the racing dashes; telemetry arrives in km/h. */ enum class SpeedUnit(val id: String, val label: String, val perKmh: Float) { AUTO("auto", "", 1f), KMH("kmh", "km/h", 1f), MPH("mph", "mph", 0.621371f); - /** Auto is mph where road speed limits are signed in mph, by the device's region, else km/h. */ fun resolve(country: String): SpeedUnit = if (this != AUTO) this else if (country.uppercase() in MPH_REGIONS) MPH else KMH fun next(): SpeedUnit = entries[(ordinal + 1) % entries.size] companion object { - /** The United States and its territories, the United Kingdom and its Crown Dependencies, Liberia, the Bahamas, Belize. */ private val MPH_REGIONS = setOf("US", "PR", "GU", "VI", "AS", "MP", "GB", "IM", "JE", "GG", "LR", "BS", "BZ") fun from(id: String?): SpeedUnit = entries.firstOrNull { it.id == id } ?: AUTO } } -/** The racing dash under the strip; Auto picks one by the game's data format. */ enum class StatsDash(val id: String) { AUTO("auto"), SPEEDOMETER("speedometer"), CIRCUIT("circuit"), TRACE("trace"), DIALS("dials"), GLANCE("glance"); - /** The dash to draw for data from [source] (Telemetry.source). */ fun resolve(source: String): StatsDash = if (this != AUTO) this else when (source) { "Forza" -> CIRCUIT "OutGauge" -> DIALS @@ -44,7 +38,6 @@ enum class StatsDash(val id: String) { fun from(id: String?): StatsDash = entries.firstOrNull { it.id == id } ?: AUTO - /** A tap on the dash moves on from the one on screen, so Auto never looks like a tap did nothing. */ fun tapped(shown: StatsDash): StatsDash = DRAWN[(DRAWN.indexOf(shown) + 1).mod(DRAWN.size)] } } diff --git a/app/src/main/java/com/droiddeck/launcher/session/SteamDeckRepo.kt b/app/src/main/java/com/droiddeck/launcher/session/SteamDeckRepo.kt index 2a940a96f..9503e8e2f 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SteamDeckRepo.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SteamDeckRepo.kt @@ -7,12 +7,10 @@ import java.net.HttpURLConnection import java.net.URL import java.net.URLEncoder -/** steamdeckrepo.com's public boot video list, the same one its website and its own manager app read. */ object SteamDeckRepo { private const val TAG = "SteamDeckRepo" const val BASE = "https://steamdeckrepo.com" - /** The site shows twelve movies a page. */ const val PAGE_SIZE = 12 const val MAX_DOWNLOAD_BYTES = 120L shl 20 private const val MAX_JSON_BYTES = 1 shl 20 @@ -23,7 +21,6 @@ object SteamDeckRepo { TRENDING("trending"), DOWNLOADS("downloads-desc"), LIKES("likes-desc"), NEWEST("created_at-desc"), } - /** `null` is every device. */ val DEVICES = listOf("steam_deck", "steam_machine") class Post( @@ -36,7 +33,6 @@ object SteamDeckRepo { object Failed : Page() } - /** The text of [key], empty when it is missing or JSON null, which `optString` would turn into the word "null". */ private fun JSONObject.text(key: String): String = if (isNull(key)) "" else optString(key) fun parse(json: String): List? { @@ -83,10 +79,6 @@ object SteamDeckRepo { return result } - /** - * The number of the last page for a search, found by asking for pages: the site does not say how many there are, but a - * page past the end is empty and the last one is short. Null when the site cannot be reached. - */ fun lastPage(sort: Sort, device: String?, search: String, cap: Int = 512, count: (Int) -> Int? = { p -> (fetch(sort, device, search, p) as? Page.Ok)?.posts?.size }): Int? { @@ -111,7 +103,6 @@ object SteamDeckRepo { return good } - /** Reads [input] to its end, or null as soon as it holds more than [limit] bytes, so a huge answer is never kept whole. */ internal fun readLimited(input: java.io.InputStream, limit: Int): ByteArray? { val out = java.io.ByteArrayOutputStream() val buffer = ByteArray(16 * 1024) @@ -142,7 +133,6 @@ object SteamDeckRepo { } } - /** Fetches a small picture such as a cover; null when it cannot be had. */ fun bytes(url: String, limit: Int = 4 shl 20): ByteArray? { if (!url.startsWith("https://")) return null var connection: HttpURLConnection? = null @@ -161,10 +151,6 @@ object SteamDeckRepo { } } - /** - * Downloads a movie to [target] through the site's own download address, so the site counts it like a download from - * its website, and falls back to the video's address. True when a file was saved. - */ fun download(post: Post, target: File, onProgress: (Int) -> Unit = {}): Boolean { for (url in listOf("$BASE/post/download/${post.id}", post.video)) { if (save(url, target, onProgress)) return true diff --git a/app/src/main/java/com/droiddeck/launcher/session/SteamDownloads.kt b/app/src/main/java/com/droiddeck/launcher/session/SteamDownloads.kt index b8b898457..83b101db4 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SteamDownloads.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SteamDownloads.kt @@ -3,7 +3,6 @@ package com.droiddeck.launcher.session import java.io.File import java.io.RandomAccessFile -/** A Steam download or update the client is working on. [total] is 0 while Steam has not sized it yet. */ data class SteamDownload( val appId: String, val name: String, @@ -13,7 +12,6 @@ data class SteamDownload( val paused: Boolean = false, ) -/** Reads Steam's own appmanifests and content log, the same files [SteamDownloadMonitor] watches. */ object SteamDownloads { fun read(runtimeRoot: File, mounts: Map = emptyMap(), client: String? = null): List { val names = HashMap() @@ -32,7 +30,6 @@ object SteamDownloads { return apps + shaders } - /** The arm64 and x86-64 Steam homes can both hold an unfinished manifest of one app; the page keys rows by app. */ internal fun onePerApp(downloads: List): List = downloads.groupBy { it.appId }.values.map { same -> same.maxBy { it.done } } @@ -46,7 +43,6 @@ object SteamDownloads { return SteamDownload(id, name, done, total, paused = (flags and UPDATE_PAUSED) != 0L) } - /** App ids whose latest shader-cache line in the content log says the update is still running. */ internal fun runningShaderUpdates(lines: Sequence): List { val state = LinkedHashMap() for (line in lines) { diff --git a/app/src/main/java/com/droiddeck/launcher/session/SteamWebLogin.kt b/app/src/main/java/com/droiddeck/launcher/session/SteamWebLogin.kt index f9075857f..356df06f7 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SteamWebLogin.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SteamWebLogin.kt @@ -8,20 +8,12 @@ import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.PBEKeySpec import javax.crypto.spec.SecretKeySpec -/** A cookie for one https host, in the form CookieManager.setCookie takes. */ data class WebCookie(val host: String, val header: String) -/** - * The web sign-in the Steam client in the session already has, so the bottom-screen chat opens signed in. - * Steam's built-in browser keeps it in its Chromium cookie store under config/htmlcache. With no keyring - * (SteamOS and DroidDeck) Chromium encrypts values as "v10": AES-128-CBC, key PBKDF2-SHA1("peanuts", - * "saltysalt", 1 round), IV of 16 spaces. Values are never logged. - */ object SteamWebLogin { val HOSTS = listOf("steamcommunity.com", "steam-chat.com") private const val COOKIES = "config/htmlcache/Default/Cookies" private const val V10 = "v10" - // From cookie store version 24 Chromium puts the SHA-256 of the host before each value. private const val HOST_HASH_VERSION = 24 private const val HOST_HASH_BYTES = 32 @@ -39,19 +31,16 @@ object SteamWebLogin { String(value, Charsets.UTF_8) }.getOrNull() - /** Steam's own pages send the sign-in with its "||" separator percent-encoded. */ internal fun header(name: String, value: String, httpOnly: Boolean): String { val v = if (name == "steamLoginSecure") value.replace("||", "%7C%7C") else value return "$name=$v; Path=/; Secure" + if (httpOnly) "; HttpOnly" else "" } - /** Empty when the client has no web sign-in, for example before the first login. */ fun read(runtimeRoot: File, client: String?, scratch: File): List { val homes = client?.let { listOf(if (it == "x86_64") SteamDownloadMonitor.STEAM_HOMES[1] else SteamDownloadMonitor.STEAM_HOMES[0]) } ?: SteamDownloadMonitor.STEAM_HOMES val store = homes.map { File(runtimeRoot, "$it/$COOKIES") }.filter { it.isFile }.maxByOrNull { it.lastModified() } ?: return emptyList() - // A copy, journal included: Steam keeps the store open and may be in the middle of a write. scratch.deleteRecursively() if (!scratch.mkdirs()) return emptyList() return try { diff --git a/app/src/main/java/com/droiddeck/launcher/session/TelemetryOutput.kt b/app/src/main/java/com/droiddeck/launcher/session/TelemetryOutput.kt index afd60b02d..b3d884148 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/TelemetryOutput.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/TelemetryOutput.kt @@ -4,17 +4,11 @@ import android.content.Context import com.droiddeck.launcher.frontend.Library import java.io.File -/** - * Codemasters games keep their UDP / motion output in My Games//hardwaresettings/hardware_settings_config.xml - * inside the game's Proton prefix and read it when the game starts. Turning it on points that element at the - * In-game stats listener; the file as it was is kept beside it, and its element is put back when turned off. - */ object TelemetryOutput { data class Game(val name: String, val prefixId: String, val file: File, val on: Boolean) data class MenuGame(val name: String, val setting: String, val port: Int) - /** Games whose output is only a setting in the game's own menus, by Steam app id. */ val MENU_GAMES: Map = mapOf( 1293830 to MenuGame("Forza Horizon 4", "Data Out", GameTelemetry.FORZA_PORT), 1551360 to MenuGame("Forza Horizon 5", "Data Out", GameTelemetry.FORZA_PORT), @@ -28,7 +22,6 @@ object TelemetryOutput { private const val ADDRESS = "127.0.0.1" private const val EXTRADATA = "3" private val TAGS = listOf("udp", "motion") - // Byte-for-byte: every byte maps to one char and back, whatever the file's own encoding is. private val BYTES = Charsets.ISO_8859_1 fun scan(compatdata: List): List = compatdata.asSequence() @@ -43,7 +36,6 @@ object TelemetryOutput { .sortedBy { it.name.lowercase() } .toList() - /** Turns the game's output on or off; false when the file could not be read or written. */ fun set(game: Game, on: Boolean): Boolean = runCatching { val file = game.file val original = File(file.path + ORIGINAL_SUFFIX) @@ -62,7 +54,6 @@ object TelemetryOutput { true }.getOrDefault(false) - /** Automatic: turns the output on for every found game the user has not switched off and that is not running. */ fun applyAutomatic(context: Context, running: String?) { if (!SessionPrefs.telemetryAuto(context)) return automaticTargets(scan(Library.compatdataDirs(context)), SessionPrefs.telemetryOff(context), running).forEach { set(it, true) } @@ -73,11 +64,9 @@ object TelemetryOutput { internal fun automaticTargets(games: List, off: Set, running: String?): List = games.filter { !it.on && key(it) !in off && !isRunning(it.prefixId, running) } - /** Games DroidDeck changed, which go back to their own setting when Automatic is switched off. */ internal fun restoreTargets(games: List, running: String?): List = games.filter { File(it.file.path + ORIGINAL_SUFFIX).isFile && !isRunning(it.prefixId, running) } - /** Whether [running] (the focused Steam app id, or another key for non-Steam programs) may be the game in [prefixId]. */ internal fun isRunning(prefixId: String, running: String?): Boolean { val id = running?.toLongOrNull() ?: return running != null return prefixId == id.toString() || prefixId == id.toInt().toString() diff --git a/app/src/main/java/com/droiddeck/launcher/session/VacDiagnostics.kt b/app/src/main/java/com/droiddeck/launcher/session/VacDiagnostics.kt index 2f0d03c28..e38d1c454 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/VacDiagnostics.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/VacDiagnostics.kt @@ -10,17 +10,9 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -/** - * Source games state VAC-secure or insecure only in their console, which reaches a file (console.log in the - * mod folder) only with -condebug. The report quotes the lines it found and says inconclusive rather than guessing. - */ object VacDiagnostics { private const val TAG = "VacDiagnostics" - /** - * Checked by the launch wrapper. Lines: session start (epoch seconds), the Steam client it ran, then - * "lengthcanonical path" for each console.log already there. - */ private const val MARKER = "root/.droiddeck-vac-diagnostics" private const val LAUNCHES = "root/.droiddeck-vac-launches" @@ -30,7 +22,6 @@ object VacDiagnostics { private const val MAX_QUOTED = 40 private val INSECURE = Regex("insecure mode", RegexOption.IGNORE_CASE) - // "Connecting to" is only an attempt: L4D2 refuses a VAC server with a dialog that never reaches the console. private val CONNECTED = Regex("^Connected to", RegexOption.IGNORE_CASE) private val ATTEMPTED = Regex("^Connecting to", RegexOption.IGNORE_CASE) private val NOTABLE = Regex("\\bVAC\\b|secure|Connected to|Connecting to|Disconnect|challenge|game coordinator", RegexOption.IGNORE_CASE) @@ -45,7 +36,6 @@ object VacDiagnostics { File(root, LAUNCHES).delete() if (enabled(context)) { val arch = SessionPrefs.steamClientArch(context) - // -condebug appends, so each console.log's length now is where this session's lines start. val sizes = consoleLogs(context, arch).joinToString("") { "${it.length()}\t${canonical(it)}\n" } marker.writeText("${System.currentTimeMillis() / 1000}\n$arch\n$sizes") } else marker.delete() @@ -57,7 +47,6 @@ object VacDiagnostics { private const val SNAPSHOT_MARKER = ".vac-marker" private const val SNAPSHOT_LAUNCHES = ".vac-launches" - /** Call when a session stops, before the next [arm] replaces its marker and launch list. */ fun snapshot(context: Context, dir: File) { val root = LinuxRuntime.rootDir(context) try { @@ -145,7 +134,6 @@ object VacDiagnostics { "Inconclusive - the game never connected to a server. Join a VAC-secured server, then check again." } - /** Source writes //console.log, Source 2 /game//console.log. */ private fun consoleLogs(context: Context, arch: String): List { val found = LinkedHashMap() for (library in libraryRoots(context, arch)) { @@ -176,7 +164,6 @@ object VacDiagnostics { return roots.filter { it.isDirectory }.distinctBy { runCatching { it.canonicalPath }.getOrDefault(it.path) } } - /** Two libraries can hold the same game and mod folder; a repeated name takes a tag from the log's path. */ internal fun consoleNames(logs: List): List { val used = HashSet() return logs.map { log -> diff --git a/app/src/main/java/com/droiddeck/launcher/session/VideoEncodeBridge.kt b/app/src/main/java/com/droiddeck/launcher/session/VideoEncodeBridge.kt index 8e4b24730..b7cc20a43 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/VideoEncodeBridge.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/VideoEncodeBridge.kt @@ -4,7 +4,6 @@ import android.util.Log import com.droiddeck.launcher.core.SessionPart import java.io.File -/** Steam's Remote Play V4L2 encoder in the guest talks to this MediaCodec encoder (venc.c, venc_server.cpp). */ class VideoEncodeBridge(val socket: File) : SessionPart() { override fun start() { socket.parentFile?.mkdirs() 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 4b355caa4..21398bc4c 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/WinComponents.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/WinComponents.kt @@ -103,11 +103,6 @@ object WinComponents { const val VCREDIST_ARM64 = "vcredist2022_arm64" - /** - * The ARM64 Visual C++ 2015-2022 runtime as registry values only (the version a 14.44.35211 installer writes, in both - * registry views). Xbox games' launchers running as ARM64EC check for it; Wine's own CRT does the work. Same approach as - * MGamer404's Bannerlator #627. Used until the catalog carries a component of this name. - */ internal fun builtInArm64Runtime(): Component { fun value(view: String, name: String, type: String, data: String) = Step("set_register_key", JSONObject() .put("action", "set_register_key").put("key", "HKLM\\Software\\${view}Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\arm64") @@ -388,7 +383,6 @@ object WinComponents { val archiveName = archive.optString("name") val url = c.snapshot.substringBeforeLast('/') + "/" + archiveName val file = File(work, "${c.name}.files.tar.xz") - // The snapshot came straight from github.com, so its digest may vouch for a mirror's copy of the archive. val sha = archive.optString("sha256").takeIf { it.matches(Regex("(?i)[0-9a-f]{64}")) } if (!Downloader.downloadFile(url, file, false, sha) { f -> onProgress(Progress(c.name, archiveName, Phase.DOWNLOAD, if (f < 0) -1 else Math.round(f * 100f))) }) { return null to "download failed: $archiveName" diff --git a/app/src/main/java/com/droiddeck/launcher/storage/SteamStorage.kt b/app/src/main/java/com/droiddeck/launcher/storage/SteamStorage.kt index 9697addd6..de3648e83 100644 --- a/app/src/main/java/com/droiddeck/launcher/storage/SteamStorage.kt +++ b/app/src/main/java/com/droiddeck/launcher/storage/SteamStorage.kt @@ -48,7 +48,6 @@ object SteamStorage { ) } - /** The folder alone does not mean installed: Decky's CEF marker creates it. */ fun x64ClientPresent(root: File): Boolean = File(root, "steamrt64/steam").isFile || File(root, "package/droiddeck-installed").isFile || File(root, "steamapps").isDirectory @@ -58,7 +57,6 @@ object SteamStorage { private val PENDING_MANIFEST = Regex("appmanifest_(\\d+)\\.acf\\..+") - // A partial download has no manifest yet, only a temporary one and its staging folder. fun manifests(library: File): Listing? { val apps = File(library, "steamapps") val names = apps.list() ?: return null @@ -91,8 +89,6 @@ object SteamStorage { enum class Kind { ARM_PROTON, REQUIRED, OLD_RUNTIME, X86_PROTON } - // DroidDeck's ARM64 Steam client cannot run Valve's x86 Protons, and its own Proton and FEX tools - // replace the scout and soldier container runtimes. fun kind(m: Manifest): Kind { val dir = m.installDir.orEmpty() return when { @@ -146,7 +142,6 @@ object SteamStorage { private val ARM_IN_NAME = Regex("arm64|aarch64") - // The x86-64 client ignores tools named for arm64; droiddeck-steam-x64 names its wrappers with a64 instead. fun x64ToolName(name: String): String = name.replace(ARM_IN_NAME, "a64") class ToolUse(val mappings: Map, val installed: Set, val x64: Boolean = false) { diff --git a/app/src/main/java/com/droiddeck/launcher/storage/StorageScan.kt b/app/src/main/java/com/droiddeck/launcher/storage/StorageScan.kt index aa0a76cb2..9c5fff91a 100644 --- a/app/src/main/java/com/droiddeck/launcher/storage/StorageScan.kt +++ b/app/src/main/java/com/droiddeck/launcher/storage/StorageScan.kt @@ -89,7 +89,6 @@ object StorageScan { fun measure(file: File): Long = size(file).also { known[file.path] = it } } - // Proton tools' files live only in the arm64 client's root; the x86-64 client has wrappers of them. fun steamRoot(context: Context) = SessionPrefs.steamRoot(context, "arm64") fun scan(context: Context): StorageReport { @@ -113,7 +112,6 @@ object StorageScan { Library.steamLibraries(context, arch).map { (root, label) -> val internal = canonical(root) == primary val data = if (internal) root else SecondaryLibrary.privateRoot(files, root) - // The x86-64 card folder and its steamapps appear only with that client's first session and library write. val listing = SteamStorage.manifests(root) ?: SteamStorage.Listing(emptyList(), emptySet(), true).takeIf { x64 && (if (internal) !File(root, "steamapps").exists() else !root.exists() && root.parentFile?.isDirectory == true) @@ -127,7 +125,6 @@ object StorageScan { .filter { !SteamStorage.isTool(it) && it.installed }.map { it.appId.toString() }.toSet() val armUse = runCatching { SteamStorage.compatMappings(File(steamRoot, "config/config.vdf").readText()) }.getOrNull() ?.let { SteamStorage.ToolUse(it, installedGames(false)) } - // An x86-64 client without a config.vdf has never mapped a game to anything. val x64Vdf = x64Root?.let { File(it, "config/config.vdf") } val x64Use = if (x64Vdf == null || !x64Vdf.exists()) SteamStorage.ToolUse(emptyMap(), emptySet(), x64 = true) else runCatching { SteamStorage.ToolUse(SteamStorage.compatMappings(x64Vdf.readText()), installedGames(true), x64 = true) }.getOrNull() diff --git a/app/src/main/java/com/droiddeck/launcher/storage/StorageState.kt b/app/src/main/java/com/droiddeck/launcher/storage/StorageState.kt index f7086336e..503e769be 100644 --- a/app/src/main/java/com/droiddeck/launcher/storage/StorageState.kt +++ b/app/src/main/java/com/droiddeck/launcher/storage/StorageState.kt @@ -115,7 +115,6 @@ object StorageState { }, "storage-remove-tool").start() } - // Every action works from a fresh scan so nothing is deleted on the strength of an older one. private fun act(context: Context, name: String, action: (Context, StorageReport) -> Boolean, onDone: (Long?) -> Unit = {}) { if (cleaning || SessionState.running) { onDone(null); return } val app = context.applicationContext @@ -134,7 +133,6 @@ object StorageState { }, "storage-$name").start() } - // A game can be installed in both Steam clients, and an id can be left over in several libraries. fun clearShaders(context: Context, appId: Int, x64: Boolean = false) = act(context, "shaders-$appId", { _, fresh -> val dir = fresh.games.firstOrNull { it.appId == appId && it.x64 == x64 }?.shaderDir ?: return@act false FileOps.delete(dir) @@ -204,7 +202,6 @@ object StorageState { } } - // Steam treats an app as installed while its manifest exists, so the manifest goes first. private fun deleteTool(tool: ToolUsage) { tool.manifests.forEach { check(!it.exists() || it.delete()) { "cannot delete $it" } } tool.dirs.forEach { FileOps.delete(it) } 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 9dfc6b665..2b52fe358 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/CloudSavePaths.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/CloudSavePaths.kt @@ -12,10 +12,6 @@ object CloudSavePaths { class GogCloudLocation(val name: String, val template: String) - /** - * cloudStorage.enabled with an empty locations list is Galaxy SDK IStorage, whose objects the - * API names under a location called `__default`. - */ fun gogSaveFromRemote(enabled: Boolean, locations: List>, clientId: String): GogCloudLocation? { if (!enabled) return null val named = locations.filter { it.second.isNotEmpty() } 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 659ae7005..ace330415 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/CloudSaves.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/CloudSaves.kt @@ -131,10 +131,6 @@ object CloudSaves { } } - /** - * The selected cloud location's name: GOG prefixes every cloud file with it, and the local folder - * is the location itself. Older builds had no name cached; fetch it once for those. - */ private fun gogLocationName(context: Context, id: String, folder: File): String? { val p = GogPrefs.get(context) p.getString("cloud_location_$id", null)?.ifEmpty { null }?.let { return it } @@ -194,11 +190,6 @@ object CloudSaves { internal class Migration(val baseline: Map, val moved: Int, val cleaned: Int, val failed: Int) - /** - * Moves only files whose `"/"` key proves the old mapping wrote them; anything - * the game itself created under that name is left where it is. Identical duplicates are removed - * only when they prove the same. The name must be one path segment: it comes from remote config. - */ internal fun migrateNested(dir: File, name: String, base: Map): Migration? { if (name.isEmpty() || name == "." || name == ".." || name.contains('/') || name.contains('\\')) return null val nested = File(dir, name) @@ -254,8 +245,6 @@ object CloudSaves { private fun migrateGogNested(context: Context, id: String, folder: File, dir: File) { val p = GogPrefs.get(context) - // Run once per game: after this the flat layout is authoritative, even if the game itself - // keeps a folder named like the cloud location. if (p.getInt("cloud_layout_$id", 0) >= CLOUD_LAYOUT) return val name = gogLocationName(context, id, folder) ?: return val base = baseline(context, Store.GOG, id).orEmpty() @@ -281,9 +270,6 @@ object CloudSaves { 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) - fun downloadOrDefer(context: Context, store: Store, id: String): Result = downloadOrDefer(context, store, id, Executor { Thread(it, "cloud-deferred").start() }) 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 9c32bd4c1..3d46a28da 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/CloudTransport.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/CloudTransport.kt @@ -47,7 +47,6 @@ internal fun gunzip(data: ByteArray): ByteArray? = try { java.util.zip.GZIPInputStream(data.inputStream()).use { it.readBytes() } } catch (e: Exception) { null } -/** Same gzip GOG uses on PUT: deflate level 6, mtime 0, OS Unix. */ internal fun gzipGogPut(data: ByteArray): ByteArray { val deflater = Deflater(6, true) val raw = ByteArrayOutputStream() @@ -98,17 +97,14 @@ internal fun isoDate(s: String?): Long { class NoCloudSaves : Exception("no cloud saves") -/** GOG cloud names are `/`; the name is not part of the folder. */ internal fun splitGogName(name: String): Pair { val slash = name.indexOf('/') return if (slash <= 0 || slash == name.length - 1) null to name else name.substring(0, slash) to name.substring(slash + 1) } -/** Maps cloud names to paths under the selected location's folder; the chosen location name is returned. */ internal fun gogRelativeNames(files: List, location: String?): Pair> { val locations = files.mapNotNull { splitGogName(it.name).first }.distinct() - // Only infer a location when every name is prefixed; a mixed listing keeps its old shape. val allPrefixed = files.isNotEmpty() && files.all { splitGogName(it.name).first != null } val chosen = when { location != null && (location in locations || allPrefixed) -> location @@ -167,10 +163,6 @@ class GogCloud( return relative(raw) } - /** - * The API names every file `/`. The location's folder is the sync root, so the - * first segment is dropped. Older builds kept it, writing saves one folder too deep. - */ private fun relative(raw: List): List { val (chosen, files) = gogRelativeNames(raw, location) prefix = chosen?.let { "$it/" } 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 01c7eebcf..89b393654 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/CredentialCipher.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/CredentialCipher.kt @@ -12,7 +12,6 @@ import javax.crypto.spec.GCMParameterSpec object CredentialCipher { const val VERSION = 1 - /** Sealed with the hardware-keystore (TEE) key; [VERSION] envelopes keep opening with the older, possibly StrongBox, key. */ const val VERSION_TEE = 2 const val ALG = "AES/GCM" private const val TRANSFORMATION = "AES/GCM/NoPadding" @@ -23,7 +22,6 @@ object CredentialCipher { fun existing(): SecretKey = key() - /** Key for [VERSION] envelopes written before [VERSION_TEE]. */ fun legacy(): SecretKey = existing() val version: Int get() = VERSION @@ -35,8 +33,6 @@ object CredentialCipher { e is android.security.keystore.KeyPermanentlyInvalidatedException || e is KeyMissing || e is org.json.JSONException || e is IllegalArgumentException - // StrongBox took over 10 s per Xbox credential write on a tester's phone, past the backend's - // 10 s reply timeout, so new writes use a TEE key. The old alias stays readable. object Keystore : KeyProvider { private const val LEGACY_ALIAS = "droiddeck-store-credentials" private const val ALIAS = "droiddeck-store-credentials-tee" 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 027ab1243..ced9ae8fb 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/SteamLiveShortcuts.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/SteamLiveShortcuts.kt @@ -130,7 +130,6 @@ object SteamLiveShortcuts { if (evaluate(context, js) != null) Log.i(TAG, "live shortcuts: art $assetType for $id") } - /** Moves and resizes a game's logo in the running client; with null the client's default position comes back. */ fun setLogoPosition(context: Context, appId: Long, position: LogoPosition.Value?) { if (!clientRunning()) return val id = appId and 0xFFFFFFFFL 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 f69380f7c..5de9b5e1a 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreAccounts.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreAccounts.kt @@ -95,7 +95,6 @@ object StoreAccounts { return json.optString("display_name", "").ifEmpty { json.optString("username", "") }.ifEmpty { store.label } } - /** The name the Stores page shows. Xbox shows the gamertag, else the sign-in email with its name part masked. */ fun displayName(context: Context, store: Store): String? { if (store != Store.XBOX) return signedInAs(context, store) val json = read(context, store) ?: return if (isUnavailable(store)) store.label else null 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 14415fd9c..2b86be8b2 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreArt.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreArt.kt @@ -29,7 +29,6 @@ object StoreArt { return writeLogo(get(url) ?: return false, dst) } - /** A logo is drawn over the hero, so it stays a PNG with its transparency instead of going through [place]. */ internal fun writeLogo(bytes: ByteArray, dst: File): Boolean { if (dst.isFile && dst.length() > 0) return true val bitmap = runCatching { BitmapFactory.decodeByteArray(bytes, 0, bytes.size) }.getOrNull() ?: return false 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 967e56ca7..ad59e511a 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreCatalog.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreCatalog.kt @@ -26,7 +26,6 @@ data class CatalogItem( val logoUrl: String? = null, ) { val accessible: Boolean get() = owned || subscription - /** Licensed now, or a supported package the account's subscription can install. */ val installable: Boolean get() = accessible || includedWith.isNotBlank() val isDiscounted: Boolean get() = discountPercent > 0 && originalPrice.isNotBlank() val key: String get() = "${store.id}:$id" @@ -95,7 +94,6 @@ internal fun retainOrder(items: List, previous: List, key: (T) -> Stri return previous.map { byKey.getValue(key(it)) } } -/** A detail page opens for search results too; the library wins when it already knows the product. */ fun detailCatalogItem( store: Store, id: String, @@ -114,11 +112,6 @@ fun mergeItems(old: List?, new: List): List?, patched: List): List { if (old.isNullOrEmpty()) return patched val byKey = patched.associateBy { it.key } 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 d1acc8937..c26bcd17d 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreInstallRoot.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreInstallRoot.kt @@ -39,7 +39,6 @@ object StoreInstallRoot { } 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})") @@ -51,19 +50,12 @@ object StoreInstallRoot { @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)) { @@ -93,7 +85,6 @@ object StoreInstallRoot { fun customTarget(context: Context): Target? { val folder = defaultStorageFolder(context) ?: return null - // Writes are probed when the folder is chosen; File.canWrite() is unreliable on FUSE volumes. if (!folder.isDirectory) return null val label = SessionPrefs.defaultStorageFolderLabel(context).ifBlank { folderLabel(context, folder) } return Target(File(folder, ON_VOLUME), label, removable = true, folder = true) @@ -163,7 +154,6 @@ object StoreInstallRoot { } } - /** Xodus has its own PRoot mount table and cannot see the selected Steam library's SD bind. */ fun externalRootsForXodus(context: Context): List> { val roots = externalRoots(context) val selectedLibrary = GameStorage.effective(context)?.let { File(it.path, ON_VOLUME) } 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 2f4df83f2..c29ea4165 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreInstalls.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreInstalls.kt @@ -19,7 +19,6 @@ object StoreInstalls { StoreGameSidecar(store, id, title, exe = "", cover = cover, hero = hero, state = StoreGameSidecar.STATE_INSTALLING).write(folder) } - /** Epic commits verified files individually, not a whole-game transaction. Keep updates resumable. */ @JvmStatic fun markUpdating(folder: File) { val before = StoreGameSidecar.read(folder) ?: return diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreLanguage.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreLanguage.kt index bc9afe3c2..c8ab32800 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreLanguage.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreLanguage.kt @@ -6,18 +6,9 @@ import com.droiddeck.launcher.session.SessionPrefs import org.json.JSONArray import java.util.Locale -/** - * Language for a third-party store install ([Store.GOG], [Store.EPIC]): the app/system locale by - * default, or the code the user picked in the install dialog / store settings. - * - * GOG depots and Epic install tags use overlapping but not identical codes (`en` vs `en-US`, - * `nb` vs `no`). Matching is on the language part plus a few aliases, so a Polish phone selects - * `pl-PL` depots without the user having to know the store's spelling. - */ object StoreLanguage { const val EXTRA = "installLanguage" - /** Codes the settings list and Epic's tag table share; GOG often uses the same with a region. */ val KNOWN: List = listOf( "en", "pl", "de", "fr", "es", "es-419", "pt", "pt-BR", "it", "ru", "uk", "cs", "hu", "nl", "sv", "da", "fi", "nb", "ro", "bg", "el", "tr", "ar", "th", "vi", "ja", "ko", @@ -36,7 +27,6 @@ object StoreLanguage { fun selectable(store: Store): Boolean = store == Store.GOG || store == Store.EPIC - /** Tag actually sent to the store: settings override, else the app/system locale. */ @JvmStatic fun preferredCode(context: Context): String { val chosen = SessionPrefs.storesInstallLanguage(context) @@ -68,10 +58,6 @@ object StoreLanguage { fun fallbackList(store: Store): List = if (selectable(store)) KNOWN else emptyList() - /** - * One code from [available]. An exact normalized code wins (`pl-PL` over `pl`). A base-language - * match is used only when that exact code is not offered, then English, then the first code. - */ @JvmStatic fun pick(available: List, preferred: String): String { if (available.isEmpty()) return preferred @@ -82,11 +68,6 @@ object StoreLanguage { ?: available.first() } - /** - * True when a GOG depot whose `languages` array is [listed] overlaps [chosen], including a - * base-language match (`pl` accepts `pl-PL`). Shared depots (`*`, empty, missing) always match. - * Install selection uses [depotMatchesResolved] after [pick] has chosen a single code. - */ @JvmStatic fun depotAccepted(listed: Array?, chosen: String): Boolean { if (listed.isNullOrEmpty()) return true @@ -97,10 +78,6 @@ object StoreLanguage { } } - /** - * True when [listed] is shared, or contains the one code [resolved] already selected. - * `pl-PL` does not also keep a `pl` depot. - */ @JvmStatic fun depotMatchesResolved(listed: Array?, resolved: String): Boolean { if (listed.isNullOrEmpty()) return true @@ -109,7 +86,6 @@ object StoreLanguage { return listed.any { primary(it) == want } } - /** Normalized code without collapsing to the base language. `pl-PL` stays `pl-pl`. */ internal fun primary(code: String): String? { val raw = code.trim().lowercase().replace('_', '-') if (raw.isEmpty()) return null 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 bd94cfc3b..95cd69ebd 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreLogFiles.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreLogFiles.kt @@ -26,7 +26,6 @@ object StoreLogFiles { private val CLOCK = SimpleDateFormat("HH:mm:ss.SSS", Locale.US) - /** Appends [text] with a time stamp. Works in any of the app's processes. */ fun stamped(context: Context, text: String) = append(context, "${synchronized(CLOCK) { CLOCK.format(Date()) }} $text") fun append(context: Context, line: String) { 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 37169cf96..91d96f6ec 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreLoginActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreLoginActivity.kt @@ -183,10 +183,6 @@ class StoreLoginActivity : ComponentActivity() { return root } - /** - * The activity handles density changes itself so that folding between two displays of different - * density keeps the page and the sign-in in progress; the dp-based chrome sizes are applied again. - */ override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) if (!::topBar.isInitialized) return @@ -221,7 +217,6 @@ class StoreLoginActivity : ComponentActivity() { setBackgroundColor(0xFF0A0B0D.toInt()) settings.javaScriptEnabled = true settings.domStorageEnabled = true - // Store sign-in pages are web pages; they never need to read local files or content:// URIs. settings.allowFileAccess = false settings.allowContentAccess = false settings.userAgentString = userAgent 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 ff44ca763..cf774f47b 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoresState.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoresState.kt @@ -145,7 +145,6 @@ object StoresState { private val libraryAsked = java.util.concurrent.ConcurrentHashMap() - /** Once per store, again after a minute when the last sync never produced a library (offline, expired sign-in). */ fun loadLibrary(context: Context, store: Store) { if (!isSignedIn(store) || library.containsKey(store)) return val now = android.os.SystemClock.elapsedRealtime() @@ -205,7 +204,6 @@ object StoresState { } var openDownloads by mutableStateOf(false) - /** A "store:id" key another page asks the Stores page to open. */ var openGameRequest by mutableStateOf(null) private fun askForNotifications(context: Context) { @@ -323,10 +321,6 @@ object StoresState { internal fun post(block: () -> Unit) = main.post(block) - /** - * A worker thread whose failure is logged. An uncaught exception on a plain Thread reaches the crash - * handler, which ends the whole app, and none of these jobs is worth that. - */ private fun background(name: String, onFailure: () -> Unit = {}, block: () -> Unit) { Thread({ try { 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 baae64b3c..379161ebf 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 @@ -142,7 +142,6 @@ 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() @@ -196,7 +195,6 @@ object DownloadQueue { 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) 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 ce5afa91b..394164c4c 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 @@ -47,7 +47,6 @@ object EpicBackend : StoreBackend, EpicLaunchSupport { if (!quiet) StoresState.status[store] = app.getString(R.string.stores_sync_library) } val result = EpicLibrary.sync(app, force) { line -> if (!quiet) StoresState.post { StoresState.status[store] = line } } - // Throttled means the cache is fresh, possibly written by another sync after this one read it. val fresh = if (result == EpicLibrary.SyncResult.Throttled) EpicLibrary.cached(app).map { EpicLibrary.toCatalogItem(app, it) } else cached StoresState.post { StoresState.status.remove(store) 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 9332bb986..6a1e5f65c 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 @@ -47,8 +47,6 @@ object GogAuth { Log.i(TAG, "signed in") } - fun storedToken(context: Context): String? = StoreAccounts.read(context, Store.GOG)?.optString("access_token", "")?.ifEmpty { null } - @JvmStatic @Synchronized fun validToken(context: Context): String? { 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 59a370775..fe2153193 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 @@ -42,12 +42,9 @@ object GogBackend : StoreBackend { if (!quiet) StoresState.status[store] = app.getString(com.droiddeck.launcher.R.string.stores_sync_library) } val result = GogLibrary.sync(app, force, onStatus = { line -> if (!quiet) StoresState.post { StoresState.status[store] = line } }, onList = { list -> - // Mapped here, off the main thread: the listing paints the tab before any product is asked about. val items = list.map { GogLibrary.toCatalogItem(app, it) } - // An empty listing is not news: posting it would drop the cards already on screen. if (items.isNotEmpty()) StoresState.post { StoresState.library[store] = com.droiddeck.launcher.stores.mergeItems(StoresState.library[store], items) } }, onItems = { changed -> - // Only the products that came back are touched: a card already on screen keeps its object, and a product the grid has not seen yet is added where its title belongs. val items = changed.map { GogLibrary.toCatalogItem(app, it) } StoresState.post { StoresState.library[store] = com.droiddeck.launcher.stores.patchItems(StoresState.library[store], items) } }) @@ -95,7 +92,6 @@ object GogBackend : StoreBackend { private fun fetchLogo(id: String): String? = runCatching { StoreNet.get("https://api.gog.com/v2/games/$id?locale=en-US")?.let { logoOf(JSONObject(it)) } }.getOrNull() - /** `images.logo2x` from the products API is an opaque banner, not a logo; the v2 `logo` link is a transparent PNG. */ internal fun logoOf(v2: JSONObject): String? = GogStoreCatalog.absolutize(v2.optJSONObject("_links")?.optJSONObject("logo")?.optString("href")).takeIf { it.startsWith("https://") } 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 9b66b95de..0dafce41c 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 @@ -62,7 +62,6 @@ public final class GogDownloadManager { public final long bytes; public List dependencies = new ArrayList<>(); public boolean dependenciesRead = false; - /** Language actually written, after the manifest's depot list resolved the request. Null for builds that have no language depots. */ public String language; Result(String exeRelative, String buildId, long bytes) { this.exeRelative = exeRelative; this.buildId = buildId; this.bytes = bytes; } } @@ -71,7 +70,6 @@ public final class GogDownloadManager { InstallException(String message) { super(message); } } - /** A failure of the offline-installer fallback; its partial files can be discarded. */ public static final class InstallerException extends InstallException { InstallerException(String message) { super(message); } } @@ -311,7 +309,6 @@ public final class GogDownloadManager { return StoreLanguage.depotMatchesResolved(listed, resolved); } - /** Language codes listed on this product's Windows gen2 depots, excluding `*`. */ public static List listLanguages(Context ctx, String gameId) { try { String token = GogAuth.validToken(ctx); @@ -497,12 +494,6 @@ public final class GogDownloadManager { } } - /** - * GOG's Windows installer is a 32-bit Inno Setup stub. On ARM64 it runs through - * Proton/FEX, where its path strings are corrupted before CreateFileW (observed on - * Wine 11 + FEX: "Path not found"). Extracting the same files with innoextract in - * the Linux guest avoids that path entirely. - */ private static void extractInstaller(Context ctx, File installPath, String setupName, Callback cb, AtomicBoolean cancelled) throws InstallException { String guestDir = AddedGames.INSTANCE.guestPath(ctx, installPath); if (guestDir == null || guestDir.isEmpty()) { @@ -531,11 +522,6 @@ public final class GogDownloadManager { InstallerPart(String url, String filename, long size) { this.url = url; this.filename = filename; this.size = size; } } - /** - * Every file of the first usable Windows installer. GOG lists a setup stub and its data parts as - * separate files, often without a filename; the parts are named after the stub so the installer - * finds them when launched (setup_<id>.exe, setup_<id>-1.bin, ...). - */ static List installerParts(JSONObject downloads, String gameId) { List out = new ArrayList<>(); if (downloads == null) return out; 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 0e76d2fab..8f48740ee 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 @@ -21,13 +21,9 @@ object GogLibrary { private const val LAST_SYNC_KEY = "library_synced_at" private const val SKIPPED_KEY = "library_skipped" private const val THROTTLE_MS = 6L * 60L * 60L * 1000L - // mediaType=1 leaves the movies and soundtracks out, hiddenFlag=0 the hidden ones. private const val LIST_URL = "https://embed.gog.com/account/getFilteredProducts?mediaType=1&hiddenFlag=0&sortBy=title" private const val LIST_MAX_PAGES = 100 - // The listing's image is a bare hash, and the CDN serves it only through Galaxy's own formatters: the - // vertical cover is the one card art it carries. The wide background only the per-game fetch knows. private const val COVER_SUFFIX = "_glx_vertical_cover.webp" - // The older account endpoint: owned ids only. The fallback for a listing that cannot be read. private const val OWNED_URL = "https://embed.gog.com/user/data/games" private val syncing = AtomicBoolean(false) @@ -62,28 +58,22 @@ object GogLibrary { val token = GogAuth.validToken(app) ?: return SyncResult.NotLoggedIn onStatus(app.getString(R.string.stores_sync_game_list)) val listing = ownedListing(token) - // The listing is the only source of titles: without it the owned ids still sync, just without the early paint. val listed: List = listing?.games ?: ownedIds(token)?.map { GogGame(it, "", "", "", "", "", 1, null, "", "") } ?: return SyncResult.Failed(app.getString(R.string.stores_sync_gog_unreachable)) val named = listing != null StoresState.logLine(if (named) "GOG: listing ${listed.size} games" else "GOG: listing unreadable, ${listed.size} owned ids") - // A page that failed leaves games unnamed: they are still owned, so their cards and cached details stay. val ownedSet = listing?.owned ?: listed.mapTo(HashSet()) { it.gameId } val cachedList = cached(app) val stale = System.currentTimeMillis() - p.getLong(LAST_SYNC_KEY, 0L) >= THROTTLE_MS val heavy = force || cachedList.isEmpty() || stale val cachedIds = cachedList.mapTo(HashSet()) { it.gameId } - // A product the last sync found uninstallable stays out of the list while it is re-checked. val skipped = java.util.Collections.newSetFromMap(java.util.concurrent.ConcurrentHashMap()) skipped.addAll(p.getStringSet(SKIPPED_KEY, emptySet()).orEmpty()) val idsToFetch = if (heavy) ownedSet.toList() else ownedSet.filter { it !in cachedIds && it !in skipped } val merged = LinkedHashMap() - // The cache's own entry stays even when the game is re-fetched: its details beat the listing's. for (g in cachedList) if (g.gameId in ownedSet && g.gameId !in skipped) merged[g.gameId] = g - // A game the sync has not reached keeps the listing's entry, so its card is there from the start. for (g in listed) if (g.gameId !in merged && g.gameId !in skipped) merged[g.gameId] = g - // Only products that were asked about are cached: a bare listing entry is asked about again. val detailed = HashSet(cachedIds) if (named) onList(sorted(merged)) if (idsToFetch.isEmpty()) { @@ -101,15 +91,12 @@ object GogLibrary { for ((idx, f) in futures.withIndex()) { val g = runCatching { f.get() }.getOrNull() if (g != null) { - // The cover the card already shows stays, so the wide art lands over it instead of over a gap. val filled = g.withCover(merged[g.gameId]?.verticalCover) merged[g.gameId] = filled; detailed.add(g.gameId); fetched++; skipped.remove(g.gameId); onItems(listOf(filled)) } if ((idx + 1) % 5 == 0) onStatus(app.getString(R.string.stores_sync_progress, idx + 1, futures.size)) - // Saved as it goes: a sync the user closes the app on still leaves a library to paint next launch. if ((idx + 1) % 20 == 0) saveCache(p, merged.values.filter { it.gameId in detailed }) } - // A product this round found uninstallable leaves when the round is over, not in the middle of it. for (id in skipped) merged.remove(id) val finalList = merged.values.toList() saveCache(p, finalList.filter { it.gameId in detailed }) @@ -132,11 +119,9 @@ object GogLibrary { var page = 1 var pages = 1 while (page <= pages && page <= LIST_MAX_PAGES) { - // A page that cannot be read ends the listing; the pages already read still name most of the library. val listed = StoreNet.get("$LIST_URL&page=$page", bearer = token, userAgent = GogAuth.GALAXY_UA)?.let { parseListing(it) } if (listed == null) { StoresState.logLine("GOG: listing page $page of $pages unreadable") - // The unread pages hold games too: without their ids those games would leave the library and the cache. owned.addAll(ownedIds(token) ?: return null) break } @@ -145,7 +130,6 @@ object GogLibrary { pages = listed.totalPages page++ } - // Not one page read: the caller falls back to the owned ids. return all.ifEmpty { null }?.let { Listing(it, owned) } } @@ -166,7 +150,6 @@ object GogLibrary { val games = ArrayList(arr.length()) for (i in 0 until arr.length()) { val o = arr.optJSONObject(i) ?: continue - // 1801418160 is GOG's own "Galaxy" entry, in every library. val id = o.optLong("id", 0L).toString() if (id == "0" || id == "1801418160") continue if (o.optBoolean("isMovie", false) || !o.optBoolean("isGame", true)) continue diff --git a/app/src/main/java/com/droiddeck/launcher/stores/itch/ItchInstaller.kt b/app/src/main/java/com/droiddeck/launcher/stores/itch/ItchInstaller.kt index d4dcf1983..3896091e3 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/itch/ItchInstaller.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/itch/ItchInstaller.kt @@ -135,7 +135,6 @@ internal class ItchInstaller( private fun download(key: String, downloadKeyId: String, uploadId: String, version: String, target: File, expected: Long, handle: DownloadQueue.JobHandle) { - // A partial file is only resumed when it came from this exact upload build. val origin = File(target.path + ".from") val stamp = "$uploadId $version $expected" if (!target.isFile || runCatching { origin.readText() }.getOrNull() != stamp) { @@ -169,7 +168,6 @@ internal class ItchInstaller( if (have > 0) handle.log(if (append) "itch.io: resuming at ${have} bytes" else "itch.io: server ignored the resume, starting over") var done = if (append) have else 0L val total = conn.contentLengthLong.takeIf { it > 0 }?.let { if (append) it + have else it } ?: expected - // The first report is the queue's speed baseline, so resumed bytes don't read as instant throughput. handle.progress(done, total) val buffer = ByteArray(128 * 1024) conn.inputStream.use { input -> @@ -293,7 +291,6 @@ internal class ItchInstaller( return if (path == root || path.startsWith(root + File.separator)) file else null } - // No JVM library reads RAR5; the runtime's bsdtar (libarchive) does. private fun unrar(archive: File, handle: DownloadQueue.JobHandle, kind: String) { if (!LinuxRuntime.isInstalled(app)) throw IllegalStateException(app.getString(R.string.stores_itch_archive, kind)) val binds = if (listOf(app.filesDir, app.cacheDir).any { work.absolutePath.startsWith(it.absolutePath + "/") }) null @@ -437,8 +434,6 @@ internal class ItchInstaller( FileInputStream(source).use { input -> FileOutputStream(target).use { out -> input.copyTo(out, 128 * 1024) } } } - // Streaming readers stop silently on zips whose data descriptors disagree with the local headers; - // the central directory is authoritative. private fun unzip(archive: File, handle: DownloadQueue.JobHandle) { val total = archive.length() var written = 0L diff --git a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCatalog.kt b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCatalog.kt index d4a16c409..6f50a0303 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCatalog.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCatalog.kt @@ -10,7 +10,6 @@ import java.time.Instant internal object XboxCatalog { private val productId = Regex("[A-Z0-9]{12}") private val plans = setOf("CFQ7TTC0KGQ8", "CFQ7TTC0KHS0", "CFQ7TTC0P85B", "CFQ7TTC0K5DJ", "CFQ7TTC0K6L8", "CFQ7TTC10QFD") - /** Plans whose store offers advertise PC games. Library entitlements under any active plan count, as the backend resolves a PC package for them. */ private val pcPlans = mapOf( "CFQ7TTC0KGQ8" to "PC Game Pass", "CFQ7TTC0KHS0" to "Game Pass Ultimate", @@ -137,7 +136,6 @@ internal object XboxCatalog { private const val CLOUD_GAME_PASS = "29a81209-df6f-41fd-a528-2ae6b91f719c" private const val CLOUD_ALL = "ce573635-7c18-4d0c-9d68-90b932393470" - /** Microsoft's public cloud lists (catalog.gamepass.com "sigls"; ids as Better xCloud by redphx, MIT, uses them). */ fun cloudCatalog(market: String = Xodus.market()): XboxCloudCatalog = XboxCloudCatalog(cloudList(CLOUD_GAME_PASS, market), cloudList(CLOUD_ALL, market)) @@ -169,7 +167,6 @@ val CatalogItem.xboxPc: Boolean get() = extra["pc"] != "0" val CatalogItem.xboxCloud: Boolean get() = extra["cloud"] == "1" val CatalogItem.xboxCloudId: String get() = extra["cloudId"]?.takeIf { it.isNotEmpty() } ?: id -/** Microsoft lists console and PC editions as separate products; a cloud-only one with a PC game's title folds into that card. */ internal fun mergeEditions(games: List): List { fun key(title: String) = title.lowercase().replace(Regex("[^a-z0-9]+"), "") val cloudOnly = games.filter { !it.xboxPc && it.xboxCloud }.groupBy { key(it.title) } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudActivity.kt b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudActivity.kt index 1fa7bdfe5..66f2a1fc8 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudActivity.kt @@ -27,11 +27,6 @@ import androidx.webkit.WebSettingsCompat import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature -/** - * Microsoft's web player (xbox.com/play) in its own process and WebView data directory: the store sign-in - * clears its own web data after each attempt, so the player keeps a separate Microsoft session. Signing out of - * the store clears it ([clearWebSignIn]). - */ class XboxCloudActivity : ComponentActivity() { private var web: WebView? = null private var custom: View? = null @@ -61,7 +56,6 @@ class XboxCloudActivity : ComponentActivity() { loadWithOverviewMode = true userAgentString = chromeUserAgent(userAgentString) } - // No "X-Requested-With: " header: it marks the page as embedded in an app. if (WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST)) WebSettingsCompat.setRequestedWithHeaderOriginAllowList(view.settings, emptySet()) Log.i(TAG, "UA ${view.settings.userAgentString}") @@ -107,7 +101,6 @@ class XboxCloudActivity : ComponentActivity() { intent.getStringExtra("url")?.takeIf { url -> runCatching { java.net.URI(url) }.getOrNull()?.let { it.scheme == "https" && it.host == "www.xbox.com" && it.userInfo == null && it.port == -1 } == true } ?: START_URL - // Without the WebView markers the page treats this as Chrome for Android. private fun chromeUserAgent(default: String): String = default.replace("; wv)", ")").replace(Regex(" Version/\\d+(\\.\\d+)*"), "") @@ -124,7 +117,6 @@ class XboxCloudActivity : ComponentActivity() { if (hasFocus) hideBars() } - // A controller's unused B arrives as BACK; ask before leaving so a stray press never ends a stream. override fun dispatchKeyEvent(event: KeyEvent): Boolean { if (event.keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) { val fromController = event.source and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD || diff --git a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudGames.kt b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudGames.kt index 10b034eaa..1f8dd66e6 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudGames.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudGames.kt @@ -14,7 +14,6 @@ import java.io.File import java.net.HttpURLConnection import java.net.URL -/** Xbox games added to the Games tab to stream before (or without) installing them. */ object XboxCloudGames { class Entry(val id: String, val title: String, val poster: String?, val hero: String?) @@ -69,7 +68,6 @@ object XboxCloudGames { }.distinctBy { it.id } }.getOrDefault(emptyList()) - /** Games-tab rows for the added games that are not installed (an installed copy has its own row). */ fun rows(context: Context, installedIds: Set): List = entries.filter { it.id !in installedIds }.map { e -> val dir = artDir(context, e.id) val appId = rowId(e.id) @@ -100,10 +98,6 @@ object XboxCloudGames { private val prefetching = java.util.concurrent.atomic.AtomicBoolean(false) - /** - * Fetches the streaming tokens in the background while a cloud game is on screen (about 20 s), so Stream starts at - * once; both players take them from this process (the native one through XboxCloudTokenService). - */ fun prefetch(context: Context) { val app = context.applicationContext if (!prefetching.compareAndSet(false, true)) return @@ -116,7 +110,6 @@ object XboxCloudGames { }, "xcloud-prefetch").start() } - /** Streaming runs outside the Linux session; the session owns the screen, audio and controllers while it runs. */ fun stream(context: Context, id: String, title: String) { if (SessionState.running) { android.widget.Toast.makeText(context, "Close the Steam session to stream Xbox games.", android.widget.Toast.LENGTH_LONG).show() @@ -125,7 +118,6 @@ object XboxCloudGames { context.startActivity(XboxCloudPlayerActivity.playerIntent(context, id, title).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } - /** Whether the signed-in library says this game streams (cached library; reads disk). */ fun streams(context: Context, id: String): Boolean = runCatching { StoresState.library[Store.XBOX]?.firstOrNull { it.id == id }?.let { return it.xboxCloud } val app = context.applicationContext diff --git a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudPlayerActivity.kt b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudPlayerActivity.kt index 9b8d11c0d..354850932 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudPlayerActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudPlayerActivity.kt @@ -43,7 +43,6 @@ import com.droiddeck.launcher.stores.StoresState import org.json.JSONObject import java.util.concurrent.Executors -/** Runs the WebRTC client page (tools/xcloud-player); signalling and tokens stay on this side of the bridge. */ class XboxCloudPlayerActivity : ComponentActivity() { private lateinit var root: FrameLayout private var web: WebView? = null @@ -57,7 +56,6 @@ class XboxCloudPlayerActivity : ComponentActivity() { @Volatile private var webUrl: String? = null @Volatile private var settings = XboxCloudSettings() @Volatile private var sessionSignals: XboxCloudSignalling? = null - /** The settings this stream started with (stream-start settings changed since then need a restart). */ @Volatile private var startedWith: XboxCloudSettings? = null private var menu: androidx.compose.ui.platform.ComposeView? = null @@ -106,7 +104,6 @@ class XboxCloudPlayerActivity : ComponentActivity() { allowContentAccess = false } view.setBackgroundColor(Color.BLACK) - // The Gamepad API only sees controllers while the WebView has input focus. view.isFocusable = true view.isFocusableInTouchMode = true view.addJavascriptInterface(Bridge(), "DDCloud") @@ -225,7 +222,6 @@ class XboxCloudPlayerActivity : ComponentActivity() { private fun isGuideKey(event: KeyEvent): Boolean = event.keyCode == KeyEvent.KEYCODE_BUTTON_MODE - /** One press is the stream's Xbox button; two quick presses or a hold open the player menu. */ private fun guideKey(event: KeyEvent): Boolean { if (!isGuideKey(event)) return false if (event.action == KeyEvent.ACTION_DOWN) { @@ -256,7 +252,6 @@ class XboxCloudPlayerActivity : ComponentActivity() { web?.evaluateJavascript("window.__ddButton && window.__ddButton('Nexus', $value)", null) } - // Back opens the player menu so a stray press never ends a stream. override fun dispatchKeyEvent(event: KeyEvent): Boolean { if (menu != null) { if (isGuideKey(event)) { @@ -274,7 +269,6 @@ class XboxCloudPlayerActivity : ComponentActivity() { if (event.action == KeyEvent.ACTION_UP) { if (notice == null) openMenu() else finish() } return true } - // Reporting controller buttons handled stops Android turning an unused one into a fallback key. val handled = super.dispatchKeyEvent(event) val fromController = event.source and android.view.InputDevice.SOURCE_GAMEPAD == android.view.InputDevice.SOURCE_GAMEPAD || event.source and android.view.InputDevice.SOURCE_JOYSTICK == android.view.InputDevice.SOURCE_JOYSTICK @@ -389,7 +383,6 @@ class XboxCloudPlayerActivity : ComponentActivity() { fun intent(context: Context, productId: String, title: String): Intent = Intent(context, XboxCloudPlayerActivity::class.java) .putExtra("product", productId.uppercase()).putExtra("title", title) - /** The player the settings pick: the native engine (frame generation) where it can run, else the web player. */ fun playerIntent(context: Context, productId: String, title: String): Intent { val native = XboxNativePlayerActivity.supported() && XboxCloudSettings.load(context).playerEngine == "native" return if (!native) intent(context, productId, title) else Intent(context, XboxNativePlayerActivity::class.java) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudSession.kt b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudSession.kt index ca2120278..835a55166 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudSession.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudSession.kt @@ -6,7 +6,6 @@ import org.json.JSONObject import java.net.HttpURLConnection import java.net.URL -/** Streaming-service tokens stay in memory only. Request shapes follow Greenlight / xal-node by unknownskl (MIT). */ internal object XboxCloudSession { class Region(val name: String, val baseUri: String, val isDefault: Boolean) @@ -22,7 +21,6 @@ internal object XboxCloudSession { @Volatile private var cached: Session? = null - /** One fetch at a time (about 20 s: two ticket exchanges); a second caller waits and gets the same tokens. */ @Synchronized fun get(app: Context, account: String): Session { cached?.takeIf { it.account == account && it.fresh }?.let { return it } @@ -39,10 +37,6 @@ internal object XboxCloudSession { fun clear() { cached = null; titleIds = null } - /** - * For the native player's own process: the main process's tokens (it keeps them, and fetches them ahead while a - * cloud game's page is open), handed over in memory through XboxCloudTokenService; null if that is not reachable. - */ fun fromMainProcess(app: Context, account: String, timeoutSeconds: Long = 90): Session? { cached?.takeIf { it.account == account && it.fresh }?.let { return it } val reply = XboxCloudTokenService.request(app, account, timeoutSeconds) ?: return null diff --git a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudSettings.kt b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudSettings.kt index 7b8806a36..247b1b033 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudSettings.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudSettings.kt @@ -4,43 +4,41 @@ import android.content.Context import org.json.JSONObject data class XboxCloudSettings( - val resolution: String = "1080p", // 720p | 1080p | 1080p_hq - val region: String = "", // "" = Microsoft's choice - val language: String = "", // "" = the device's language - val codec: String = "default", // default | high | normal | low - val bitrateMbps: Int = 0, // 0 = no cap + val resolution: String = "1080p", + val region: String = "", + val language: String = "", + val codec: String = "default", + val bitrateMbps: Int = 0, val preventDrops: Boolean = false, - val statsLevel: Int = 0, // 0 off, 1 fps, 2 basic, 3 detailed, 4 full + val statsLevel: Int = 0, val statsLayout: String = "horizontal", - val statsCorner: String = "right", // right | left - val volume: Int = 100, // 100..300 % - val rumble: Int = 100, // 0..100 % + val statsCorner: String = "right", + val volume: Int = 100, + val rumble: Int = 100, val maxFps: Int = 0, val brightness: Int = 0, val contrast: Int = 0, val gamma: Float = 1f, val saturation: Int = 100, val sharpen: Int = 0, - val sharpenMode: String = "cas", // cas | usm + val sharpenMode: String = "cas", val deband: Int = 0, val fxaa: Boolean = false, val toon: Boolean = false, val crt: Boolean = false, val ntsc: Boolean = false, - val playerEngine: String = "web", // web | native (WebRTC library + DroidDeck's compositor, frame generation) - val frameGen: String = "ddfg", // native engine: off | ddfg (2x) | lsfg | lsfg3 | lsfg4 (needs Lossless.dll) - val fpsLimit: Int = 0, // native engine target: 0 = the screen's rate, else 30/45/60/120/144/165 - val fpsCustomOn: Boolean = false, // use fpsCustom instead of the presets - val fpsCustom: Int = 60, // 10..165 - val instantInput: Boolean = true, // native: send controller changes at once (else every 16 ms) - val smallBuffer: Boolean = false, // native: WebRTC shows frames as soon as decoded (no playout delay) - val fastDecoder: Boolean = true, // native: the hardware decoder's low-latency mode - val zeroCopy: Boolean = false, // native, frame generation off: decode straight onto the screen + val playerEngine: String = "web", + val frameGen: String = "ddfg", + val fpsLimit: Int = 0, + val fpsCustomOn: Boolean = false, + val fpsCustom: Int = 60, + val instantInput: Boolean = true, + val smallBuffer: Boolean = false, + val fastDecoder: Boolean = true, + val zeroCopy: Boolean = false, ) { - /** The native engine's frame-rate target (0 = the screen's own rate). */ val fpsTarget get() = if (fpsCustomOn) fpsCustom else fpsLimit - /** The service picks the stream resolution from the reported OS (Better xCloud's mapping). */ val osName get() = when (resolution) { "720p" -> "android"; "1080p_hq" -> "tizen"; else -> "windows" } fun toJson(): String = JSONObject() diff --git a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudSettingsPanel.kt b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudSettingsPanel.kt index 1131fb124..b4f9ab9cb 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudSettingsPanel.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudSettingsPanel.kt @@ -52,18 +52,14 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -/** Shared by the Xbox cloud settings page and the in-player menu; [live] changes reach the running stream. */ @Composable fun XboxCloudSettingsPanel( settings: XboxCloudSettings, regions: List, live: Boolean, - /** "stream", "picture" or "overlay" shows that section only; null shows all. */ section: String? = null, - /** The row to select when the panel opens (the last one used), or null for no initial selection. */ restoreRow: Int? = null, onRowFocused: (Int) -> Unit = {}, - /** The settings the running stream started with; rows that differ say they need a restart (null: no stream). */ startedWith: XboxCloudSettings? = null, onChange: (XboxCloudSettings) -> Unit, ) { @@ -78,7 +74,6 @@ fun XboxCloudSettingsPanel( } } -/** Gives each settings row its place in the page, so the last-used row can be selected again. */ private class PanelFocus { var next = 0 var count = 0 @@ -99,7 +94,6 @@ private fun PanelContent( startedWith: XboxCloudSettings?, onChange: (XboxCloudSettings) -> Unit, ) { - // Stream-start settings changed during a stream wait for a restart; back at the starting value, no note. fun pending(differs: (XboxCloudSettings) -> Boolean) = startedWith != null && differs(startedWith) if (section == null || section == "stream") Section("Stream", if (live) "Applies to the next stream." else "Applies when a stream starts.") { @@ -116,7 +110,6 @@ private fun PanelContent( "apply to the web engine.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 8.dp, bottom = 6.dp)) } - // The service picks the size from the reported device profile (the PC profile can send 1440p). Choice("Resolution", listOf("720p" to "720p (phone)", "1080p" to "Best (PC)", "1080p_hq" to "1080p HQ (TV)"), settings.resolution, pending { it.resolution != settings.resolution }) { onChange(settings.copy(resolution = it)) } Choice("Server region", listOf("" to "Automatic") + regions.map { it to it }, settings.region, @@ -176,7 +169,6 @@ private fun PanelContent( settings.zeroCopy) { onChange(settings.copy(zeroCopy = it)) } } - // "overlay-native": the native player's page (its sound has no volume boost). if (section == null || section == "overlay" || section == "overlay-native") Section("Overlay and sound", null) { Choice("Performance overlay", listOf(0 to "Off", 1 to "FPS only", 2 to "Basic", 3 to "Detailed", 4 to "Full"), settings.statsLevel) { onChange(settings.copy(statsLevel = it)) @@ -202,10 +194,6 @@ private fun Section(title: String, note: String?, content: @Composable ColumnSco } } -/** - * One settings row, driven by a controller: it takes focus as a whole (highlighted), Left / Right change its - * value, A / Enter selects; the − / + buttons and switches inside stay for touch only. - */ @Composable private fun SettingRow( onLeft: (() -> Unit)?, @@ -236,7 +224,6 @@ private fun SettingRow( if (action != null) { action(); true } else false } .focusable() - // A tap selects too, without a second focus stop (a clickable would be one). .pointerInput(Unit) { detectTapGestures { currentSelect?.invoke() } } .padding(horizontal = 8.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, @@ -257,7 +244,6 @@ private fun Step(label: String, value: String, onMinus: () -> Unit, onPlus: () - } } -/** A value slider (touch) that a controller moves with Left / Right in steps of 1 (hold to repeat). */ @Composable private fun SliderRow(label: String, value: Int, min: Int, max: Int, onValue: (Int) -> Unit) { SettingRow(onLeft = { onValue((value - 1).coerceAtLeast(min)) }, onRight = { onValue((value + 1).coerceAtMost(max)) }, onSelect = null) { @@ -293,7 +279,6 @@ private fun Toggle(label: String, note: String, checked: Boolean, needsRestart: color = MaterialTheme.colorScheme.tertiary, modifier = Modifier.padding(start = 8.dp, bottom = 4.dp)) } -/** One entry of the cloud players' menu list (highlighted when selected; A or a tap opens it). */ @Composable internal fun XboxMenuEntry(title: String, note: String?, focus: FocusRequester? = null, onFocused: () -> Unit = {}, onClick: () -> Unit) { val cs = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudSignalling.kt b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudSignalling.kt index a5e7587af..8488de6e9 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudSignalling.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudSignalling.kt @@ -3,7 +3,6 @@ package com.droiddeck.launcher.stores.xbox import org.json.JSONArray import org.json.JSONObject -/** Request shapes follow Microsoft's web player as Greenlight (MIT) implements them. */ internal class XboxCloudSignalling( private val session: XboxCloudSession.Session, private val titleId: String, @@ -62,7 +61,6 @@ internal class XboxCloudSignalling( private fun version(min: Int, max: Int) = JSONObject().put("minVersion", min).put("maxVersion", max) - /** The exchange answers 204 until the console has replied. */ private fun poll(path: String): XboxCloudSession.Reply { repeat(60) { val reply = XboxCloudSession.request(session, "GET", path) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudTokenService.kt b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudTokenService.kt index 03df4c19d..1568406ed 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudTokenService.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCloudTokenService.kt @@ -16,10 +16,6 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors import java.util.concurrent.TimeUnit -/** - * Hands the main process's Xbox Cloud Gaming tokens to the native player's process (:xcloudnative) over Binder, so - * they stay in memory (not exported, same app only) and a stream does not wait for a second ~20 s token fetch. - */ class XboxCloudTokenService : Service() { private val worker = Executors.newSingleThreadExecutor() private val messenger = Messenger(Handler(Looper.getMainLooper()) { message -> @@ -42,7 +38,6 @@ class XboxCloudTokenService : Service() { private const val REQUEST = 1 private const val REPLY = 2 - /** Blocks until the main process answers (its tokens, or "error"), or returns null when it cannot be reached. */ fun request(context: Context, account: String, timeoutSeconds: Long): Bundle? { val app = context.applicationContext val done = CountDownLatch(1) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCredentials.kt b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCredentials.kt index f969c20eb..c8b126519 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCredentials.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxCredentials.kt @@ -14,7 +14,6 @@ internal class XboxCredentials(private val app: Context, expectedGeneration: Str val name = "droiddeck-xbox-${UUID.randomUUID()}" private val generation = lifecycle(app, expectedGeneration) { it } private val server = LocalServerSocket(name) - // Some devices refuse the abstract socket from inside PRoot; the backend then falls back to this keyed loopback port. private val loopback = java.net.ServerSocket(0, 8, java.net.InetAddress.getByAddress(byteArrayOf(127, 0, 0, 1))) val port: Int get() = loopback.localPort val key: String = ByteArray(32).also { java.security.SecureRandom().nextBytes(it) }.joinToString("") { "%02x".format(it) } @@ -38,7 +37,6 @@ internal class XboxCredentials(private val app: Context, expectedGeneration: Str return false } - /** Also written to the stores log: sign-in runs in its own process, where only that file reaches a shared log. */ private fun note(text: String) { android.util.Log.w("XboxCredentials", text) com.droiddeck.launcher.stores.StoreLogFiles.stamped(app, "Xbox credentials: $text") @@ -84,7 +82,6 @@ internal class XboxCredentials(private val app: Context, expectedGeneration: Str } } } - /** Each connection gets its own thread; an accept error is logged and retried instead of silently ending the loop. */ private fun listen(route: String, accept: () -> S, serve: (S) -> Unit) = Thread({ var failures = 0 while (!closed) { @@ -101,7 +98,6 @@ internal class XboxCredentials(private val app: Context, expectedGeneration: Str } }, "xbox-credentials-$route").start() - /** Logs requests that take 2 s or more, and the stage a request is still in after 8 s (the backend waits 30 s). */ private fun tracked(route: String, block: (step: (String) -> Unit) -> JSONObject): JSONObject { val started = android.os.SystemClock.elapsedRealtime() val stage = java.util.concurrent.atomic.AtomicReference("auth") @@ -183,7 +179,6 @@ internal class XboxCredentials(private val app: Context, expectedGeneration: Str } } - /** The connecting socket must belong to this app's user; an unreadable table (newer Android) leaves the key as the check. */ private fun loopbackOwner(peerPort: Int): Boolean { val uid = android.os.Process.myUid() val rows = listOf("/proc/net/tcp", "/proc/net/tcp6").mapNotNull { runCatching { java.io.File(it).readLines().drop(1) }.getOrNull() }.flatten() diff --git a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxGamertag.kt b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxGamertag.kt index b065e8baf..c6e62596b 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxGamertag.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxGamertag.kt @@ -4,15 +4,12 @@ import android.content.Context import com.droiddeck.launcher.stores.Store import com.droiddeck.launcher.stores.StoreAccounts import com.droiddeck.launcher.stores.StoresState -/** Keeps the account's Xbox gamertag in its record so the Stores page can show it instead of the sign-in email. */ internal object XboxGamertag { - /** `xodus-cli profile` allows itself 45 s, and starting it through PRoot adds to that. */ private const val TIMEOUT_SECONDS = 75L private val triedGeneration = java.util.concurrent.atomic.AtomicReference(null) internal fun valid(value: String): Boolean = value.isNotEmpty() && value.length <= 64 && value.none { it.isISOControl() } - /** Reads the gamertag with `xodus-cli profile` and stores it; false when Xbox didn't return one. */ fun refresh(app: Context, credentialGeneration: String? = null): Boolean = try { val gamertag = Xodus.run(app, listOf("profile", "--json"), timeout = TIMEOUT_SECONDS, credentialGeneration = credentialGeneration) .optString("gamertag").trim() @@ -29,7 +26,6 @@ internal object XboxGamertag { false } - /** Fetches a missing gamertag once per signed-in account, on its own thread so it never holds up a library sync. */ fun ensure(app: Context) { Thread({ runCatching { diff --git a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxLoginActivity.kt b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxLoginActivity.kt index 39840b260..530eca4e5 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxLoginActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxLoginActivity.kt @@ -55,8 +55,6 @@ class XboxLoginActivity : ComponentActivity() { Thread({ if (closed) return@Thread try { - // A record without a username is left from a sign-in that never finished; starting clean - // also avoids reading it back with an older, slow Keystore key. progress("preparing the sign-in", "Preparing Microsoft sign-in…") val leftover = StoreAccounts.read(applicationContext, Store.XBOX) if (leftover != null && leftover.optString("username").isBlank()) { @@ -73,7 +71,6 @@ class XboxLoginActivity : ComponentActivity() { Xodus.start(applicationContext, listOf("--external-webview-stdio", "login"), credentialGeneration = accountGeneration).use { process -> child = process if (closed) return@use - // StoresState.logLine writes no file in this process, so the reason would miss the shared log. Xodus.diagnostics(process) { line -> logStep(line) line.removePrefix("NEXUS_CREDENTIAL_ERROR_V1 ").takeIf { it != line && it !in codes && codes.size < 6 }?.let(codes::add) @@ -164,7 +161,6 @@ class XboxLoginActivity : ComponentActivity() { private fun progress(name: String, text: String) { step = name; show(text) } - /** Written to the stores log, which the shared log zip carries; the main process's log helpers don't run in this process. */ private fun logStep(line: String) { val text = StoreLog.redactLine("Xbox sign-in: $line") Log.i(TAG, text) @@ -230,7 +226,6 @@ class XboxLoginActivity : ComponentActivity() { companion object { private const val TAG = "XboxLogin" - /** Our own check messages are safe to show; anything else is named by its type, never its text, which may hold sign-in data. */ internal fun failure(e: Exception): String { val message = e.message.orEmpty() return if (e is IllegalStateException && message.isNotBlank() && message != "Check failed." && message.length <= 160) message diff --git a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxNativePad.kt b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxNativePad.kt index 661023d54..e42c2ebaf 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxNativePad.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxNativePad.kt @@ -4,14 +4,8 @@ import android.view.InputDevice import android.view.KeyEvent import android.view.MotionEvent -/** - * Controller state for the native cloud player, from Android key and motion events (no Gamepad API). Button - * bits follow the xCloud input report (Greenlight's packet.ts): Nexus 2, Menu 4, View 8, A 16, B 32, X 64, - * Y 128, D-pad 256..2048, shoulders 4096 / 8192, stick clicks 16384 / 32768. - */ internal class XboxNativePad { data class State(val buttons: Int, val lx: Float, val ly: Float, val rx: Float, val ry: Float, val lt: Float, val rt: Float) { - /** Greenlight's rule: any button change, a stick moved past 0.08 or held past 0.15, a trigger past 0.01. */ fun differsFrom(o: State): Boolean = buttons != o.buttons || listOf(lx to o.lx, ly to o.ly, rx to o.rx, ry to o.ry).any { (a, b) -> Math.abs(a - b) > 0.08f || Math.abs(a) > 0.15f } || Math.abs(lt - o.lt) > 0.01f || Math.abs(rt - o.rt) > 0.01f @@ -22,15 +16,12 @@ internal class XboxNativePad { @Volatile private var lx = 0f; @Volatile private var ly = 0f @Volatile private var rx = 0f; @Volatile private var ry = 0f @Volatile private var lt = 0f; @Volatile private var rt = 0f - /** Called after any change (instant controller input). */ @Volatile var onChanged: (() -> Unit)? = null - /** Buttons pressed on behalf of the app (the menu's Xbox button), OR-ed in. */ @Volatile var overrideButtons = 0 fun snapshot() = State(buttons or hatButtons or overrideButtons, lx, ly, rx, ry, lt, rt) - /** True when the event was a controller button this pad tracks. */ fun onKey(event: KeyEvent): Boolean { val bit = when (event.keyCode) { KeyEvent.KEYCODE_BUTTON_A -> 16 @@ -47,7 +38,6 @@ internal class XboxNativePad { KeyEvent.KEYCODE_BUTTON_THUMBR -> 32768 KeyEvent.KEYCODE_BUTTON_START -> 4 KeyEvent.KEYCODE_BUTTON_SELECT -> 8 - // Digital triggers on pads without analog axes. KeyEvent.KEYCODE_BUTTON_L2 -> { lt = if (event.action == KeyEvent.ACTION_DOWN) 1f else 0f; onChanged?.invoke(); return true } KeyEvent.KEYCODE_BUTTON_R2 -> { rt = if (event.action == KeyEvent.ACTION_DOWN) 1f else 0f; onChanged?.invoke(); return true } else -> return false @@ -57,7 +47,6 @@ internal class XboxNativePad { return true } - /** True when the event came from a joystick (sticks, triggers, hat). */ fun onMotion(event: MotionEvent): Boolean { if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK || event.action != MotionEvent.ACTION_MOVE) return false val device = event.device diff --git a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxNativePlayerActivity.kt b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxNativePlayerActivity.kt index 1333be117..2125bf802 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxNativePlayerActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxNativePlayerActivity.kt @@ -56,13 +56,6 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors import java.util.concurrent.TimeUnit -/** - * Native Xbox Cloud Gaming player (opt-in, Android 10+): Google's WebRTC library instead of the web page, the - * xCloud channels ported from Greenlight (XboxNativeProtocol), controller from Android input events, and the - * picture through DroidDeck's own compositor (XboxNativeVideo) so DDFG or LSFG can generate frames. Runs in - * its own process (:xcloudnative): that compositor instance is never the Linux session's. - * Sign-in and session requests are the web player's (XboxCloudSession, XboxCloudSignalling). - */ class XboxNativePlayerActivity : ComponentActivity() { private lateinit var root: FrameLayout private lateinit var surfaceView: SurfaceView @@ -79,9 +72,7 @@ class XboxNativePlayerActivity : ComponentActivity() { @Volatile private var signalling: XboxCloudSignalling? = null @Volatile private var settings = XboxCloudSettings() @Volatile private var closed = false - /** The stream reached the screen at least once (a later failure is a lost connection, not a failed start). */ @Volatile private var connectedOnce = false - /** The session was ended because the player stayed in the background (Android freezes the app soon after). */ @Volatile private var endedInBackground = false private var backgroundEnd: Runnable? = null private var notice: androidx.compose.ui.platform.ComposeView? = null @@ -93,14 +84,8 @@ class XboxNativePlayerActivity : ComponentActivity() { private var menuPage by androidx.compose.runtime.mutableStateOf("") private var lastMainEntry = 0 private val lastRow = mutableMapOf() - /** The settings this stream started with (stream-start settings changed since then need a restart). */ private var startedWith: XboxCloudSettings? = null - /** - * The picture's route right now: true = zero-copy (decoder straight onto the screen), false = WebRTC's texture - * through the Vulkan compositor. Zero-copy whenever it is chosen and frame generation is off; switched live. - */ @Volatile private var zeroCopy = false - /** The SurfaceView's surface while it exists (UI thread). */ private var screen: android.view.Surface? = null private var screenW = 0 private var screenH = 0 @@ -108,7 +93,6 @@ class XboxNativePlayerActivity : ComponentActivity() { @Volatile private var decoders: org.webrtc.DroidDeckLowLatencyDecoderFactory? = null private var lastZeroCopyFrames = 0L private var lastSubmitted = 0L - /** The release of the Guide press that opened the menu must not close it again. */ private var swallowGuideUp = false override fun onCreate(savedInstanceState: Bundle?) { @@ -138,7 +122,6 @@ class XboxNativePlayerActivity : ComponentActivity() { override fun surfaceCreated(holder: SurfaceHolder) {} override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { screen = holder.surface; screenW = width; screenH = height - // Zero-copy: the decoder owns this surface (back on it after a stand-in); the Vulkan compositor must not connect to it. if (zeroCopy) { decoders?.setZeroCopySurface(holder.surface); return } @Suppress("DEPRECATION") nativeVideo.setSurface(holder.surface, width, height, windowManager.defaultDisplay.refreshRate) @@ -166,12 +149,10 @@ class XboxNativePlayerActivity : ComponentActivity() { webPlayer = true) } - /** One popup at a time, in the app's dialog style; A or Enter picks the selected button, Back closes the player. */ private fun showNotice(title: String, text: String, webPlayer: Boolean) = runOnUiThread { if (notice != null || isFinishing || isDestroyed) return@runOnUiThread closeMenu() show("") - // The stream is over: stale numbers must not stay on screen. overlayOn = false overlay.visibility = android.view.View.GONE val view = androidx.compose.ui.platform.ComposeView(this) @@ -207,13 +188,9 @@ class XboxNativePlayerActivity : ComponentActivity() { root.addView(view, FrameLayout.LayoutParams(-1, -1)) } - // ---- background ---- - override fun onStop() { super.onStop() if (closed || isChangingConfigurations || signalling == null || endedInBackground) return - // Android freezes a background app about a minute after it leaves the screen; the connection then dies - // without a goodbye. End the session cleanly before that and offer a restart on return. log("in the background") val end = Runnable { backgroundEnd = null; endInBackground() } backgroundEnd = end @@ -243,15 +220,12 @@ class XboxNativePlayerActivity : ComponentActivity() { runCatching { pc?.close() } } - // ---- session ---- - private fun run() { val account = com.droiddeck.launcher.stores.StoreAccounts.signedInAs(applicationContext, com.droiddeck.launcher.stores.Store.XBOX) ?: error("Sign in to the Xbox store first.") val requested = intent.getStringExtra("product").orEmpty() val cached = XboxLibraryCache.read(applicationContext, account, Xodus.market())?.games?.firstOrNull { it.id == requested } val product = cached?.extra?.get("cloudId")?.takeIf { it.isNotEmpty() } ?: requested - // The main process's tokens when it has them (no second ~20 s fetch); else this process fetches its own. val shared = runCatching { XboxCloudSession.fromMainProcess(applicationContext, account) } .onFailure { log("tokens from the main process: ${it.message?.take(160)}") }.getOrNull() log(if (shared != null) "tokens from the main process" else "tokens fetched here") @@ -280,7 +254,6 @@ class XboxNativePlayerActivity : ComponentActivity() { if (closed) return show("Connecting…") - // Small network buffer: no playout delay, frames go to the decoder and screen as soon as they are complete. val trials = if (settings.smallBuffer) "WebRTC-ForcePlayoutDelay/min_ms:0,max_ms:0/WebRTC-ZeroPlayoutDelay/min_pacing:0ms,max_decode_queue_size:8/" else "" PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions.builder(applicationContext) .setFieldTrials(trials).createInitializationOptions()) @@ -290,7 +263,6 @@ class XboxNativePlayerActivity : ComponentActivity() { surfaceView.holder.surface.takeIf { it.isValid } } else null if (zeroSurface != null) { - // The compositor may already hold the screen; it must let go before the decoder takes it. val ready = CountDownLatch(1) runOnUiThread { zeroCopy = true; video?.setSurface(null, 0, 0, 60f); ready.countDown() } ready.await(2, TimeUnit.SECONDS) @@ -336,7 +308,6 @@ class XboxNativePlayerActivity : ComponentActivity() { return reply } - /** Opus stereo (Greenlight), H.264 profile order (Main first unless the user picked one) and the bitrate cap. */ private fun tweakOffer(sdp: String): String { var out = sdp.replace("useinbandfec=1", "useinbandfec=1; stereo=1") val prefix = when (settings.codec) { "low" -> "420"; "normal" -> "42e"; else -> "4d" } @@ -360,7 +331,6 @@ class XboxNativePlayerActivity : ComponentActivity() { return out } - /** The server's candidates, plus the IPv4 behind a Teredo address (Greenlight's ice.ts / teredo.ts). */ private fun addRemoteCandidate(peer: PeerConnection, c: JSONObject) { val line = c.optString("candidate").removePrefix("a=") if (line.isEmpty() || line == "end-of-candidates") return @@ -410,11 +380,9 @@ class XboxNativePlayerActivity : ComponentActivity() { private fun attach(receiver: RtpReceiver) = synchronized(this) { val track = receiver.track() - // Always attached: in zero-copy WebRTC delivers no frames, and after a live switch back they flow again. if (track is VideoTrack && !videoAttached) { videoAttached = true; video?.let { track.addSink(it) }; log("video track") } } - /** LSFG needs the Lossless.dll and its shader cache built; otherwise DDFG. */ private fun effectiveFrameGen(): String { if (!XboxNativeVideo.isLsfg(settings.frameGen)) return settings.frameGen val status = com.droiddeck.launcher.gpu.Lossless.sync(applicationContext) @@ -439,8 +407,6 @@ class XboxNativePlayerActivity : ComponentActivity() { override fun onSetFailure(error: String) = done(Result.failure(IllegalStateException(error))) } - // ---- input, rumble, overlay ---- - private var guideDown = false private var guideDownAt = 0L private var lastGuideUp = 0L @@ -454,7 +420,6 @@ class XboxNativePlayerActivity : ComponentActivity() { override fun dispatchKeyEvent(event: KeyEvent): Boolean { if (menu != null) { - // Guide closes the menu; Back goes up one page, then closes it. if (isGuide(event)) { if (event.action == KeyEvent.ACTION_UP) { if (swallowGuideUp) swallowGuideUp = false else closeMenu() }; return true } if (event.keyCode == KeyEvent.KEYCODE_BACK) { if (event.action == KeyEvent.ACTION_UP) { if (menuPage.isNotEmpty()) menuPage = "" else closeMenu() } @@ -462,7 +427,6 @@ class XboxNativePlayerActivity : ComponentActivity() { } return super.dispatchKeyEvent(event) } - // Guide / the Input Controls menu button: once = the Xbox button, twice or held = the player menu. if (isGuide(event)) { if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) { val pending = pendingNexus @@ -482,7 +446,6 @@ class XboxNativePlayerActivity : ComponentActivity() { return super.dispatchKeyEvent(event) } - // With the menu open, sticks and the hat go to Android unhandled, which turns them into D-pad keys for the menu. override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean = (menu == null && pad.onMotion(event)) || super.dispatchGenericMotionEvent(event) @@ -495,7 +458,6 @@ class XboxNativePlayerActivity : ComponentActivity() { runCatching { vibrator.vibrate(VibrationEffect.createOneShot(durationMs.coerceAtMost(1000).toLong(), amplitude)) } } - /** The overlay is remembered with the other cloud settings (on = overlay level above 0). */ @Volatile private var overlayOn = false private var lastJitterDelay = 0.0 @@ -507,12 +469,6 @@ class XboxNativePlayerActivity : ComponentActivity() { private var lastLost = 0.0 private var lastReceived = 0.0 - /** - * The overlay, at the web player's levels and layout (settings statsLevel / statsLayout): 1 fps and frame - * generation base -> shown; 2 + resolution, ping; 3 + bitrate, jitter buffer, decode time, packet loss; 4 + frame - * generation's added delay, the estimated picture delay (one-way network + buffer + decode + frame generation + - * one screen refresh), codec and server. - */ private fun updateOverlay() { val level = settings.statsLevel if (!overlayOn || level <= 0) return @@ -549,7 +505,6 @@ class XboxNativePlayerActivity : ComponentActivity() { (m["currentRoundTripTime"] as? Number)?.toDouble()?.let { rttMs = it * 1000 } } } - // Zero-copy: WebRTC never sees a decoded frame, so size and decode time come from the decoder itself. if (zeroCopy) decoders?.let { d -> d.videoSize()?.let { size = it } val ms = d.takeDecodeMs() @@ -561,8 +516,6 @@ class XboxNativePlayerActivity : ComponentActivity() { val fgGpuMs = if (fgOn && fg!![5] > 0f) fg[5].toDouble() else 0.0 @Suppress("DEPRECATION") val refreshMs = 1000.0 / windowManager.defaultDisplay.refreshRate val total = (if (rttMs >= 0) rttMs / 2 else 0.0) + bufferMs + decodeMs + fgWaitMs + fgGpuMs + refreshMs - // What reaches the screen: the decoder's own count in zero-copy, the presented rate with frame generation, - // else the frames handed to the compositor (a frame-rate target drops some of WebRTC's decoded frames). val submitted = video?.frames ?: 0L val handed = (submitted - lastSubmitted).toInt().also { lastSubmitted = submitted } val shownFps = when { @@ -591,9 +544,6 @@ class XboxNativePlayerActivity : ComponentActivity() { } } - // ---- menu ---- - - /** The player menu (as the web player's): Resume, Xbox menu, Frame generation, Stream quality, overlay, Leave. */ private fun openMenu() { if (menu != null) return pad.reset() @@ -607,7 +557,6 @@ class XboxNativePlayerActivity : ComponentActivity() { val entries = androidx.compose.runtime.remember { List(7) { androidx.compose.ui.focus.FocusRequester() } } val choices = androidx.compose.runtime.remember { List(5) { androidx.compose.ui.focus.FocusRequester() } } androidx.compose.runtime.LaunchedEffect(menuPage) { - // Wait a frame so the entries are attached before one of them takes the selection. androidx.compose.runtime.withFrameNanos { } runCatching { if (menuPage.isEmpty()) entries[lastMainEntry.coerceIn(0, 6)].requestFocus() @@ -615,8 +564,6 @@ class XboxNativePlayerActivity : ComponentActivity() { } } val cs = androidx.compose.material3.MaterialTheme.colorScheme - // A full-size box first: the root of a ComposeView gets fixed full-screen constraints, so the panel's - // width only holds inside it (as the web player's menu does). androidx.compose.foundation.layout.Box(androidx.compose.ui.Modifier.fillMaxSize()) { androidx.compose.foundation.layout.Column( androidx.compose.ui.Modifier.fillMaxHeight().width(400.dp).background(cs.surface.copy(alpha = 0.95f)) @@ -695,11 +642,6 @@ class XboxNativePlayerActivity : ComponentActivity() { view.requestFocus() } - /** - * Applies a settings change from any menu page at once: frame generation, the frame-rate target, zero-copy, instant - * input and the overlay. Resolution, region, language, codec, bitrate, the network buffer, the decoder mode and the - * engine belong to the cloud session and need a restart (the page says so). - */ private fun applyLive(before: XboxCloudSettings, changed: XboxCloudSettings) { settings = changed XboxCloudSettings.save(this, changed) @@ -709,10 +651,6 @@ class XboxNativePlayerActivity : ComponentActivity() { applyOverlay() } - /** - * Zero-copy draws every decoded frame straight onto the screen, so it is used only when nothing has to touch the - * frames: frame generation off and no frame-rate target below the stream's 60 fps (the compositor drops frames for that). - */ private fun zeroCopyWanted(s: XboxCloudSettings) = s.zeroCopy && s.frameGen == XboxNativeVideo.FG_OFF && (s.fpsTarget == 0 || s.fpsTarget >= 60) @@ -720,7 +658,6 @@ class XboxNativePlayerActivity : ComponentActivity() { overlayOn = settings.statsLevel > 0 overlay.visibility = if (overlayOn) android.view.View.VISIBLE else android.view.View.GONE overlay.textSize = 12f - // Top left keeps clear of the clock Xbox draws top right while its guide is open. val left = settings.statsCorner == "left" overlay.layoutParams = (overlay.layoutParams as FrameLayout.LayoutParams).apply { gravity = Gravity.TOP or (if (left) Gravity.START else Gravity.END) @@ -729,7 +666,6 @@ class XboxNativePlayerActivity : ComponentActivity() { overlay.gravity = if (left) Gravity.START else Gravity.END } - /** Stream-start settings need a new cloud session: end this one, then open the player again. */ private fun restartStream() { val signals = signalling closed = true @@ -756,10 +692,6 @@ class XboxNativePlayerActivity : ComponentActivity() { worker.execute { video?.setFrameGeneration(effectiveFrameGen()) } } - /** - * Live route switch (UI thread): zero-copy when chosen and frame generation is off, else the compositor. - * The screen surface takes one producer at a time, so the old one lets go before the new one connects. - */ private fun applyRoute() { val d = decoders ?: return val want = zeroCopyWanted(settings) @@ -783,10 +715,6 @@ class XboxNativePlayerActivity : ComponentActivity() { video?.setSurface(target, screenW, screenH, windowManager.defaultDisplay.refreshRate) } - /** - * Ask Android for the display mode whose refresh rate fits the target (at least the target when possible, - * else the fastest; the fastest for "the screen's rate"), so generated frames are not held back at 60 Hz. - */ private fun applyDisplayRate(target: Int) { @Suppress("DEPRECATION") val display = windowManager.defaultDisplay val current = display.mode @@ -799,10 +727,6 @@ class XboxNativePlayerActivity : ComponentActivity() { private val rehideBars = Runnable { if (window.decorView.hasWindowFocus()) hideBars() } - /** - * Bars that come back while the player has focus (return from recents or another app, a swipe) are hidden - * again after a moment; focus alone missed the return from the background (10-24). - */ private fun watchBars() { androidx.core.view.ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { v, insets -> if (insets.isVisible(WindowInsetsCompat.Type.navigationBars()) || insets.isVisible(WindowInsetsCompat.Type.statusBars())) { @@ -844,11 +768,9 @@ class XboxNativePlayerActivity : ComponentActivity() { } companion object { - /** How long a stream may stay in the background (Android froze the app ~70 s after it left the screen, 10-24). */ private const val BACKGROUND_GRACE_MS = 45_000L private val FG_MODES = listOf(XboxNativeVideo.FG_OFF, XboxNativeVideo.FG_DDFG, XboxNativeVideo.FG_LSFG, XboxNativeVideo.FG_LSFG3, XboxNativeVideo.FG_LSFG4) - /** True when the native engine can run here (ImageReader GPU buffers need Android 10). */ fun supported(): Boolean = Build.VERSION.SDK_INT >= 29 } } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxNativeProtocol.kt b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxNativeProtocol.kt index 348079f52..cb44b43fe 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxNativeProtocol.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxNativeProtocol.kt @@ -1,3 +1,4 @@ +// The xCloud data-channel protocol is ported from Greenlight by UnknownSKL (Jim Kroon), MIT; see tools/xcloud-player/NOTICE.md. package com.droiddeck.launcher.stores.xbox import org.json.JSONObject @@ -11,12 +12,6 @@ import java.util.concurrent.Executors import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit -/** - * The xCloud data-channel protocol for the native player, ported from Greenlight's player (UnknownSKL, MIT; - * tools/xcloud-player/greenlight/lib/channel): chat / control / input / message channels, the message - * handshake, authorization and gamepad announcements, and the binary input reports. The controller state - * comes from Android input events ([pad]); rumble reports come back through [onRumble]. - */ internal class XboxNativeProtocol( pc: PeerConnection, private val pad: XboxNativePad, @@ -81,12 +76,10 @@ internal class XboxNativeProtocol( "HandshakeAck" -> { handshakeDone = true log("handshake done") - // Greenlight's order: authorization (announces controller 0 and removes it), input start, config. send(control, JSONObject().put("message", "authorizationRequest").put("accessKey", "4BDB3609-C1F1-4195-9B37-FEFF45DA8B8E").toString()) gamepadChanged(0, true); gamepadChanged(0, false) startInput() sendConfig() - // Our controller, after the authorization's add/remove. gamepadChanged(0, true) send(control, JSONObject().put("message", "videoKeyframeRequested").put("ifrRequested", true).toString()) } @@ -101,7 +94,6 @@ internal class XboxNativeProtocol( complete(data.optString("id"), "") onServerDisconnect() } - // The game's own dialogs: accept (Greenlight shows a confirm; the native player has no page). "/streaming/systemUi/messages/ShowMessageDialog" -> complete(data.optString("id"), JSONObject().put("Result", 0)) else -> {} } @@ -134,18 +126,14 @@ internal class XboxNativeProtocol( } private fun startInput() { - // Client metadata: max touch points. send(input, header(8, 15).apply { put(1.toByte()); flip() }) loop = timer.scheduleWithFixedDelay({ runCatching { tick() } }, 16, 16, TimeUnit.MILLISECONDS) } - /** Instant controller input: send a report now if the state changed (the 16 ms loop stays as the heartbeat). */ fun inputChanged() { - // At most one extra report every 4 ms (stick motion arrives far faster than that). if (handshakeDone && loop != null && System.nanoTime() - lastSentAt > 4_000_000L) timer.execute { runCatching { tick() } } } - /** Greenlight's queue: a gamepad frame when the state changed (sticks past a small threshold) or every 33 ms. */ @Synchronized private fun tick() { val state = pad.snapshot() @@ -154,13 +142,13 @@ internal class XboxNativeProtocol( if (!changed && now - lastSentAt < 33_000_000L) return lastSent = state; lastSentAt = now val packet = header(2, 14 + 1 + 23).apply { - put(1.toByte()) // one frame - put(0.toByte()) // gamepad index + put(1.toByte()) + put(0.toByte()) putShort(state.buttons.toShort()) putShort(axis(state.lx)); putShort(axis(-state.ly)); putShort(axis(state.rx)); putShort(axis(-state.ry)) putShort(trigger(state.lt)); putShort(trigger(state.rt)) - putInt(1) // physical physicality - order(ByteOrder.BIG_ENDIAN); putInt(1); order(ByteOrder.LITTLE_ENDIAN) // virtual physicality (Greenlight writes it big-endian) + putInt(1) + order(ByteOrder.BIG_ENDIAN); putInt(1); order(ByteOrder.LITTLE_ENDIAN) flip() } send(input, packet) @@ -171,7 +159,7 @@ internal class XboxNativeProtocol( private fun onInputData(buffer: DataChannel.Buffer) { val data = buffer.data.order(ByteOrder.LITTLE_ENDIAN) - if (data.remaining() < 13 || (data.get(0).toInt() and 0xFF) != 128) return // vibration report + if (data.remaining() < 13 || (data.get(0).toInt() and 0xFF) != 128) return val strong = (data.get(4).toInt() and 0xFF) / 100f val weak = (data.get(5).toInt() and 0xFF) / 100f val duration = data.getShort(8).toInt() and 0xFFFF diff --git a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxNativeVideo.kt b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxNativeVideo.kt index dc3657dbe..193f4e58b 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxNativeVideo.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/xbox/XboxNativeVideo.kt @@ -20,13 +20,6 @@ import org.webrtc.VideoSink import java.io.File import java.util.ArrayDeque -/** - * The native player's picture: WebRTC's hardware-decoded frames are drawn (GL, WebRTC's EglRenderer) into an - * ImageReader whose images are GPU-shareable HardwareBuffers, and each one goes to DroidDeck's own compositor in - * stream mode (WaylandCompositor.nativeStreamFrame) as the whole scene, where DroidDeck's frame generation (DDFG, - * or LSFG with the Lossless.dll) multiplies it. The player runs in its own process (:xcloudnative), so this - * compositor is never the one a Linux session uses. Android 10+ (ImageReader usage flags). - */ internal class XboxNativeVideo(private val context: Context, eglBase: EglBase, private val log: (String) -> Unit) : VideoSink { companion object { const val FG_OFF = "off" @@ -40,7 +33,6 @@ internal class XboxNativeVideo(private val context: Context, eglBase: EglBase, p fun modeName(mode: String) = when (mode) { FG_DDFG -> "DDFG 2×"; FG_LSFG -> "LSFG 2×"; FG_LSFG3 -> "LSFG 3×"; FG_LSFG4 -> "LSFG 4×"; else -> "Off" } - /** LSFG can run: the Lossless.dll was imported (or found in Steam) and its shader cache built. */ fun lsfgReady(context: Context) = Lossless.state(context).ready private const val SCALE_FIT = 1 private const val ALIGN_CENTER = 0 @@ -53,19 +45,16 @@ internal class XboxNativeVideo(private val context: Context, eglBase: EglBase, p private var reader: ImageReader? = null private var readerW = 0 private var readerH = 0 - /** Images the compositor may still read: the oldest goes back to the reader once two newer ones arrived. */ private val held = ArrayDeque() private var surface: Surface? = null private var attached = false @Volatile private var frameGen = FG_OFF @Volatile private var refreshHz = 60f - /** The frame-rate target (0 = the screen's rate): the frame-gen pacer's cap, or a limit on stream frames without it. */ @Volatile private var fpsTarget = 0 private var nextDueNs = 0L @Volatile var frames = 0L private set - /** The screen: the compositor presents to it (null detaches, e.g. for zero-copy or when the surface goes). */ fun setSurface(target: Surface?, width: Int, height: Int, displayRefreshHz: Float) = synchronized(lock) { refreshHz = displayRefreshHz if (target == null) { @@ -75,7 +64,6 @@ internal class XboxNativeVideo(private val context: Context, eglBase: EglBase, p } surface = target val runtime = File(context.cacheDir, "xcloud-wayland").apply { mkdirs() } - // System Vulkan driver; this process's compositor shows only the stream. val started = CompositorHost.startOrAttach(context, target, runtime.path, null, null, context.applicationInfo.nativeLibraryDir, width, height, displayRefreshHz, 0) if (started) { @@ -89,10 +77,6 @@ internal class XboxNativeVideo(private val context: Context, eglBase: EglBase, p applyFrameGen() } - /** - * Gives the screen surface up and waits until the compositor has really disconnected from it: the window request - * is applied on the compositor thread at its next tick, and the decoder (zero-copy) can only connect after that. - */ private fun letGo() { WaylandCompositor.nativeSetFrameGenArmed(false, 0, 0) CompositorHost.detach(surface) @@ -100,13 +84,11 @@ internal class XboxNativeVideo(private val context: Context, eglBase: EglBase, p if (!WaylandCompositor.nativeWaitSurface(1000)) log("compositor did not let go of the screen within 1 s") } - /** Frame-rate target, 0 = the screen's rate. Live. */ fun setFpsTarget(fps: Int) { fpsTarget = fps synchronized(lock) { if (attached) applyFrameGen() } } - /** off, ddfg (2x) or lsfg / lsfg3 / lsfg4 (needs the Lossless.dll). Live. */ fun setFrameGeneration(mode: String) { frameGen = mode synchronized(lock) { if (attached) applyFrameGen() } @@ -137,8 +119,6 @@ internal class XboxNativeVideo(private val context: Context, eglBase: EglBase, p synchronized(lock) { if (r !== reader) { image.close(); return } val buffer = image.hardwareBuffer - // Without frame generation a target below the stream's rate drops stream frames evenly: each frame has a - // due time one interval after the last one's, so 30 of 60 frames (or 45: three of four) reach the screen. val now = System.nanoTime() val target = fpsTarget val skip = frameGen == FG_OFF && target in 1 until 60 && run { @@ -154,7 +134,6 @@ internal class XboxNativeVideo(private val context: Context, eglBase: EglBase, p } private fun applyFrameGen() { - // The pacer caps generated output at this rate: the user's target, else the screen's. val cap = if (fpsTarget > 0) fpsTarget.toFloat() else refreshHz val mode = when { frameGen == FG_DDFG -> FrameGen.Mode(FrameGen.ENGINE_DDFG, 2) @@ -165,7 +144,6 @@ internal class XboxNativeVideo(private val context: Context, eglBase: EglBase, p log("native frame generation: $frameGen, target ${if (fpsTarget > 0) fpsTarget else "screen"} fps" + (problem?.let { " ($it)" } ?: "")) } - /** {planned, planned, sourceFps, presentedFps, thermal, -1}, or null while nothing is shown. */ fun stats(): FloatArray? = synchronized(lock) { if (attached) WaylandCompositor.nativeFrameGenStats() else null } fun frameGenProblem(): Int = synchronized(lock) { if (attached) WaylandCompositor.nativeFrameGenProblem() else -1 } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/xbox/Xodus.kt b/app/src/main/java/com/droiddeck/launcher/stores/xbox/Xodus.kt index 5d6160c44..f803d0641 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/xbox/Xodus.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/xbox/Xodus.kt @@ -154,7 +154,6 @@ internal object Xodus { const val SIGN_IN_REJECTED = "Microsoft content licensing rejected sign-in \\(HTTP 401\\)\\. Sign in again in Settings\\." - /** Passes the backend's known stderr diagnostics to [report]; other stderr text never leaves the process. */ fun diagnostics(child: Child, report: (String) -> Unit = { com.droiddeck.launcher.stores.StoresState.logLine("Xbox: $it") }): Thread = Thread({ runCatching { child.process.errorStream.bufferedReader().useLines { lines -> lines.forEach { line -> diff --git a/app/src/main/java/com/droiddeck/launcher/sync/SyncPrefs.kt b/app/src/main/java/com/droiddeck/launcher/sync/SyncPrefs.kt index 9f4937385..a0b1a63ce 100644 --- a/app/src/main/java/com/droiddeck/launcher/sync/SyncPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/sync/SyncPrefs.kt @@ -25,7 +25,6 @@ object SyncPrefs { fun userPaused(context: Context): Boolean = prefs(context).getBoolean("paused", false) fun setUserPaused(context: Context, on: Boolean) { prefs(context).edit().putBoolean("paused", on).apply() } - /** Whether DroidDeck has paused all devices through the API and must resume them. */ fun pauseApplied(context: Context): Boolean = prefs(context).getBoolean("pause-applied", false) fun setPauseApplied(context: Context, on: Boolean) { prefs(context).edit().putBoolean("pause-applied", on).apply() } diff --git a/app/src/main/java/com/droiddeck/launcher/sync/Syncthing.kt b/app/src/main/java/com/droiddeck/launcher/sync/Syncthing.kt index b1510c038..c7879492f 100644 --- a/app/src/main/java/com/droiddeck/launcher/sync/Syncthing.kt +++ b/app/src/main/java/com/droiddeck/launcher/sync/Syncthing.kt @@ -48,10 +48,8 @@ object Syncthing { var apiKeyTail by mutableStateOf(null); private set var userPaused by mutableStateOf(false); private set var autoPause by mutableStateOf(null); private set - /** Whether Syncthing has confirmed every device is paused, as opposed to a pause only being requested. */ var devicesPaused by mutableStateOf(false); private set val pauseRequested get() = userPaused || autoPause != null - /** Bumped to ask the Setup page to show the Sync tab. */ var openRequests by mutableStateOf(0) private set var openPending = false @@ -63,7 +61,6 @@ object Syncthing { val needsAttention get() = phase == Phase.FAILED || snapshot?.needsAttention == true - /** Called on the main thread whenever the state above changes; the background service follows it. */ @Volatile var onChange: (() -> Unit)? = null private val main = Handler(Looper.getMainLooper()) @@ -87,7 +84,6 @@ object Syncthing { private fun guiSocket(context: Context) = File(home(context), "gui.sock") private fun binary(context: Context) = File(context.applicationInfo.nativeLibraryDir, "libsyncthingnative.so") - /** DroidDeck's own storage: its private data directory and its folder on each shared volume. */ fun storageRoots(context: Context): List = listOf(context.dataDir) + context.getExternalFilesDirs(null).filterNotNull().mapNotNull { it.parentFile } @@ -138,7 +134,6 @@ object Syncthing { worker.execute { api?.let { applyPause(it) } } } - /** Starts or stops Syncthing to match the settings and whether DroidDeck is on screen. */ fun evaluate() { if (Looper.myLooper() != Looper.getMainLooper()) { main.post { evaluate() }; return } val enabled = SyncPrefs.enabled(app) @@ -200,7 +195,6 @@ object Syncthing { } } - /** Adds a folder at [path] shared with [devices]; pass the offered folder's [id] to accept a share. */ fun addFolder(context: Context, path: File, label: String, devices: List, id: String = newFolderId(), onDone: (Boolean) -> Unit) { worker.execute { val ok = runCatching { @@ -228,14 +222,12 @@ object Syncthing { worker.execute { api?.let { a -> runCatching { a.removeFolder(id) }.onFailure { Log.w(TAG, "remove $id failed", it) } } } } - /** Starts a loopback proxy for the in-app web UI, which can't reach Syncthing's private socket itself. */ fun webUiProxy(context: Context): SyncthingProxy? { if (phase != Phase.RUNNING) return null val token = Base64.encodeToString("${SyncthingConfig.GUI_USER}:${SyncPrefs.guiPassword(context)}".toByteArray(), Base64.NO_WRAP) return runCatching { SyncthingProxy(guiSocket(context), "Basic $token") }.onFailure { Log.w(TAG, "web UI proxy failed", it) }.getOrNull() } - /** UI screens showing live status call this so the status refreshes faster while they're visible. */ fun watch(): () -> Unit { watchers.incrementAndGet() return { watchers.decrementAndGet() } @@ -456,7 +448,6 @@ object Syncthing { java.time.OffsetDateTime.parse(s).toInstant().toEpochMilli().takeIf { it > 86_400_000L } }.getOrNull() - /** Pauses any folder outside DroidDeck's storage as soon as Syncthing saves its config, and stops Syncthing if it can't. */ private fun enforce(gen: Int, guard: SyncthingGuard) { var since = 0L var pending = true @@ -509,7 +500,6 @@ object Syncthing { } } - // Runs from the worker, poll and enforce threads; serialised so an older decision can't land after a newer one. private fun applyPause(client: SyncthingApi) = synchronized(pauseLock) { val context = app val auto = autoPauseReason() @@ -524,7 +514,6 @@ object Syncthing { if (want && !held && pauseFailures.get() >= ENFORCE_ATTEMPTS) failClosed(generation, IllegalStateException(FAILURE_PAUSE), FAILURE_PAUSE) } - // An unreadable peer list counts as unpaused, so a required pause is reasserted rather than assumed. private fun hasUnpausedPeer(client: SyncthingApi): Boolean = runCatching { val myId = client.myId() val devices = client.devices() diff --git a/app/src/main/java/com/droiddeck/launcher/sync/SyncthingApi.kt b/app/src/main/java/com/droiddeck/launcher/sync/SyncthingApi.kt index 9498bf4fb..7a7eaab24 100644 --- a/app/src/main/java/com/droiddeck/launcher/sync/SyncthingApi.kt +++ b/app/src/main/java/com/droiddeck/launcher/sync/SyncthingApi.kt @@ -29,7 +29,6 @@ class SyncthingApi(val socket: File, private val apiKey: String) { private fun merge(path: String, change: JSONObject.() -> Unit) = send("PUT", path, get(path).apply(change)) - /** Long-polls for config saves; returns the last event id seen, or [since] on timeout. */ fun waitForConfigSaved(since: Long, timeoutSeconds: Int): Pair { val events = JSONArray(request("GET", "/rest/events?events=ConfigSaved&since=$since&timeout=$timeoutSeconds", null, (timeoutSeconds + 10) * 1000)) if (events.length() == 0) return since to false diff --git a/app/src/main/java/com/droiddeck/launcher/sync/SyncthingConfig.kt b/app/src/main/java/com/droiddeck/launcher/sync/SyncthingConfig.kt index 15838ae48..b367d5908 100644 --- a/app/src/main/java/com/droiddeck/launcher/sync/SyncthingConfig.kt +++ b/app/src/main/java/com/droiddeck/launcher/sync/SyncthingConfig.kt @@ -37,7 +37,6 @@ object SyncthingConfig { val root = doc.documentElement val gui = root.child("gui") ?: doc.createElement("gui").also { root.appendChild(it) } gui.setAttribute("tls", "false") - // An absolute path makes Syncthing listen on a Unix socket, which other apps can't reach in our private storage. gui.set("address", guiSocket) gui.set("unixSocketPermissions", "600") val options = root.child("options") ?: doc.createElement("options").also { root.appendChild(it) } diff --git a/app/src/main/java/com/droiddeck/launcher/sync/SyncthingGuard.kt b/app/src/main/java/com/droiddeck/launcher/sync/SyncthingGuard.kt index 30d00a66b..ae27645f6 100644 --- a/app/src/main/java/com/droiddeck/launcher/sync/SyncthingGuard.kt +++ b/app/src/main/java/com/droiddeck/launcher/sync/SyncthingGuard.kt @@ -2,7 +2,6 @@ package com.droiddeck.launcher.sync import java.io.File -/** Decides which Syncthing folder paths stay inside DroidDeck's own storage. */ class SyncthingGuard(private val home: File, roots: List, forbidden: List) { private val roots = roots.map { it.canonicalFile } private val forbidden = forbidden.map { it.canonicalFile } @@ -20,7 +19,6 @@ class SyncthingGuard(private val home: File, roots: List, forbidden: List< return roots.any { within(target, it) } && forbidden.none { within(target, it) || within(it, target) } } - /** Whether [dir] can be opened while looking for a folder: inside DroidDeck's storage and outside Syncthing's own files. */ fun browsable(dir: File): Boolean { val target = runCatching { dir.canonicalFile }.getOrNull() ?: return false return roots.any { within(target, it) } && forbidden.none { within(target, it) } diff --git a/app/src/main/java/com/droiddeck/launcher/sync/SyncthingHttp.kt b/app/src/main/java/com/droiddeck/launcher/sync/SyncthingHttp.kt index 4cee6c672..b2ebcb3c9 100644 --- a/app/src/main/java/com/droiddeck/launcher/sync/SyncthingHttp.kt +++ b/app/src/main/java/com/droiddeck/launcher/sync/SyncthingHttp.kt @@ -5,7 +5,6 @@ import java.io.IOException import java.io.InputStream import java.io.OutputStream -/** Just enough HTTP/1.1 to talk to Syncthing over its Unix socket, one request per connection. */ internal object SyncthingHttp { private const val LINE_LIMIT = 16 shl 10 private const val HEADER_LIMIT = 100 @@ -32,7 +31,6 @@ internal object SyncthingHttp { return code to String(body, Charsets.UTF_8) } - /** Reads one CRLF-terminated line, or null at end of stream. */ fun readLine(input: InputStream): String? { val line = ByteArrayOutputStream() while (true) { diff --git a/app/src/main/java/com/droiddeck/launcher/sync/SyncthingPorts.kt b/app/src/main/java/com/droiddeck/launcher/sync/SyncthingPorts.kt index c8bf4cc7b..671a98d0b 100644 --- a/app/src/main/java/com/droiddeck/launcher/sync/SyncthingPorts.kt +++ b/app/src/main/java/com/droiddeck/launcher/sync/SyncthingPorts.kt @@ -7,7 +7,6 @@ import java.net.ServerSocket data class SyncthingPorts(val sync: Int, val discovery: Int) object SyncthingPortPicker { - // Syncthing's defaults, plus the alternates Syncthing-Fork's debug build uses. internal val RESERVED = setOf(8384, 8385, 22000, 22001, 21027, 21028) internal const val SYNC = 22100 internal const val DISCOVERY = 21127 diff --git a/app/src/main/java/com/droiddeck/launcher/sync/SyncthingProxy.kt b/app/src/main/java/com/droiddeck/launcher/sync/SyncthingProxy.kt index 58779eb42..e4487f1e9 100644 --- a/app/src/main/java/com/droiddeck/launcher/sync/SyncthingProxy.kt +++ b/app/src/main/java/com/droiddeck/launcher/sync/SyncthingProxy.kt @@ -15,12 +15,7 @@ import java.util.Collections import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Semaphore -/** - * Bridges the in-app WebView to Syncthing's Unix socket while the web UI is open. Any app can connect to a - * loopback port, so requests without this session's cookie are refused before they reach Syncthing. - */ class SyncthingProxy(private val socket: File, private val auth: String) : Closeable { - // Android's getLoopbackAddress() is ::1, but the WebView is pointed at 127.0.0.1. private val server = ServerSocket(0, 16, InetAddress.getByAddress(byteArrayOf(127, 0, 0, 1))) private val token = SyncPrefs.randomToken() val url = "http://127.0.0.1:${server.localPort}/" @@ -82,7 +77,6 @@ class SyncthingProxy(private val socket: File, private val auth: String) : Close val kept = if (name.equals("Cookie", ignoreCase = true)) withoutToken(value) else value if (kept.isNotEmpty()) head.append(name).append(": ").append(kept).append("\r\n") } - // One request per connection keeps every request behind serve's cookie check. head.append("Authorization: ").append(auth).append("\r\nConnection: close\r\n\r\n") upstream.outputStream.write(head.toString().toByteArray(Charsets.ISO_8859_1)) Thread({ runCatching { input.copyTo(upstream.outputStream) } }, "syncthing-proxy-body").apply { isDaemon = true }.start() @@ -98,7 +92,6 @@ class SyncthingProxy(private val socket: File, private val auth: String) : Close private fun withoutToken(header: String) = cookies(header).filter { !it.startsWith("$COOKIE=") }.joinToString("; ") - /** Bounds the whole request line and headers by one deadline, so a slow client can't hold a worker. */ private class HeadDeadline(private val client: Socket, input: InputStream, private val until: Long) : FilterInputStream(input) { @Volatile private var lifted = false diff --git a/app/src/main/java/com/droiddeck/launcher/sync/SyncthingService.kt b/app/src/main/java/com/droiddeck/launcher/sync/SyncthingService.kt index 1ca58487d..9b47b0329 100644 --- a/app/src/main/java/com/droiddeck/launcher/sync/SyncthingService.kt +++ b/app/src/main/java/com/droiddeck/launcher/sync/SyncthingService.kt @@ -13,7 +13,6 @@ import com.droiddeck.launcher.FrontEndActivity import com.droiddeck.launcher.R import com.droiddeck.launcher.stores.download.StoreDownloadService -/** Keeps the app process, and with it Syncthing, alive while "Run in the background" is chosen. */ class SyncthingService : Service() { override fun onBind(intent: Intent?): IBinder? = null diff --git a/app/src/main/java/com/droiddeck/launcher/sync/SyncthingStatus.kt b/app/src/main/java/com/droiddeck/launcher/sync/SyncthingStatus.kt index b52415ed6..c07439e0e 100644 --- a/app/src/main/java/com/droiddeck/launcher/sync/SyncthingStatus.kt +++ b/app/src/main/java/com/droiddeck/launcher/sync/SyncthingStatus.kt @@ -31,7 +31,6 @@ data class SyncSnapshot( val syncing get() = folders.any { it.syncing } val needBytes get() = folders.filter { it.syncing }.sumOf { it.needBytes } - /** Share of the syncing folders' data already in place, 0..100. */ val percent: Int get() { val active = folders.filter { it.syncing } val global = active.sumOf { it.globalBytes } @@ -41,7 +40,6 @@ data class SyncSnapshot( } internal object SyncthingParse { - /** Stands in for a folder whose status request failed, so it shows as an error rather than as up to date. */ fun unreadableStatus(): JSONObject = JSONObject().put("state", "error").put("errors", 1) fun folder(config: JSONObject, status: JSONObject?, blocked: Boolean, names: Map): SyncFolder { diff --git a/app/src/main/java/com/droiddeck/launcher/sync/SyncthingWebActivity.kt b/app/src/main/java/com/droiddeck/launcher/sync/SyncthingWebActivity.kt index 7d26ab159..3e036f943 100644 --- a/app/src/main/java/com/droiddeck/launcher/sync/SyncthingWebActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/sync/SyncthingWebActivity.kt @@ -40,7 +40,6 @@ import com.droiddeck.launcher.ui.DroidDeckTheme import com.droiddeck.launcher.ui.LocalPalette import com.droiddeck.launcher.ui.SecondaryButton -/** Syncthing's own web UI, signed in automatically and kept on the loopback address. */ class SyncthingWebActivity : ComponentActivity() { private var web: WebView? = null @@ -100,7 +99,6 @@ class SyncthingWebActivity : ComponentActivity() { cookies.setAcceptCookie(true) val origin = Uri.parse(url).authority webViewClient = object : WebViewClient() { - // The proxy cookie isn't port-scoped, so nothing else on loopback may receive it. override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? = if (request.url.authority == origin) null else WebResourceResponse("text/plain", "utf-8", 403, "Forbidden", emptyMap(), null) @@ -115,7 +113,6 @@ class SyncthingWebActivity : ComponentActivity() { } } -/** Keeps Syncthing's folder path field read-only, so new folders get the default path inside DroidDeck's Sync folder. */ private fun lockFolderPath(note: String) = """ (function (note) { if (window.droiddeckPathLock) return; 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 e000c996a..7e0103ec8 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/AddedGameEditor.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/AddedGameEditor.kt @@ -287,7 +287,6 @@ private fun ArtOnlyEditor( if (logoOpen && loaded != null) LogoPositionHost(chosen, arts, loaded, onBack = { logoOpen = false }, onCloseAll = { logoOpen = false; close() }) { version++; onChanged() } } -/** The position as last read; null until it has been read, so the screen never opens on a guess and saves it over the real one. */ private class LoadedPosition(val value: LogoPosition.Value?) @Composable @@ -300,7 +299,6 @@ private fun rememberLogoPosition(target: ArtTarget?, version: Int): LoadedPositi return position } -/** The "Adjust logo" screen for [target], over the game's background and logo as they are now. */ @Composable private fun LogoPositionHost( target: ArtTarget, arts: List, start: LoadedPosition, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/BootMovieSection.kt b/app/src/main/java/com/droiddeck/launcher/ui/BootMovieSection.kt index 2c090048d..8e84ae4cb 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/BootMovieSection.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/BootMovieSection.kt @@ -91,16 +91,12 @@ import java.io.File private enum class MovieTab { INSTALLED, IMPORT, REPO, SOUND } -/** Downloads run here rather than in a tab, so switching tabs or closing the settings does not cancel one halfway. */ private val installScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) -/** The downloads under way, by post id and percent; kept here so they are still known when the tab or the settings come back. */ private val installProgress = mutableStateMapOf() -/** What the movie list looks like right now: every movie and the one Steam will play. */ private class MovieSnapshot(val entries: List, val selected: String) -/** The movie list, read again every couple of seconds so a movie picked, added or removed inside Steam shows up here too. */ @Composable private fun rememberMovies(refresh: Int, onSelectionChanged: () -> Unit): MovieSnapshot? { val app = LocalContext.current.applicationContext @@ -143,7 +139,6 @@ private fun TabChip(label: String, selected: Boolean, modifier: Modifier = Modif } } -/** The "Boot movie" settings group: Installed movies, Import your own movies, SteamDeckRepo and Sound. */ @Composable internal fun BootMovieGroup(s: ModeSettings, a: ModeSettingsActions, host: MenuHost) { var tab by rememberSaveable { mutableStateOf(MovieTab.INSTALLED) } @@ -179,7 +174,6 @@ private fun InstalledTab(a: ModeSettingsActions) { val app = LocalContext.current.applicationContext val scope = rememberCoroutineScope() var saving by remember { mutableStateOf(false) } - // The report is for bug reports, so it shows only while Debugging tools is on (read each time this tab opens). val debugging = remember { com.droiddeck.launcher.agent.AgentAccess.commandsEnabled(app) } if (debugging) { SettingsRow(stringResource(R.string.boot_movie_report), stringResource(R.string.boot_movie_report_hint)) { @@ -222,7 +216,6 @@ private fun movieSourceLabel(source: String): String = stringResource( }, ) -/** The installed movies as a list with covers; the one Steam plays is highlighted, and the list follows changes made in Steam. */ @Composable private fun InstalledMoviesDialog(a: ModeSettingsActions, onDismiss: () -> Unit) { val app = LocalContext.current.applicationContext @@ -497,10 +490,8 @@ private fun RepoTab(a: ModeSettingsActions) { } } -/** Downloads [post] and puts it among the installed movies; returns a message when it failed, null when it worked. */ private fun install(app: android.content.Context, post: SteamDeckRepo.Post, onProgress: (Int) -> Unit): String? { val folder = File(app.cacheDir, "boot-movie-download").apply { mkdirs() } - // Leftovers of a download that was cut short are cleared, and every download has a file of its own. folder.listFiles()?.forEach { if (System.currentTimeMillis() - it.lastModified() > 60 * 60 * 1000L) it.delete() } val file = File.createTempFile("movie-", ".webm", folder) return try { @@ -563,7 +554,6 @@ private const val AFTER_OPEN = 3 private const val AFTER_INSTALL = 1 private const val AFTER_USE = 2 -/** Install and Use are different buttons in turn, and the button is disabled while installing; it takes focus back for the controller. */ @Composable private fun RepoAction( installed: Boolean, inUse: Boolean, percent: Int?, busy: Boolean, modifier: Modifier, onInstall: () -> Unit, onUse: () -> Unit, @@ -593,7 +583,6 @@ private fun RepoAction( ) } -/** A movie from SteamDeckRepo played straight from the site, so it can be checked before it is installed. */ @Composable private fun PreviewDialog( post: SteamDeckRepo.Post, installed: Boolean, inUse: Boolean, percent: Int?, busy: Boolean, @@ -674,7 +663,6 @@ private fun MoviePreview(url: String, modifier: Modifier = Modifier) { } } -/** A dialog with one text box, for a search term or a page number; the Android keyboard comes up with it. */ @Composable internal fun TextEntryDialog( title: String, initial: String, number: Boolean, confirm: String, onSubmit: (String) -> Unit, onDismiss: () -> Unit, @@ -697,7 +685,6 @@ internal fun TextEntryDialog( } } -/** Page buttons around the current page, which opens a box to type another page into; first and last page show their numbers. */ @OptIn(ExperimentalLayoutApi::class) @Composable private fun RepoPager(page: Int, last: Int?, hasMore: Boolean, onPage: (Int) -> Unit, onJump: () -> Unit) { diff --git a/app/src/main/java/com/droiddeck/launcher/ui/BottomChatPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/BottomChatPage.kt index e609537b7..8fee83eb9 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/BottomChatPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/BottomChatPage.kt @@ -56,23 +56,14 @@ private const val STEAM_CHAT = "https://steamcommunity.com/chat" private const val CHAT_PAGE_SCALE = 0.75f private val STEAM_HOSTS = listOf("steamcommunity.com", "steam-chat.com", "steampowered.com", "steamstatic.com", "steamcontent.com") -/** Steam chat stays on Steam's own sites; anything else is not opened on the bottom screen. */ internal fun steamChatAllows(host: String?): Boolean = host != null && STEAM_HOSTS.any { host == it || host.endsWith(".$it") } -/** The desktop chat fits the bottom panel better than the phone site; keep WebView's Chrome version. */ internal fun desktopUserAgent(webView: String?): String { val chrome = webView?.let { Regex("Chrome/[\\d.]+").find(it)?.value } ?: "Chrome/127.0.6533.103" return "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) $chrome Safari/537.36" } -/** - * At the panel's own scale Steam's desktop chat gets about 600x300 CSS pixels and its friends list collapses, so the - * page is laid out [cssWidth] wide and drawn at [scale]. Steam's site header (the rows before .chat_main_flex; its - * class name is generated) is hidden to leave the height to the friends list and the chat. The group chat list keeps - * a fixed 114 px height and an empty-list note that cover the online friends, so it shrinks to its rows (at most a - * quarter of the panel) and the note is hidden. - */ internal fun steamChatFitScript(cssWidth: Int, scale: Float): String { val zoom = "%.2f".format(java.util.Locale.US, scale) return "(function(){var m=document.querySelector('meta[name=viewport]');" + @@ -92,7 +83,6 @@ fun BottomChatPage() { val context = LocalContext.current val colors = MaterialTheme.colorScheme var chatOpen by remember { mutableStateOf(true) } - // Set when Android ended the page's renderer; the chat opens again once DroidDeck is back in front. var stopped by remember { mutableStateOf(false) } val lifecycle = LocalLifecycleOwner.current.lifecycle DisposableEffect(lifecycle) { @@ -105,7 +95,6 @@ fun BottomChatPage() { lifecycle.addObserver(observer) onDispose { lifecycle.removeObserver(observer) } } - // null while the Steam client's sign-in is being copied, then whether there was one. var fromSteam by remember { mutableStateOf(null) } LaunchedEffect(chatOpen) { if (!chatOpen) { @@ -145,7 +134,6 @@ fun BottomChatPage() { } } -/** With [onClose] set the chat is open: the row also carries Close and leaves out the long mic hint. */ @Composable private fun VoiceRow(context: Context, onClose: (() -> Unit)? = null, note: String = "") { val colors = MaterialTheme.colorScheme @@ -153,7 +141,6 @@ private fun VoiceRow(context: Context, onClose: (() -> Unit)? = null, note: Stri val max = remember { audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC).coerceAtLeast(1) } var volume by remember { mutableIntStateOf(audio.getStreamVolume(AudioManager.STREAM_MUSIC)) } fun step(direction: Int) { - // No flags: Android's volume panel would cover the game on the main screen. audio.adjustStreamVolume(AudioManager.STREAM_MUSIC, direction, 0) volume = audio.getStreamVolume(AudioManager.STREAM_MUSIC) } @@ -178,8 +165,6 @@ private fun VoiceRow(context: Context, onClose: (() -> Unit)? = null, note: Stri private fun SteamChatView(onStopped: () -> Unit) { AndroidView(factory = { ctx -> WebView(ctx).apply { - // AndroidView's default WRAP_CONTENT height gives the page a 0 px tall viewport, which collapses Steam - // chat's full-height layout (measured on the Retroid Pocket Duo: html and every container 0 px). layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) setBackgroundColor(0xFF0A0B0D.toInt()) settings.javaScriptEnabled = true @@ -199,8 +184,6 @@ private fun SteamChatView(onStopped: () -> Unit) { if (view.width > 0) view.evaluateJavascript(steamChatFitScript((view.width / scale).roundToInt(), CHAT_PAGE_SCALE), null) } - // Android ends the page's renderer once DroidDeck goes to picture-in-picture ("isolated not needed" - // on the Retroid Pocket Duo); the page cannot come back, so the chat closes and reopens on resume. override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail): Boolean { onStopped() return true diff --git a/app/src/main/java/com/droiddeck/launcher/ui/BottomDashboard.kt b/app/src/main/java/com/droiddeck/launcher/ui/BottomDashboard.kt index 4943d99d7..25fd17149 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/BottomDashboard.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/BottomDashboard.kt @@ -45,7 +45,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.droiddeck.launcher.R -/** A page the bottom screen can show while the game runs on the main screen. */ class BottomPage( val id: String, @StringRes val label: Int, @@ -56,10 +55,6 @@ class BottomPage( internal fun pickBottomPage(remembered: String?, ids: List): String? = ids.firstOrNull { it == remembered } ?: ids.firstOrNull { it == DEFAULT_PAGE } ?: ids.firstOrNull() -/** - * The tab picked during one session. Each Steam session starts with no pick, so the dashboard opens on Chat; reopened - * later in the same session it shows the tab picked last. - */ internal object SessionBottomTab { private var session: String? = null private var tab: String? = null @@ -74,13 +69,11 @@ internal object SessionBottomTab { } } -/** The page a game start switches to; not remembered as the user's pick. */ internal fun pageForGameStart(ids: List): String? = GAME_PAGE.takeIf { it in ids } private const val DEFAULT_PAGE = "chat" private const val GAME_PAGE = "telemetry" -/** Tabs show their names while every tab still fits at [TAB_WIDTH]; otherwise only the open tab keeps its name. */ internal fun bottomTabsWithLabels(widthDp: Float, tabs: Int): Boolean = tabs * TAB_WIDTH + 2 * BAR_BUTTON <= widthDp private const val TAB_WIDTH = 132f diff --git a/app/src/main/java/com/droiddeck/launcher/ui/BottomDownloadsPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/BottomDownloadsPage.kt index 0ca56a10b..7ad576f95 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/BottomDownloadsPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/BottomDownloadsPage.kt @@ -42,7 +42,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext -/** Bytes per second between two reads of the same download; 0 when it did not move or restarted. */ internal fun downloadRate(previousDone: Long, previousAt: Long, done: Long, at: Long): Long = if (previousDone < 0L || done <= previousDone || at <= previousAt) 0L else (done - previousDone) * 1000L / (at - previousAt) diff --git a/app/src/main/java/com/droiddeck/launcher/ui/BottomNotificationsPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/BottomNotificationsPage.kt index 379c74b6d..266cb5a59 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/BottomNotificationsPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/BottomNotificationsPage.kt @@ -39,7 +39,6 @@ import com.droiddeck.launcher.R import com.droiddeck.launcher.session.BottomNotifications import kotlinx.coroutines.delay -/** DroidDeck's own switch on Android 11 and later; the list of all apps before that. */ internal fun notificationAccessIntent(context: android.content.Context): Intent = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { Intent(Settings.ACTION_NOTIFICATION_LISTENER_DETAIL_SETTINGS).putExtra( diff --git a/app/src/main/java/com/droiddeck/launcher/ui/BottomPages.kt b/app/src/main/java/com/droiddeck/launcher/ui/BottomPages.kt index da9695cf9..ffe71e400 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/BottomPages.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/BottomPages.kt @@ -8,17 +8,13 @@ import androidx.compose.material.icons.outlined.Notifications import androidx.compose.material.icons.outlined.PhotoLibrary import com.droiddeck.launcher.R -/** What the bottom-screen pages can ask the running session to do. */ class BottomPageActions( val takeScreenshot: ((onDone: (Boolean) -> Unit) -> Unit)? = null, val fps: () -> Float? = { null }, - /** Opens an Android settings page on the bottom screen; the dashboard comes back once [done] is true. */ val openSettings: ((intent: android.content.Intent, done: () -> Boolean) -> Unit)? = null, - /** The focused game's Steam app id as text (or another key for non-Steam programs); null in the Steam menus. */ val runningGame: () -> String? = { null }, ) -/** The pages the bottom-screen dashboard offers during a session, in tab order. */ fun sessionBottomPages(actions: BottomPageActions): List = listOf( BottomPage("telemetry", R.string.bottom_page_telemetry, Icons.Outlined.Speed) { BottomTelemetryPage(actions.fps, actions.runningGame) }, BottomPage("downloads", R.string.bottom_page_downloads, Icons.Outlined.Download) { BottomDownloadsPage() }, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/BottomTelemetryPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/BottomTelemetryPage.kt index c1fb9c2a7..fc1a992ad 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/BottomTelemetryPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/BottomTelemetryPage.kt @@ -78,10 +78,8 @@ private const val THUNDER_RETRY_MS = 3_000L private const val ELITE_MS = 1_000L private const val ELITE_SEARCH_MS = 10_000L private const val OUTPUT_SCAN_MS = 3_000L -/** Top strip history: one reading a second for 30 s. */ private const val PERF_N = 30 -/** What arrives on one telemetry port; written only by that port's listener. */ private class PortTraffic { @Volatile var packets = 0L @Volatile var length = 0 @@ -208,7 +206,6 @@ fun BottomTelemetryPage(fps: () -> Float?, runningGame: () -> String? = { null } } } val f = file - // Status.json changes while Elite runs; one older than a minute belongs to an earlier session. elite = if (f != null && now - f.lastModified() < 60_000L) { withContext(Dispatchers.IO) { runCatching { GameStatusFiles.elite(f.readText()) }.getOrNull() } } else null @@ -335,7 +332,6 @@ private fun httpGet(url: String): String { } -/** The Steam client the session runs, shown in the strip's top line while no game runs. */ internal fun steamClientLabel(arch: String): Int = if (arch == "x86_64") R.string.stats_steam_client_x86 else R.string.stats_steam_client_arm @Composable diff --git a/app/src/main/java/com/droiddeck/launcher/ui/BuildWatermark.kt b/app/src/main/java/com/droiddeck/launcher/ui/BuildWatermark.kt index f2a9e8f65..648d754aa 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/BuildWatermark.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/BuildWatermark.kt @@ -17,7 +17,6 @@ import android.widget.FrameLayout import com.droiddeck.launcher.BuildConfig import com.droiddeck.launcher.QuestXrActivity -/** A faint bottom-right tag on every screen of a test or local build; stable and preview builds draw none. */ object BuildWatermark { private const val TITLE_LIMIT = 80 diff --git a/app/src/main/java/com/droiddeck/launcher/ui/CardDetailHost.kt b/app/src/main/java/com/droiddeck/launcher/ui/CardDetailHost.kt index b7f5d44db..2a50f3c35 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/CardDetailHost.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/CardDetailHost.kt @@ -40,10 +40,8 @@ import kotlinx.coroutines.delay import androidx.compose.runtime.setValue import androidx.compose.runtime.getValue -/** A grid of cards with a game page that floods out of the opened card and drains back into it. */ internal val LocalDetailFloor = compositionLocalOf<() -> Float> { { Float.MAX_VALUE } } -/** How far the host reaches past the page's padding, so a game page can paint to the screen edge and still inset its content. */ internal class DetailInset(val horizontal: Dp, val top: Dp) internal val LocalDetailInset = compositionLocalOf { DetailInset(0.dp, 0.dp) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ColorPickerDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/ColorPickerDialog.kt index d83e1abe1..d8b3c61a2 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ColorPickerDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ColorPickerDialog.kt @@ -43,7 +43,6 @@ import kotlin.math.atan2 import kotlin.math.hypot import kotlin.math.min -/** A small colour square; pressing it opens the colour picker. */ @Composable internal fun ColorCube(color: Color, description: String, focus: FocusRequester? = null, onClick: () -> Unit) { val pal = LocalPalette.current @@ -59,7 +58,6 @@ internal fun ColorCube(color: Color, description: String, focus: FocusRequester? ) } -/** Colour wheel (touch), hue / saturation / brightness sliders (controller and touch) and a hex box for one colour. */ @Composable internal fun ColorPickerDialog(title: String, initial: Int, onDismiss: () -> Unit, onApply: (Int) -> Unit) { val shown = rememberShown(onDismiss) @@ -117,7 +115,6 @@ private fun Swatch(color: Color) { Box(Modifier.size(48.dp).clip(shape).background(color).border(1.dp, LocalPalette.current.line2, shape)) } -/** Hue around the circle and saturation out from the middle, at full brightness; brightness has its own slider. */ @Composable private fun ColorWheel(hue: Float, sat: Float, value: Float, onPick: (hue: Float, sat: Float) -> Unit) { val rim = MaterialTheme.colorScheme.onBackground 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 a934d39cf..a28326a4b 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt @@ -930,7 +930,6 @@ private fun availableFor(l: String, catalog: List, snapshot: Snapsh return catalog.filter { it.comp == l && ComponentsManager.safeName(it.file) !in stored } } -// The rows to show: each stable row carries its paired nightly (if any), and a paired nightly has no row of its own. private fun availableRows(l: String, catalog: List, snapshot: Snapshot?): List> { val available = availableFor(l, catalog, snapshot) val pairs = ComponentsManager.nightlyPairs(available) @@ -1456,7 +1455,6 @@ private fun GetLine( Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line)) } -// Stable | Nightly in the same box as SmallButton: one focus stop, A (or a tap) flips it. @Composable private fun ChannelToggle(nightly: Boolean, enabled: Boolean, onFlip: () -> Unit) { val colors = MaterialTheme.colorScheme 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 aadacf1f4..45862d8ed 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/DeckControlsPanel.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/DeckControlsPanel.kt @@ -254,11 +254,8 @@ class DeckControlsPanel( show() } - /** Whether a finger going down at ([x], [y]) lands on one of the panel's controls. */ fun takesTouchAt(x: Float, y: Float): Boolean = controlUnder(this, x, y) - // Every control here takes the touches it gets: buttons and tabs (clickable), the scrolling tab strip, - // and the drawn surfaces, pads and face buttons. The layouts around them do not. private fun controlUnder(view: View, x: Float, y: Float): Boolean { if (view.visibility != VISIBLE || x < 0f || y < 0f || x >= view.width || y >= view.height) return false if (view.isClickable || view is HorizontalScrollView || view is Surface || view is DirectionPad || view is FaceButtons) return true diff --git a/app/src/main/java/com/droiddeck/launcher/ui/DroidDeckHudSettings.kt b/app/src/main/java/com/droiddeck/launcher/ui/DroidDeckHudSettings.kt index d1e3069f1..821fc9af2 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/DroidDeckHudSettings.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/DroidDeckHudSettings.kt @@ -6,7 +6,6 @@ import androidx.compose.ui.res.stringResource import com.droiddeck.launcher.R import com.droiddeck.launcher.hud.DroidDeckHudSize -/** Bannerlator's DroidDeck HUD settings (DeckOverlaySettings): one row per key of the HUD's config string. */ @Composable internal fun DroidDeckHudSettings(host: MenuHost, a: DrawerActions, track: (String) -> Modifier) { val cfg = a.hudConfig @@ -97,12 +96,10 @@ internal fun DroidDeckHudSettings(host: MenuHost, a: DrawerActions, track: (Stri private fun flag(on: Boolean) = if (on) "1" else "0" -// The Pill also draws the battery temperature and watts. private val EXTRA = mapOf(DroidDeckHudSize.PILL to setOf("Temp", "Power")) private class HudMetric(val chip: String, val key: String, val label: Int) -// DroidDeckHudSize.supportedChips() names; Wrapper and DX version have nothing to show in DroidDeck. private val METRICS = listOf( HudMetric("FPS", "showFPS", R.string.drawer_hud_fps), HudMetric("FPS graph", "showFPSGraph", R.string.hud_metric_graph), @@ -127,8 +124,6 @@ private val METRICS = listOf( HudMetric("Clock", "showClock", R.string.hud_metric_clock), ) -// Switches a size lists but never draws (DroidDeckHudView): the Pill and Minimal always show the FPS, the -// Pill has no graph, Tiles has no graph and no 0.01% low, the Strip always shows the FPS. private val HIDDEN = mapOf( DroidDeckHudSize.PILL to setOf("FPS", "FPS graph"), DroidDeckHudSize.MINIMAL to setOf("FPS"), diff --git a/app/src/main/java/com/droiddeck/launcher/ui/DualScreenPanel.kt b/app/src/main/java/com/droiddeck/launcher/ui/DualScreenPanel.kt index e6825c415..b46afbb97 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/DualScreenPanel.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/DualScreenPanel.kt @@ -128,8 +128,6 @@ object DualScreenPanel { val host = context as? ComponentActivity val target = host?.getSystemService(Context.DISPLAY_SERVICE) ?.let { (it as android.hardware.display.DisplayManager).getDisplay(display) } - // A second-display activity makes the system launcher treat its bottom home as lost and bring itself forward; - // a presentation belongs to this task and leaves that home alone. if (host != null && target != null) { val shown = runCatching { DualScreenPresentation(host, target).also { it.show() } } if (shown.isSuccess) { @@ -170,7 +168,6 @@ object DualScreenPanel { return true } - // The panel resumes when an app opens over the owner on the primary screen; pulling the owner back would hide that app. fun yieldTo(task: Int, app: Boolean) { if (ownerTask == task) ownerYielded = app } 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 bb1370c15..92a6d0b36 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FocusGlide.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FocusGlide.kt @@ -66,7 +66,6 @@ internal class FocusGlide(view: View?) { private set fun accepts(view: View): Boolean { - // DropdownMenu recreates its popup view after closing. if (this.view == null || deferredWindow && this.view !== view && host?.isAttached != true) this.view = view return this.view === view } 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 a8f3f829f..45ef20fe8 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndGames.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndGames.kt @@ -74,7 +74,6 @@ internal fun UninstallEpicLauncher(s: FrontEndState, a: FrontEndActions) { } } -/** A skipped sync is not a success: "0 files" used to show for a signed-out store or cloud saves turned off. */ internal fun cloudMessage(context: android.content.Context, r: com.droiddeck.launcher.stores.CloudSaves.Result): String = when (r.result) { "ok" -> when { r.conflicts > 0 -> context.getString(R.string.cloud_conflict_files, r.conflicts) 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 23d836912..0c91a8873 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt @@ -291,8 +291,6 @@ internal object Motion { if (scale == 0f) snap() else spring(damping, stiffness) } -private val Shape10 = RoundedCornerShape(10.dp) - internal val Shape12 = RoundedCornerShape(12.dp) internal fun hueOf(name: String) = (name.hashCode().toUInt() % 360u).toFloat() @@ -312,13 +310,6 @@ internal fun Rise(i: Int, modifier: Modifier = Modifier, content: @Composable () ) { content() } } -@Composable -private fun Modifier.staggerIn(i: Int): Modifier { - val t = remember { Animatable(0f) } - LaunchedEffect(Unit) { t.animateTo(1f, Motion.tw(360, i * 30)) } - return graphicsLayer { alpha = t.value; translationY = (1f - t.value) * 10.dp.toPx() } -} - @Composable internal fun Modifier.shine(trigger: Boolean, strength: Float = 0.22f): Modifier { val x = remember { Animatable(-1f) } 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 dfe09a03e..6bf704207 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt @@ -702,7 +702,6 @@ private fun GameSettingsCard(host: MenuHost, game: com.droiddeck.launcher.fronte GameSettingsMenus(host, game, state) } -/** The per-game settings menus, opened by setting [MenuHost.open] to [GAME_SETTINGS_MENU]. */ @Composable internal fun GameSettingsMenus(host: MenuHost, game: com.droiddeck.launcher.frontend.Library.SteamGame, state: GameSettingsState) { val appContext = androidx.compose.ui.platform.LocalContext.current.applicationContext @@ -911,14 +910,12 @@ private fun LanguageRow(host: MenuHost, chosen: String, onPick: (String) -> Unit } } -/** How fast Inner Gamer cycles its colours. */ @Composable private fun RgbSpeedRow() { val ctx = androidx.compose.ui.platform.LocalContext.current.applicationContext SliderRow(stringResource(R.string.theme_rgb_speed), stringResource(R.string.theme_rgb_speed_hint), Themes.rgbSpeed, 1..Themes.RGB_SPEEDS) { Themes.setRgbSpeed(ctx, it) } } -/** The two colours of the Gradient theme, each a small square that opens the colour picker. */ @Composable private fun GradientColorRows() { val ctx = androidx.compose.ui.platform.LocalContext.current.applicationContext 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 6f1aad142..2ec816eb1 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndWidgets.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndWidgets.kt @@ -264,23 +264,3 @@ internal fun SecondaryButton(text: String, enabled: Boolean = true, compact: Boo } } -@Composable -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) - 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 - val placed = remember { arrayOfNulls(1) } - val cornerPx = with(androidx.compose.ui.platform.LocalDensity.current) { (if (size > 48.dp) 14.dp else 12.dp).toPx() } - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.paneItem("cog").downToFirstTile().size(size).onGloballyPositioned { placed[0] = it }.clip(if (size > 48.dp) Shape14 else Shape12).background(Color.White.copy(alpha = 0.03f)).glideBorder(hot, if (size > 48.dp) Shape14 else Shape12, pal.signal, pal.line2) - .hoverable(src).clickable(role = androidx.compose.ui.semantics.Role.Button, interactionSource = src, indication = LocalIndication.current) { - placed[0]?.takeIf { it.isAttached }?.let { PageOrigin.mark(Origin(it.boundsInRoot(), cornerPx)) } - spins++ - onClick() - }, - ) { Icon(Icons.Filled.Settings, stringResource(R.string.drawer_page_settings), tint = if (hot) pal.signal else colors.onBackground, modifier = Modifier.size(if (size > 48.dp) 20.dp else 18.dp).rotate(rot)) } -} diff --git a/app/src/main/java/com/droiddeck/launcher/ui/GameDetail.kt b/app/src/main/java/com/droiddeck/launcher/ui/GameDetail.kt index aa193883d..f2f7d5ee9 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/GameDetail.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/GameDetail.kt @@ -115,7 +115,6 @@ import java.io.File import androidx.compose.runtime.setValue import androidx.compose.runtime.getValue -/** What a game page shows: `app:` for a game in the library, `[app:]:` for a store product. */ internal class GameRef(val appId: Int?, val store: Store?, val storeId: String?) { fun game(games: List): Library.SteamGame? = when { appId != null -> games.firstOrNull { it.appId == appId } @@ -195,7 +194,6 @@ internal fun GameDetail( var inPage by remember(key) { mutableStateOf(false) } var onStrip by remember(key) { mutableStateOf(false) } var dropped by remember(key) { mutableStateOf(0) } - // A tab's content is swapped or refreshed under the focused row; focus goes back to the tabs, not to the rail. LaunchedEffect(dropped) { if (dropped == 0) return@LaunchedEffect androidx.compose.runtime.withFrameNanos { } @@ -319,7 +317,6 @@ private fun GameStatusLine(source: String, game: Library.SteamGame?, item: Catal } } -/** One frosted pill that holds the game's actions and its headline scores. */ @Composable private fun GameCapsule(scores: GameScores?, actions: @Composable () -> Unit) { Row( @@ -500,7 +497,6 @@ internal fun GameActions( } } -/** Xbox Cloud Gaming rows the user added to the Library, for games not installed here. */ @Composable internal fun rememberXboxCloudRows(s: FrontEndState): List { val ctx = LocalContext.current @@ -535,7 +531,6 @@ private fun XboxInstalledStream(id: String, title: String, item: CatalogItem?) { private fun XboxLibraryButton(id: String, item: CatalogItem?) { val ctx = LocalContext.current LaunchedEffect(Unit) { XboxCloudGames.load(ctx) } - // Add and Remove replace each other; the controller focus moves to the new button instead of getting lost. val slot = remember { FocusRequester() } val inputMode = androidx.compose.ui.platform.LocalInputModeManager.current val scope = rememberCoroutineScope() @@ -572,7 +567,6 @@ private fun GameShortcutMenu(g: Library.SteamGame, a: FrontEndActions) { } } -/** The pencil opens the editor each kind of game always had: the shortcut editor for added games, art for the rest. */ @Composable private fun rememberEditArt(g: Library.SteamGame): (() -> Unit)? { val ctx = LocalContext.current @@ -593,7 +587,6 @@ private fun EditArtButton(g: Library.SteamGame) { IconAction(Icons.Outlined.Edit, stringResource(R.string.game_edit_art), "game:edit", onClick = open) } -/** Narrow screens keep one More button in the capsule instead of a row of small ones. */ @Composable private fun GameMoreMenu(g: Library.SteamGame, a: FrontEndActions) { var open by remember(g.gameId) { mutableStateOf(false) } @@ -932,7 +925,6 @@ private fun InstallTab(game: Library.SteamGame?, installed: InstalledStoreGame?, } } -/** Text a controller has to land on to bring it into view: focusable, with the focus ring. */ @Composable internal fun Modifier.readable(): Modifier { val pal = LocalPalette.current @@ -953,7 +945,6 @@ internal fun FactRow(label: String, value: String, labelWidth: androidx.compose. } } -/** Store installs are measured by [StoreSizes]; Steam and added games by walking their folder once. */ @Composable internal fun diskSize(installed: InstalledStoreGame?, folder: File): Long? { val ctx = LocalContext.current diff --git a/app/src/main/java/com/droiddeck/launcher/ui/GameGlass.kt b/app/src/main/java/com/droiddeck/launcher/ui/GameGlass.kt index dd5668ee8..a2660d1b4 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/GameGlass.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/GameGlass.kt @@ -55,13 +55,8 @@ import coil.request.ImageRequest import coil.size.Precision import kotlin.math.max -/** Buttons in a capsule render as pills; elsewhere they keep their rounded-rectangle shape. */ internal val LocalPillButtons = compositionLocalOf { false } -/** - * The page's blurred art, shared so frosted surfaces can draw the part of it behind them. - * Coordinates are read when drawing, so a surface and the backdrop always agree on where they are. - */ internal class GlassBackdrop { var painter: Painter? = null var coords: LayoutCoordinates? = null @@ -79,7 +74,6 @@ internal class GlassBackdrop { scope.drawCover(art, size, if (light) LightAmbient else DarkAmbient) } - /** Drawn inside a layer, so it fades what was painted before it toward the page's end. */ fun fadeBottom(scope: DrawScope, size: Size) { val fade = minOf(size.height * 0.5f, with(scope) { 240.dp.toPx() }) if (size.height <= 0f) return @@ -121,7 +115,6 @@ private fun ambientRequest(ctx: android.content.Context, model: Any?): ImageRequ private const val AmbientPixels = 6 -/** How washed-out the art is: average brightness less its saturation, from a tiny decode, so pale art is dimmed while dark and vivid art are left alone. */ @Composable private fun rememberArtBrightness(model: Any?): Float { val ctx = LocalContext.current @@ -148,10 +141,6 @@ private fun rememberArtBrightness(model: Any?): Float { private const val SamplePixels = 8 private const val SaturationWeight = 0.6 -/** - * A page whose game art fills the background: a blurred wash behind everything and the sharp art - * across the top, fading into the page. Surfaces inside draw frosted glass over the wash. - */ @Composable internal fun GameBackdrop( art: Any?, ambient: Any?, fallbackHue: Float, @@ -241,7 +230,6 @@ private fun ArtLayer(model: Any?, sample: Any?, fadeTop: Boolean, onPlaced: (and private val ArtMin = 220.dp private val ArtMax = 340.dp -/** A frosted surface: the page's wash behind it, a dark tint, and a thin rim. Plain surface when there is no backdrop. */ @Composable internal fun Modifier.glass(shape: Shape, lift: Float = 0f): Modifier { val g = LocalGlass.current diff --git a/app/src/main/java/com/droiddeck/launcher/ui/GestureActions.kt b/app/src/main/java/com/droiddeck/launcher/ui/GestureActions.kt index 56643effe..2ada3923e 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/GestureActions.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/GestureActions.kt @@ -41,7 +41,6 @@ internal fun filterGestureActions(actions: List, query: String, g action.title.contains(word, ignoreCase = true) || action.id.removePrefix("key_").replace('_', ' ').contains(word, ignoreCase = true) } }.sortedWith(compareBy { it.group.ordinal }.thenComparator { a, b -> - // Function keys follow their numeric sequence; names use the UI locale. if (a.group == GestureActionGroup.FUNCTION) { a.id.removePrefix("key_F").toInt().compareTo(b.id.removePrefix("key_F").toInt()) } else collator.compare(a.title, b.title) diff --git a/app/src/main/java/com/droiddeck/launcher/ui/GlassesPhonePanel.kt b/app/src/main/java/com/droiddeck/launcher/ui/GlassesPhonePanel.kt index 625c30efb..f5e9138bd 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/GlassesPhonePanel.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/GlassesPhonePanel.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.droiddeck.launcher.R -/** Actions for the phone while the picture is on the glasses. Null hides the button. */ internal class GlassesPhoneActions( val onRecenter: (() -> Unit)?, val onRecenterJoyCons: (() -> Unit)?, 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 fddb98c60..69936548c 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/GpuDriversPanel.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/GpuDriversPanel.kt @@ -225,7 +225,6 @@ internal fun GpuDriversBody( val scroll = rememberScrollState() val scope = rememberCoroutineScope() val placed = remember { arrayOfNulls(2) } - // Focusing the top mode shows as much of the in-use card above it as fits, without hiding the mode itself. val revealCard = { val (viewport, top) = placed if (viewport != null && top != null && viewport.isAttached && top.isAttached) scope.launch { diff --git a/app/src/main/java/com/droiddeck/launcher/ui/LibraryPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/LibraryPage.kt index fe0866e8a..482aeb5a6 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/LibraryPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/LibraryPage.kt @@ -112,7 +112,6 @@ import com.droiddeck.launcher.stores.sortGames import kotlinx.coroutines.launch import androidx.compose.runtime.setValue -/** One owned game: installed ones carry their [game], store products not installed yet only their [item]. */ internal class LibraryEntry( val key: String, val source: String, val title: String, val game: Library.SteamGame?, val item: CatalogItem?, val installed: Boolean, @@ -120,7 +119,6 @@ internal class LibraryEntry( val storeKey: String? get() = item?.key ?: game?.storeId?.let { "${game.source}:$it" } } -/** Installed games first claim their store product, so a game owned and installed shows once. */ internal fun libraryEntries( games: List, owned: Map>, installedStore: Set, ): List { @@ -159,7 +157,6 @@ internal fun librarySources(entries: List, order: List): L (listOf(Library.SOURCE_STEAM) + arrangeStoreTabs(order, emptySet()).map { it.store.id } + Library.ADDED) .filter { source -> entries.any { it.source == source } } -/** Survives leaving the page, so Stores can open the Library on one store's owned games. */ internal object LibraryDisplay { val scales = listOf(0.75f, 0.875f, 1f, 1.2f, 1.45f) var level by mutableIntStateOf(2) @@ -797,7 +794,6 @@ internal fun HeaderIconButton(description: String, icon: androidx.compose.ui.gra ) { Icon(icon, null, tint = if (hot) pal.signal else colors.onSurfaceVariant, modifier = Modifier.size(18.dp)) } } -/** Choosing Library in the rail lands on the first game, as Play Steam does on the Steam page. */ @Composable internal fun Modifier.paneEntry(): Modifier { val ff = LocalFrontFocus.current ?: return this diff --git a/app/src/main/java/com/droiddeck/launcher/ui/LogoPositionDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/LogoPositionDialog.kt index 2ddbdbe28..a6501dacf 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/LogoPositionDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/LogoPositionDialog.kt @@ -57,10 +57,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File -/** Steam's library hero is 3840 x 1240, so the preview keeps that shape. */ private const val HERO_RATIO = 3840f / 1240f -/** The gap Steam leaves between the pinned logo and the edge of the hero, as a share of the hero's width. */ private const val EDGE = 0.03f @Composable @@ -86,10 +84,6 @@ private fun LogoPosition.Pin.alignment(): Alignment = when (this) { internal fun logoPositionSummary(value: LogoPosition.Value?): String = if (value == null) stringResource(R.string.logo_pos_default) else "${value.pin.label()} · ${value.widthPct}%" -/** - * Where a game's logo sits on its background in Steam's library: the logo box is [Value.widthPct] x [Value.heightPct] of the - * background, pinned to one of five places, and the logo is drawn as large as fits in it. - */ @Composable private fun LogoPreview(hero: File?, logo: File?, value: LogoPosition.Value) { val shape = RoundedCornerShape(10.dp) @@ -130,7 +124,6 @@ private fun PinChip(label: String, selected: Boolean, focus: FocusRequester?, on } } -/** The "Adjust logo" screen: the game's background with its logo on it, where to pin the logo and how large to draw it. */ @OptIn(ExperimentalLayoutApi::class) @Composable internal fun LogoPositionDialog( 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 8abc5ff93..568cbca87 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt @@ -81,9 +81,7 @@ class ModeSettings( val picturePosition: String? = null, val stretch16x9: Boolean? = null, val steamClientArch: String? = null, - /** Steam only: a game launched from the front end returns there when it closes; null hides the row. */ val returnAfterGame: Boolean? = null, - /** Steam only: the client's Shader Pre-Caching setting; null hides the row. */ val shaderPrecache: Boolean? = null, val steamRepairQueued: Boolean = false, val mangoapp: Boolean = true, @@ -156,15 +154,12 @@ class ModeSettingsActions( val onDismiss: () -> Unit, ) -/** "2610 r3" from droiddeck-fex-update's FEX_TAG line (fex-2610-r3), the build x86 programs fall back to. */ internal fun fexVersionOf(script: String): String? = Regex("""(?m)^FEX_TAG=(fex-\d+-r\d+)\s*$""").find(script)?.let { fexVersionOfTag(it.groupValues[1]) } -/** "2610 r5" from a tag such as fex-2610-r5. */ internal fun fexVersionOfTag(tag: String): String? = Regex("""fex-(\d+)-r(\d+)""").matchEntire(tag.trim())?.let { "${it.groupValues[1]} r${it.groupValues[2]}" } -/** The build installed in the runtime, else the one fex.json names, else the one the launcher pins. */ @Composable private fun x64FexVersion(context: android.content.Context): String? = remember { val root = com.droiddeck.launcher.runtime.LinuxRuntime.rootDir(context) diff --git a/app/src/main/java/com/droiddeck/launcher/ui/PageSwipe.kt b/app/src/main/java/com/droiddeck/launcher/ui/PageSwipe.kt index 94c7a8cf3..3e4b57fb2 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/PageSwipe.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/PageSwipe.kt @@ -21,7 +21,6 @@ internal object PageSwipe { var direction by mutableIntStateOf(0) } -// Runs on the Final pass so carousels, sliders, scrollers and widgets that consumed the drag keep it. @Composable internal fun Modifier.pageSwipe(enabled: () -> Boolean, onSwipe: (Int) -> Unit): Modifier { val enabledNow by rememberUpdatedState(enabled) diff --git a/app/src/main/java/com/droiddeck/launcher/ui/PhysicalShortcutsDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/PhysicalShortcutsDialog.kt index 3ad5cea7a..7331d49fb 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/PhysicalShortcutsDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/PhysicalShortcutsDialog.kt @@ -179,7 +179,6 @@ private fun CaptureShortcutKeys(onKey: (KeyEvent) -> Boolean) { val window = (LocalView.current.parent as DialogWindowProvider).window DisposableEffect(window) { val previous = window.callback - // Capture before dialog navigation so Back and gamepad buttons can be assigned. window.callback = object : Window.Callback by previous { override fun dispatchKeyEvent(event: KeyEvent): Boolean = currentOnKey(event) || previous.dispatchKeyEvent(event) 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 1a20d0cb6..81c084d6e 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt @@ -1605,7 +1605,6 @@ internal fun ComponentDownloadsDrawerGroup( } AnchoredMenu(open, onDismiss = { if (host.open == key) host.open = null }, title = ComponentsManager.LABEL.getValue(comp)) { firstItemFocus -> - // A nightly paired with a stable build rides on that row; Stable/Nightly tabs beside the row pick which one. val pairs = ComponentsManager.nightlyPairs(items) val rows = items.filter { it !in pairs.values } fun shown(row: ComponentsManager.CatalogItem) = pairs[row]?.takeIf { nightlyPicked[row.file] == true } ?: row diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SessionPausedOverlay.kt b/app/src/main/java/com/droiddeck/launcher/ui/SessionPausedOverlay.kt index 2b9127306..3e5551ab3 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SessionPausedOverlay.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SessionPausedOverlay.kt @@ -72,7 +72,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull -/** The last game frame, captured as the session left the screen, and where it sat in the overlay. */ class PausedFrame(val image: ImageBitmap, val bounds: android.graphics.Rect) private const val CARD_SCALE = 0.82f @@ -125,7 +124,6 @@ fun SessionPausedOverlay( onFrameNeeded() withTimeoutOrNull(FRAME_WAIT_MS) { snapshotFlow { currentFrame }.first { it != null } } } - // Pausing mostly happens while the screen is off or the app is away, so it appears already settled. press.snapTo(1f) ring.snapTo(0f) lift.snapTo(1f) diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StatsDashes.kt b/app/src/main/java/com/droiddeck/launcher/ui/StatsDashes.kt index 9f185b824..fc7679943 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StatsDashes.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StatsDashes.kt @@ -58,12 +58,10 @@ import kotlin.math.roundToInt import kotlin.math.sin private val DASH_H = 290.dp -/** Pedal and speed trace length: 150 readings at the page's 66 ms draw rate, about 10 s. */ internal const val TRACE_N = 150 internal class TraceData(val throttle: List, val brake: List, val speed: List) -/** The racing dash in [style] with speeds in [unit] (km/h or mph); a tap moves to the next one. */ @Composable internal fun RaceDashView(style: StatsDash, t: Telemetry, trace: TraceData, unit: SpeedUnit, onTap: () -> Unit) { Box(Modifier.fillMaxWidth().clickable(onClick = onTap)) { @@ -87,7 +85,6 @@ private const val DIAL_SWEEP = 250f private fun dialAngle(v: Float, max: Float) = DIAL_START + DIAL_SWEEP * (v / max).coerceIn(0f, 1f) -/** A 250° dial face: track, optional red zone from [red], ticks and numbers every [major]. */ private fun DrawScope.dialFace( cx: Float, cy: Float, r: Float, max: Float, major: Float, label: (Float) -> String, measurer: TextMeasurer, labelStyle: TextStyle, tick: Color, minor: Color, track: Color, red: Float?, redColor: Color, @@ -468,7 +465,6 @@ private fun EdgeBar(value: Float?, color: Color) { } } -/** War Thunder's own labels the Flight layout places; others stay in the list. */ internal object FlightLabels { const val IAS = "IAS, km/h" const val ALTITUDE = "H, m" @@ -573,7 +569,6 @@ private fun Pips(pips: List, modifier: Modifier) { val pipH = (size.height - labelH) / 4f - 4.dp.toPx() for (col in 0 until 3) { val x = col * colW + (colW - pipW) / 2f - // Status.json counts half pips: 0..8 per system. val value = pips[col] / 2f for (i in 0 until 4) { val y = size.height - labelH - (i + 1) * (pipH + 4.dp.toPx()) diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StatsStrips.kt b/app/src/main/java/com/droiddeck/launcher/ui/StatsStrips.kt index 7e6ae6434..02bfee49d 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StatsStrips.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StatsStrips.kt @@ -51,13 +51,11 @@ import com.droiddeck.launcher.session.StatsStrip import kotlin.math.max import kotlin.math.roundToInt -/** FusionHUD's metric colors, so the bottom screen reads like the HUD on the top screen. */ internal object StatsColors { val cpu = Color(0xFF58A6FF) val gpu = Color(0xFF5EE08A) val ram = Color(0xFFFF7BC0) val bat = Color(0xFFFFAB5E) - // Throttle and brake stay apart for red-green colour blindness (checked against the dark surface). val throttle = Color(0xFF14A39A) val brake = Color(0xFFE5484D) val speed = Color(0xFF2E8FD6) @@ -66,7 +64,6 @@ internal object StatsColors { val badge = Color(0xFF0B0D10) } -/** The last [capacity] readings, oldest first. */ internal class History(private val capacity: Int) { private val values = ArrayDeque(capacity) fun add(v: Float) { @@ -78,7 +75,6 @@ internal class History(private val capacity: Int) { private val MARK_RATIO = DroidDeckMark.widthFor(1f) -/** The DroidDeck mark (the split D with the blue orb), the same artwork the HUD draws. */ @Composable internal fun DroidDeckLogo(height: Dp, modifier: Modifier = Modifier, body: Color = Color.White, orb: Color = LocalPalette.current.primary) { Canvas(modifier.size(height * MARK_RATIO, height)) { @@ -96,7 +92,6 @@ private fun LogoBadge(size: Dp) { } } -/** The mark and "DroidDeck" on the left, the running game's name in the middle, the clock on the right. */ @Composable private fun LogoHeader(clock: String, game: String?) { val colors = MaterialTheme.colorScheme @@ -123,7 +118,6 @@ internal fun Sparkline(values: List, min: Float, max: Float, color: Color } } -/** A readout card: dark surface, a thin accent edge and the DroidDeck mark before the title ("" = mark only). */ @Composable internal fun StatsCard(title: String?, modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) { val pal = LocalPalette.current @@ -141,7 +135,6 @@ internal fun StatsCard(title: String?, modifier: Modifier = Modifier, content: @ } } -/** What the top strip shows; histories hold one reading a second, oldest first. */ internal class StripData( val fps: Float?, val device: HudMetrics.Snapshot?, @@ -161,7 +154,6 @@ private fun batteryText(s: HudMetrics.Snapshot?): String = private fun fpsText(fps: Float?): String = fps?.takeIf { it > 0f }?.let { "%.1f".format(it) } ?: "–" -/** The top strip; a tap moves to the next style. */ @Composable internal fun StatsStripView(style: StatsStrip, d: StripData, onTap: () -> Unit) { Box(Modifier.fillMaxWidth().clickable(onClick = onTap)) { diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoragePage.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoragePage.kt index 0c1f66b9d..acf6753bf 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoragePage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoragePage.kt @@ -621,7 +621,6 @@ private fun GamesView(report: StorageReport, sessionRunning: Boolean, a: FrontEn Spacer(Modifier.height(8.dp)) Note(stringResource(R.string.storage_games_note)) confirm?.let { g -> - // steam://uninstall reaches only the session's client, the selected one. val otherClient = g.x64 != (SessionPrefs.steamClientArch(LocalContext.current) == "x86_64") if (otherClient) AlertDialog( onDismissRequest = { confirm = null }, 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 642160b15..5d9adf2cb 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoreAchievementsUi.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoreAchievementsUi.kt @@ -204,22 +204,6 @@ internal fun AchievementRow(a: Achievement, compact: Boolean, modifier: Modifier } } -@Composable -internal fun StoreAchievementsCard(game: StoreAchievements.Game, title: String, modifier: Modifier = Modifier) { - val label = StoreAchievements.forSource(game.source)?.label ?: return - val state = rememberAchievements(game) ?: return - var open by remember { mutableStateOf(false) } - StoreAchievementsCardContent(label, state, modifier) { open = true } - if (open) { - VeilRing() - Dialog(onDismissRequest = { open = false }, properties = DialogProperties(usePlatformDefaultWidth = false, decorFitsSystemWindows = false)) { - CompositionLocalProvider(LocalFrontFocus provides null) { - SessionAchievementsPanel(label, state, title, veil = Color.Transparent, floating = true, onClose = { open = false }) - } - } - } -} - @Composable internal fun StoreAchievementsCardContent(label: String, state: AchievementsState, modifier: Modifier = Modifier, onOpen: () -> Unit) { val colors = MaterialTheme.colorScheme 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 314cb13da..90d6d4ac1 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoreGameFacts.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoreGameFacts.kt @@ -177,7 +177,6 @@ internal fun StoreGameFacts(developer: String, publisher: String, releaseDate: S } } -/** The headline scores, small enough to sit inside the action capsule. */ @Composable internal fun CapsuleScores(scores: GameScores, modifier: Modifier = Modifier) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(2.dp), modifier = modifier) { @@ -216,7 +215,6 @@ private fun CapsuleStat(label: String, link: String?, showLabel: Boolean = true, } } -/** Credits and times as label and value rows, for the details panel. */ @Composable internal fun StoreDetailRows(developer: String, publisher: String, releaseDate: String, scores: GameScores) { val released = remember(releaseDate) { GameFacts.releaseLabel(releaseDate) } 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 3996d3042..8cba37f91 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 -// `split` is the size of a leading section that starts its own rows; 0 means one continuous grid. internal fun storeGridNeighbor(index: Int, count: Int, columns: Int, direction: Int, split: Int = 0): Int? { if (index !in 0 until count || columns < 1 || direction !in listOf(-1, 1)) return null if (split !in 1 until count) return (index + columns * direction).takeIf { it in 0 until count } 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 119c0a4d6..35c21173e 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt @@ -808,14 +808,6 @@ private fun SignInButton(store: Store, large: Boolean = false) { } } -@Composable -internal fun StoreLogo(store: Store, size: Dp) { - val c = sourceColours(store.id) - Box(contentAlignment = Alignment.Center, modifier = Modifier.size(size).clip(Shape14).background(c.fill).border(1.dp, c.dot.copy(alpha = 0.5f), Shape14)) { - Text(store.shortLabel, fontSize = (size.value / 4).sp, fontWeight = FontWeight.Black, color = c.ink, maxLines = 1) - } -} - @Composable private fun Storefront( @@ -862,7 +854,6 @@ private fun Storefront( val focus = LocalFrontFocus.current val scope = rememberCoroutineScope() val searching = q.isNotEmpty() || browsing || catalog - // A bar only while there is nothing else to look at: no shelves yet and no search results. val bareTab = if (searching) visible.isEmpty() else shelves == null || shelves.isEmpty val barLine = StoresState.status[store]?.takeIf { bareTab && syncCount(it) != null } val itchBar = store == Store.ITCH && q.isEmpty() @@ -1423,15 +1414,6 @@ internal fun ConfirmButton(label: String, confirm: String, compact: Boolean = fa SecondaryButton(if (armed) confirm else label, compact = compact) { if (!armed) armed = true else { armed = false; onConfirm() } } } -@Composable -internal fun stageLabel(stage: DownloadStage): String = when (stage) { - DownloadStage.MANIFEST -> stringResource(R.string.stores_stage_manifest) - DownloadStage.DOWNLOAD -> stringResource(R.string.stores_stage_download) - DownloadStage.VERIFY -> stringResource(R.string.stores_stage_verify) - DownloadStage.INSTALL -> stringResource(R.string.stores_stage_install) - DownloadStage.DONE -> stringResource(R.string.stores_stage_done) -} - internal fun openStoreUrl(ctx: android.content.Context, item: CatalogItem) = openUrl(ctx, item.storeUrl) internal fun openUrl(ctx: android.content.Context, url: String) { diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SyncFolderPicker.kt b/app/src/main/java/com/droiddeck/launcher/ui/SyncFolderPicker.kt index 26560e047..092b69615 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SyncFolderPicker.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SyncFolderPicker.kt @@ -46,7 +46,6 @@ import java.io.File internal class SyncLocation(val root: File, val label: String, val icon: ImageVector) -/** The Linux runtime's /root, which is HOME for everything DroidDeck runs in the container. */ internal fun containerHome(context: Context) = File(LinuxRuntime.rootDir(context), "root") internal fun syncLocations(context: Context): List { @@ -62,7 +61,6 @@ internal fun syncLocations(context: Context): List { shared + SyncLocation(roots.first(), context.getString(R.string.sync_location_app), Icons.Outlined.Folder) } -/** Browses only DroidDeck's own storage and returns a folder Syncthing is allowed to use. */ @Composable internal fun SyncFolderPicker(title: String, suggestedName: String?, onDismiss: () -> Unit, onPick: (File) -> Unit) { val ctx = LocalContext.current diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SyncPanel.kt b/app/src/main/java/com/droiddeck/launcher/ui/SyncPanel.kt index cac4d039e..e6073d710 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SyncPanel.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SyncPanel.kt @@ -159,7 +159,6 @@ private fun SyncRunning(snap: SyncSnapshot, onShowId: () -> Unit, onAdvanced: () ) } -/** Two columns of settings groups side by side, stacked on narrow panes. */ @Composable internal fun PairedColumns(left: @Composable ColumnScope.() -> Unit, right: @Composable ColumnScope.() -> Unit) { if (LocalNarrowPane.current) { 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 352b29a54..cb6d5ed22 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/Theme.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/Theme.kt @@ -52,7 +52,6 @@ object Themes { const val DEFAULT_PRIMARY = 0xFF8B5CF6.toInt() const val DEFAULT_SECONDARY = 0xFF22D3EE.toInt() - /** The two colours of the custom Gradient theme: primary, then secondary. Changing them restyles the app at once. */ var gradientColors by mutableStateOf(DEFAULT_PRIMARY to DEFAULT_SECONDARY) private set @@ -60,11 +59,9 @@ object Themes { const val DEFAULT_RGB_SPEED = 3 const val RGB_HUE_STEP = 3f - /** How fast Inner Gamer cycles, 1 (slow) to [RGB_SPEEDS] (fast). */ var rgbSpeed by mutableStateOf(DEFAULT_RGB_SPEED) private set - /** Where Inner Gamer is on the colour wheel, in whole [RGB_HUE_STEP] steps so the app restyles only when the colour visibly moves. */ var rgbHue by mutableStateOf(0f) private set @@ -78,7 +75,6 @@ object Themes { rgbSpeed = value } - /** Milliseconds for one trip round the colour wheel. */ fun rgbCycleMillis(speed: Int): Int = 30_000 / speed.coerceIn(1, RGB_SPEEDS) fun loadGradient(context: Context) { @@ -86,7 +82,6 @@ object Themes { rgbSpeed = SessionPrefs.rgbSpeed(context) } - /** The Inner Gamer palette at one point on the colour wheel. */ fun innerGamer(hue: Float): Palette = ThemeColors.palette( Color.hsv(hue % 360f, 0.9f, 1f).toArgb(), Color.hsv((hue + 60f) % 360f, 0.9f, 1f).toArgb(), R.string.theme_inner_gamer, R.string.theme_inner_gamer_detail, INNER_GAMER, @@ -97,7 +92,6 @@ object Themes { gradientColors = primary to secondary } - /** The theme the person chose, with the custom colours read first; for code that is not in a composition. */ fun current(context: Context): Palette { val id = SessionPrefs.theme(context) if (id == GRADIENT || id == INNER_GAMER) loadGradient(context) @@ -186,14 +180,12 @@ private val ContentDirected = Typography().run { ) } -/** Turns the colour wheel on the system's own animation clock; with animations off, in the app or in Android, the colour holds still. */ @Composable private fun InnerGamerHue(speed: Int) { if (Motion.scale == 0f) return key(speed) { InnerGamerCycle(speed) } } -/** One run of the cycle at one speed, picking up from the colour on screen so a new speed takes over at once without a jump. */ @Composable private fun InnerGamerCycle(speed: Int) { val start = remember { Themes.rgbHue } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ThemeColors.kt b/app/src/main/java/com/droiddeck/launcher/ui/ThemeColors.kt index b2db63562..938d2ef90 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ThemeColors.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ThemeColors.kt @@ -7,9 +7,7 @@ import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt -/** Colour maths for the custom gradient theme: hue, saturation and brightness in, a readable palette out. */ internal object ThemeColors { - /** Hue 0..360, saturation and brightness 0..1. */ class Hsv(val h: Float, val s: Float, val v: Float) fun toHsv(argb: Int): Hsv { @@ -32,7 +30,6 @@ internal object ThemeColors { fun hex(argb: Int): String = "#%06X".format(argb and 0xFFFFFF) - /** `#RRGGBB`, `RRGGBB` or `#RGB`; null for anything else. */ fun parseHex(text: String): Int? { val digits = text.trim().removePrefix("#") if (!digits.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }) return null @@ -46,7 +43,6 @@ internal object ThemeColors { private const val MIN_LUMINANCE = 0.10f - /** The colour, made brighter (then paler, for a deep blue that brightness alone cannot lift) when it is too dark to show on the dark background. */ fun readable(argb: Int): Int { if (Color(argb).luminance() >= MIN_LUMINANCE) return argb or 0xFF000000.toInt() val hsv = toHsv(argb) @@ -64,7 +60,6 @@ internal object ThemeColors { return out } - /** A dark neutral tinted with the primary's hue; plain greys stay grey. */ fun tint(primary: Int, saturation: Float, brightness: Float): Color { val hsv = toHsv(primary) val strength = (hsv.s / 0.35f).coerceIn(0f, 1f) @@ -79,7 +74,6 @@ internal object ThemeColors { return (max(x, y) + 0.05f) / (min(x, y) + 0.05f) } - /** Dark or white text, whichever reads better on the worse of the two gradient colours. */ internal fun inkFor(first: Int, second: Int): Color { fun worst(ink: Color) = min(contrast(ink, Color(first)), contrast(ink, Color(second))) return if (worst(DARK_INK) >= worst(Color.White)) DARK_INK else Color.White @@ -87,7 +81,6 @@ internal object ThemeColors { private const val MIN_CONTRAST = 4.5f - /** The colour nudged just enough, lighter for dark text and darker for white text, to give [ink] a readable contrast on it. */ private fun fitTo(ink: Color, argb: Int): Int { fun short(color: Int) = contrast(ink, Color(color)) < MIN_CONTRAST if (!short(argb)) return argb diff --git a/app/src/main/java/com/droiddeck/launcher/ui/TouchGestureDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/TouchGestureDialog.kt index 0fda34fc2..96502bb79 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/TouchGestureDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/TouchGestureDialog.kt @@ -55,7 +55,6 @@ import com.droiddeck.launcher.input.TouchGestureConfig.Companion.MOUSE_DRAG_MOVE @Composable fun TouchGestureDialog(game: String?, onDismiss: () -> Unit, onSaved: () -> Unit) { val context = LocalContext.current - // Keep an open editor attached to the game it was opened for. val targetGame = remember { game } val initialDefaults = remember { GesturePrefs.read(context) } val initialGame = remember { targetGame?.takeIf { GesturePrefs.hasOverride(context, it) }?.let { GesturePrefs.read(context, it) } } @@ -169,7 +168,6 @@ fun TouchGestureDialog(game: String?, onDismiss: () -> Unit, onSaved: () -> Unit private fun GestureDialogFrame(onDismiss: () -> Unit, footer: @Composable () -> Unit = {}, content: @Composable ColumnScope.() -> Unit) { val p = LocalPalette.current - // The app window does not opt into RTL, so these dialogs follow their resource locale explicitly. val direction = if (LocalConfiguration.current.layoutDirection == android.view.View.LAYOUT_DIRECTION_RTL) LayoutDirection.Rtl else LayoutDirection.Ltr Dialog(onDismissRequest = onDismiss, properties = DialogProperties(usePlatformDefaultWidth = false, decorFitsSystemWindows = false)) { CompositionLocalProvider(LocalLayoutDirection provides direction) { 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 3a86e28af..1bbeb18e2 100644 --- a/app/src/main/java/com/droiddeck/launcher/update/AppUpdates.kt +++ b/app/src/main/java/com/droiddeck/launcher/update/AppUpdates.kt @@ -211,10 +211,6 @@ object AppUpdates { internal class Guess(val follow: Follow, val certain: Boolean) - /** - * Stable and Preview builds of one commit are the same APK. Preview history can include older Stable releases, - * so an overlapping older-build guess keeps the Stable default for this run without persisting it. - */ internal fun guessFollow(catalog: Catalog?, me: Installed): Guess { if (me.pr != 0) return Guess(Follow(Channel.TEST, me.pr), true) val stable = catalog?.stable 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 87d958889..5d1d33455 100644 --- a/app/src/main/java/com/droiddeck/launcher/update/SelfInstaller.kt +++ b/app/src/main/java/com/droiddeck/launcher/update/SelfInstaller.kt @@ -57,7 +57,6 @@ object SelfInstaller { } @Suppress("DEPRECATION") val flags = if (Build.VERSION.SDK_INT >= 28) { - // Android 9 and 10 only collect an archive's certificates when GET_SIGNATURES is set. PackageManager.GET_SIGNING_CERTIFICATES or PackageManager.GET_SIGNATURES } else { PackageManager.GET_SIGNATURES @@ -91,7 +90,6 @@ object SelfInstaller { } } - /** Reports the session id before the commit, so a status broadcast that arrives right away is recognised. */ fun install(context: Context, apk: File, onSession: (Int) -> Unit = {}) { val installer = context.packageManager.packageInstaller val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL).apply { @@ -114,7 +112,6 @@ object SelfInstaller { } } - /** Only a system installer activity may be launched from the status broadcast, without URI grants. */ fun safeConfirmIntent(context: Context, confirm: Intent): Intent? { val target = confirm.resolveActivity(context.packageManager) ?: return null val info = runCatching { context.packageManager.getApplicationInfo(target.packageName, 0) }.getOrNull() ?: return null 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 ea06879fa..716644002 100644 --- a/app/src/main/java/com/droiddeck/launcher/wayland/CompositorHost.kt +++ b/app/src/main/java/com/droiddeck/launcher/wayland/CompositorHost.kt @@ -21,7 +21,6 @@ object CompositorHost { @Volatile private var ownVsyncThread = true - // The screen tick gets its own thread so a busy main thread cannot delay the compositor's frames. private val vsyncThread: HandlerThread by lazy { HandlerThread("droiddeck-vsync", Process.THREAD_PRIORITY_DISPLAY).apply { start() } } @@ -30,7 +29,6 @@ object CompositorHost { @Volatile private var vsyncGeneration = 0 - // Each resume gets a new callback; one left queued from an earlier resume, on either thread, stops itself. private fun vsyncCallback(generation: Int) = object : Choreographer.FrameCallback { override fun doFrame(frameTimeNanos: Long) { if (generation != vsyncGeneration) return 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 65e398eec..8fd329ce2 100644 --- a/app/src/main/java/com/droiddeck/launcher/wayland/WaylandCompositor.java +++ b/app/src/main/java/com/droiddeck/launcher/wayland/WaylandCompositor.java @@ -45,9 +45,8 @@ public final class WaylandCompositor { private static volatile long lastFrameNanos; private static volatile long frameIntervalNanos; - private static volatile long gameFrames; // written by the compositor thread only + private static volatile long gameFrames; - /** Frames the game has presented since the app started; the rate is the difference over time. */ public static long gameFrameCount() { return gameFrames; } public static long recentFrameIntervalMs() { @@ -321,7 +320,6 @@ public final class WaylandCompositor { public static native long nativeHudFrameTimes(float[] out); - /** Stream mode: the scene is the Xbox Cloud Gaming native player's picture, handed over frame by frame. */ public static native void nativeStreamMode(boolean on); public static native void nativeStreamFrame(android.hardware.HardwareBuffer buffer); @@ -336,7 +334,6 @@ public final class WaylandCompositor { private static float pendingDeltaX, pendingDeltaY; public static void sendPointerDelta(float dx, float dy) { - // Round-to-zero per event would drop slow motion entirely; carry the remainder. pendingDeltaX += dx * 256f; pendingDeltaY += dy * 256f; int x = (int) Math.floor(pendingDeltaX); diff --git a/app/src/main/java/org/webrtc/DroidDeckLowLatencyDecoderFactory.java b/app/src/main/java/org/webrtc/DroidDeckLowLatencyDecoderFactory.java index 2a196ed2d..c26b295e8 100644 --- a/app/src/main/java/org/webrtc/DroidDeckLowLatencyDecoderFactory.java +++ b/app/src/main/java/org/webrtc/DroidDeckLowLatencyDecoderFactory.java @@ -19,31 +19,14 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.util.HashMap; -/** - * DroidDeck's hardware video decoder factory for the native Xbox Cloud Gaming player. It builds WebRTC's own - * AndroidVideoDecoder (hence this package) around a MediaCodec wrapper that can: - * - ask the decoder for low-latency output (Android's KEY_LOW_LATENCY and Qualcomm's low-latency / picture-order - * vendor keys; unknown keys are ignored, and a decoder that refuses them is configured without them); - * - for zero-copy, decode straight onto the player's SurfaceView instead of WebRTC's texture, rendering every - * output buffer there (no GL copy, no compositor; WebRTC then gets no decoded frames to draw). - * Without either option it behaves like WebRTC's HardwareVideoDecoderFactory. - * The route is switchable while the decoder runs (setOutputSurface): {@link #useScreen} sends frames straight to the - * screen (zero-copy) or, with null, back to WebRTC's texture (for the Vulkan compositor and frame generation); - * {@link #setZeroCopySurface} moves a zero-copy decoder to a stand-in surface while the screen surface is gone. - * The factory also reports the decoded size and the decode time (input queued to output ready), which WebRTC's - * stats lack in zero-copy. - */ public class DroidDeckLowLatencyDecoderFactory implements VideoDecoderFactory { - /** Counts frames sent to the display in zero-copy mode (WebRTC's own stats see none). */ public interface FrameCounter { void onFrameShown(); } private final HardwareVideoDecoderFactory hardware; private final EglBase.Context sharedContext; private final boolean lowLatency; - /** Frames go to {@link #zeroCopySurface} (the screen or the stand-in), not WebRTC's texture. */ private volatile boolean zeroCopy; private volatile Surface zeroCopySurface; - /** Back on WebRTC's texture: render the next buffer there, so WebRTC's pending texture frame completes. */ private volatile boolean renderNext; private final FrameCounter counter; private volatile Wrapper live; @@ -64,19 +47,11 @@ public class DroidDeckLowLatencyDecoderFactory implements VideoDecoderFactory { this.counter = counter; } - /** - * Zero-copy: the surface to decode onto from now on; null = a stand-in that drops every frame (the screen - * surface is being destroyed). Must be called before a destroyed surface's callback returns. - */ public synchronized void setZeroCopySurface(Surface surface) { if (!zeroCopy) return; moveTo(surface != null ? surface : standInSurface(), surface != null ? "the screen" : "a stand-in"); } - /** - * Switch the route live: a surface = zero-copy onto it; null = WebRTC's texture again. Returns false when the - * decoder refused the new surface (the route is then unchanged). - */ public synchronized boolean useScreen(Surface screen) { if (screen != null) { Wrapper w = live; @@ -120,12 +95,10 @@ public class DroidDeckLowLatencyDecoderFactory implements VideoDecoderFactory { return standIn.getSurface(); } - /** The decoder's picture size, or null before the first decoder is configured. */ public String videoSize() { return width > 0 ? width + "x" + height : null; } - /** Average decode time since the last call, in ms (-1 when no frame was decoded in between). */ public double takeDecodeMs() { synchronized (decodeLock) { double avg = decodeCount > 0 ? decodeSumMs / decodeCount : -1; @@ -170,10 +143,8 @@ public class DroidDeckLowLatencyDecoderFactory implements VideoDecoderFactory { } } - /** MediaCodec itself (not WebRTC's wrapper) so the output surface can be changed while it runs. */ private class Wrapper implements MediaCodecWrapper { final MediaCodec raw; - /** WebRTC's texture surface, the route back from zero-copy. */ Surface texture; Wrapper(MediaCodec raw) { this.raw = raw; } @@ -207,8 +178,6 @@ public class DroidDeckLowLatencyDecoderFactory implements VideoDecoderFactory { if (from.containsKey(key)) to.setInteger(key, from.getInteger(key)); } - // Zero-copy: AndroidVideoDecoder renders one buffer to its texture and drops the rest until the texture - // hands the frame back, which never happens with the display surface; show every buffer instead. @Override public void releaseOutputBuffer(int index, boolean render) { if (zeroCopy) { raw.releaseOutputBuffer(index, true); if (counter != null) counter.onFrameShown(); } else if (renderNext) { renderNext = false; raw.releaseOutputBuffer(index, true); } diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 152a0d732..28f6862c2 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1864,6 +1864,93 @@ لوحة المفاتيح + لوحة اللمس الوحدة الطرفية مقابض Deck + لوحتا اللمس + لوحة المعلومات + لا يوجد ما يُعرض هنا بعد. + التنزيلات + لا توجد تنزيلات جارية. + Steam + جارٍ تنزيل ذاكرة المظلِّلات المؤقتة + متوقف مؤقتًا + بانتظار البدء + الإشعارات + الشاشة السفلية لـ DroidDeck + اعرض الرسائل والإشعارات الأخرى هنا أثناء اللعب. تبقى على هذه الشاشة فقط ولا تُحفظ. يطلب Android الإذن مرة واحدة: تُفتح صفحة الوصول إلى الإشعارات على هذه الشاشة؛ فعِّل DroidDeck هناك وستعود هذه الصفحة تلقائيًا. + السماح بالوصول إلى الإشعارات + لا توجد إشعارات. + الإشعارات: %1$d + مسح + لقطات الشاشة + يتطلب FPS تشغيل واجهة الأداء + السرعة + الدواسات + قوة G + التوقيت + أفضل لفة + الإطارات °C + دواسة الوقود والمكابح، آخر 10 s + السرعة، %1$s + الترس + وحدات السرعة + تلقائي (%1$s) + عميل Steam ARM + عميل Steam x86 + دواسة الوقود + المكابح + لوحة العدادات + تبديل الترس + فرامل اليد + محدِّد سرعة حارة الصيانة + TC + ABS + التيربو %1$s bar + المحرك %1$s °C + الزيت %1$s °C + موزّع الطاقة + الوقود، t + الرئيسي %1$s t + الاحتياطي %1$s t + انقر على الشريط العلوي أو لوحة العدادات لتغيير نمطها. + إرسال بيانات لوحة العدادات + تلقائي + يفعّلها لكل لعبة مدرجة هنا، بما في ذلك الألعاب التي يُعثر عليها لاحقًا. تبقى الألعاب التي توقفها متوقفة. + يغيّر هذا إعداد إخراج البيانات الخاص باللعبة نفسها. يسري في المرة التالية التي تبدأ فيها اللعبة، وإيقافه يعيد الإعداد القديم. + أغلق %1$s لتغيير هذا. + تعذّر تغيير إعدادات %1$s. + %1$s: فعِّل %2$s في إعدادات اللعبة باستخدام IP 127.0.0.1 والمنفذ %3$d. + تُحفظ في Pictures/DroidDeck. تظهر لقطات شاشة Steam هنا أيضًا. + التقاط لقطة شاشة + تم حفظ لقطة الشاشة. + تعذّر التقاط لقطة شاشة. + لا توجد لقطات شاشة بعد. + الدردشة والصوت + اقرأ دردشة Steam وردّ عليها هنا بينما تستمر اللعبة على الشاشة الرئيسية. تستخدم الحساب المسجَّل دخوله في Steam. + لا يملك Steam بعد تسجيل دخول على الويب لمشاركته، لذا سجّل الدخول هنا مرة واحدة. + أغلق Android صفحة الدردشة بينما كان DroidDeck في الخلفية. ستُفتح مجددًا عند عودتك إلى DroidDeck. + فتح Steam Chat + إغلاق الدردشة + الميكروفون مفعّل + الميكروفون متوقف + الميكروفون متوقف لهذه اللعبة. للتحدث، فعِّل «الميكروفون» في إعدادات Steam في DroidDeck (الصوت) وابدأ اللعبة من جديد. + مستوى الصوت %1$d%% + إحصاءات داخل اللعبة + FPS + CPU + GPU + البطارية + اللفة + المركز + اللفة الحالية + اللفة السابقة + الوقود + الحمولة + توزيع الطاقة SYS · ENG · WEP + الوضع القانوني + الرصيد + War Thunder + Elite Dangerous + فعِّل إخراج البيانات في لعبة السباق (يُسمّى غالبًا القياس عن بُعد أو UDP) لرؤية لوحة عدادات حية هنا. تبقى البيانات على هذا الجهاز. + يستقبل المنفذ %1$d بيانات (%2$d حزمة بحجم %3$d بايت)، لكن بتنسيق لا تستطيع هذه الصفحة قراءته بعد. + يستخدم برنامج آخر المنفذ %1$s بالفعل، لذا لا يمكن عرض تلك اللعبة هنا. جارٍ إزالة بيئة تشغيل Linux @@ -3392,6 +3479,113 @@ يمكن إعادة حساب المشاهدات المحفوظة. ابدأ معايرة جديدة لالتقاطها في نظام إحداثيات التتبع الحالي. المجموع الاختباري لنموذج تتبع اليدين غير مطابق تعذر تحميل OpenCV + Syncthing + Syncthing + المزامنة عبر Syncthing + هذا تطبيق لـ Syncthing مبني على Syncthing-Fork، يتيح لك مزامنة الملفات الموجودة في نظام الملفات الداخلي لـ DroidDeck مع نسخ Syncthing أخرى. ينطبق ذلك فقط على الملفات التي تخزّنها داخل نظام الملفات الداخلي لـ DroidDeck. + هذا الإصدار لا يتضمن Syncthing. + جارٍ تشغيل Syncthing + جارٍ البدء + محدَّث + جارٍ المزامنة + متوقف مؤقتًا + جارٍ الإيقاف المؤقت + جارٍ الاستئناف + يحتاج إلى انتباه + توقف Syncthing + إعادة المحاولة + متوقف مؤقتًا حتى الاتصال بشبكة Wi-Fi + متوقف مؤقتًا أثناء اللعب + المجلدات: %1$d · الأجهزة المتصلة: %2$d من %3$d + المتبقي: %1$s + فتح واجهة الويب + إيقاف مؤقت + استئناف + قيد التشغيل + أوقف هذا الخيار لإيقاف Syncthing تمامًا. + هذا الجهاز + معرّف الجهاز + عرض رمز QR + متقدم + المنافذ، وقت التشغيل، إعادة التعيين + فتح + بانتظارك + يريد %1$s الاتصال + يريد %1$s مشاركة «%2$s» + اقبل لاختيار مكان حفظه في DroidDeck. + المراجعة في واجهة الويب + المجلدات + لا توجد مجلدات بعد. + %1$s · %2$s + محدَّث + جارٍ المزامنة %1$d%% + جارٍ الفحص + متوقف مؤقتًا + غير متزامن + تعذّرت مزامنة ملفات: %1$d + محظور + يقع هذا المجلد خارج مساحة تخزين DroidDeck، لذلك أوقفه DroidDeck مؤقتًا. لمزامنة مجلدات خارج DroidDeck، استخدم تطبيق Syncthing-Fork. + إزالة من Syncthing + لا تؤدي إزالته إلى حذف أي ملفات. + الأجهزة + لا توجد أجهزة بعد. + متصل + غير متصل + آخر ظهور %1$s + معرّف هذا الجهاز + على جهازك الآخر، أضف جهازًا في Syncthing وامسح هذا الرمز أو الصق المعرّف. ثم اقبله هنا في واجهة الويب. + نسخ + مشاركة + تم + هل تريد إيقاف Syncthing؟ + سيتوقف Syncthing الآن ويظل متوقفًا إلى أن تعيد تشغيله. تبقى الملفات المتزامنة في مكانها. + حذف إعدادات Syncthing أيضًا + يزيل معرّف هذا الجهاز والمجلدات والأجهزة. عند تشغيله مجددًا يبدأ من جديد. + إيقاف + المنافذ + المزامنة + الاكتشاف المحلي + اختيار منافذ جديدة + يُعاد التحقق منها عند كل تشغيل. + إعادة الاختيار + إعادة التعيين + مفتاح API + إعادة الإنشاء + إعادة تعيين Syncthing + معرّف جهاز جديد، وتُزال المجلدات والأجهزة. يُحتفظ بالملفات الموجودة في مساحة تخزين DroidDeck. + إعادة التعيين + هل تريد إعادة تعيين Syncthing؟ + التشغيل + التشغيل + أثناء فتح DroidDeck + في الخلفية + عبر Wi-Fi فقط + إيقاف مؤقت أثناء الألعاب + السجلات + مخرجات Syncthing لهذه الجلسة + مشاركة + Syncthing + اضبط بيئة تشغيل Linux أولًا. + قبول + حفظ «%1$s» في + إضافة مجلد + مزامنة مجلد في مساحة تخزين DroidDeck + اختيار + إضافة + تعذّرت إضافة المجلد. + المشاركة مع + لا توجد أجهزة بعد. يمكنك مشاركة هذا المجلد من واجهة الويب لاحقًا. + استخدام هذا المجلد + مجلد جديد + المجلد الرئيسي للحاوية + مجلد Sync + مساحة تخزين DroidDeck المشتركة + DroidDeck على %1$s + بيانات تطبيق DroidDeck + تُحفظ المجلدات الجديدة في مجلد Sync الخاص بـ DroidDeck. لمزامنة مجلد آخر في DroidDeck، استخدم «إضافة مجلد» في علامة تبويب Syncthing. + Syncthing + إعادة التحميل + Syncthing لا يعمل. توقّف Steam بسبب تعليمة غير صالحة (SIGILL). قد تكون مشكلة توافق في تعليمات CPU أو تعطّلًا في العميل. شارك السجلات مع تفاصيل جهازك. يمكنك تجربة إعدادات Steam → عميل Steam → x86-64 (تجريبي). يتطلب تنزيلًا إضافيًا وقد يعمل بشكل أبطأ. تعذّر على برنامج تشغيل رسومات Linux العثور على جهاز Vulkan صالح للاستخدام. تحقّق من أن برنامج تشغيل GPU المحدد لـ Linux يدعم هذا الـ GPU. diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml index 3f959fe51..d78cd0c5a 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -1862,6 +1862,93 @@ Teclado + trackpad Terminal Agarres de Deck + trackpads + Panel + Todavía no hay nada que mostrar aquí. + Descargas + No hay descargas en curso. + Steam + Descargando caché de shaders + En pausa + Esperando para empezar + Notificaciones + DroidDeck – pantalla inferior + Muestra aquí los mensajes y otras notificaciones mientras juegas. Solo permanecen en esta pantalla y no se guardan. Android lo pregunta una vez: su página de acceso a notificaciones se abre en esta pantalla; activa DroidDeck ahí y esta página volverá sola. + Permitir acceso a notificaciones + No hay notificaciones. + %1$d notificaciones + Borrar + Capturas de pantalla + Los FPS requieren el HUD activado + Velocidad + Pedales + Fuerza G + Tiempos + Mejor vuelta + Llantas °C + Acelerador y freno, últimos 10 s + Velocidad, %1$s + Marcha + Unidades de velocidad + Automático (%1$s) + Cliente de Steam ARM + Cliente de Steam x86 + Acelerador + Freno + Luces del tablero + Cambio + Freno de mano + Limitador de pit + TC + ABS + Turbo %1$s bar + Motor %1$s °C + Aceite %1$s °C + Distribuidor de energía + Combustible, t + Principal %1$s t + Reserva %1$s t + Toca la franja superior o el tablero para cambiar su estilo. + Enviar datos al panel + Automático + Lo activa en todos los juegos de esta lista, incluidos los que se encuentren más adelante. Los juegos que desactives seguirán desactivados. + Esto cambia el ajuste de salida de datos del propio juego. Se aplica la próxima vez que se inicie el juego; al desactivarlo se restaura el ajuste anterior. + Cierra %1$s para cambiar esto. + No se pudieron cambiar los ajustes de %1$s. + %1$s: activa %2$s en los ajustes del juego con la IP 127.0.0.1 y el puerto %3$d. + Se guardan en Pictures/DroidDeck. Las capturas de Steam también aparecen aquí. + Hacer captura de pantalla + Captura de pantalla guardada. + No se pudo hacer la captura de pantalla. + Todavía no hay capturas de pantalla. + Chat y voz + Lee y responde el chat de Steam aquí mientras el juego sigue en la pantalla principal. Usa la cuenta con la que se inició sesión en Steam. + Steam todavía no tiene una sesión web que compartir, así que inicia sesión aquí una vez. + Android cerró la página del chat mientras DroidDeck estaba en segundo plano. Se volverá a abrir cuando regreses a DroidDeck. + Abrir Steam Chat + Cerrar chat + Micro activado + Micro desactivado + El micro está desactivado en este juego. Para hablar, activa Micrófono en los ajustes de Steam de DroidDeck (Audio) y vuelve a iniciar el juego. + Volumen %1$d%% + Estadísticas del juego + FPS + CPU + GPU + Batería + Vuelta + Posición + Vuelta actual + Última vuelta + Combustible + Carga + Pips SYS · ENG · WEP + Estado legal + Saldo + War Thunder + Elite Dangerous + Activa la salida de datos de un juego de carreras (a menudo llamada telemetría o UDP) para ver aquí un panel en vivo. Los datos no salen de este dispositivo. + El puerto %1$d recibe datos (%2$d paquetes de %3$d bytes), pero en un formato que esta página aún no puede leer. + Otro programa ya usa el puerto %1$s, así que ese juego no se puede mostrar aquí. Eliminando el runtime de Linux Eliminando el runtime de Linux · %1$d elemento @@ -2622,7 +2709,7 @@ Hecho No se pudo guardar la disposición táctil Disposición táctil guardada en Steam - App %1$d + App %1$s Etiqueta (opcional) No se encontraron los iconos de Steam @@ -3257,6 +3344,113 @@ Las vistas guardadas se pueden recalcular. Inicie una nueva calibración para capturar en el sistema de coordenadas de seguimiento actual. La suma de comprobación del modelo de mano no coincide OpenCV no se pudo cargar + Syncthing + Syncthing + Sincronizar con Syncthing + Basada en Syncthing-Fork, es una implementación de Syncthing que te permite sincronizar archivos del sistema de archivos interno de DroidDeck con otras instancias de Syncthing. Solo sirve para los archivos que guardas en el sistema de archivos interno de DroidDeck. + Esta compilación no incluye Syncthing. + Iniciando Syncthing + Iniciando + Actualizado + Sincronizando + En pausa + Pausando + Reanudando + Requiere atención + Syncthing se detuvo + Reintentar + En pausa hasta tener Wi-Fi + En pausa durante el juego + %1$d carpetas · %2$d de %3$d dispositivos conectados + Faltan %1$s + Abrir la interfaz web + Pausar + Reanudar + En ejecución + Desactívalo para detener Syncthing por completo. + Este dispositivo + ID del dispositivo + Mostrar QR + Avanzado + Puertos, cuándo se ejecuta, restablecer + Abrir + Esperando tu respuesta + %1$s quiere conectarse + %1$s quiere compartir “%2$s” + Acepta para elegir dónde se guarda en DroidDeck. + Revisar en la interfaz web + Carpetas + Aún no hay carpetas. + %1$s · %2$s + Actualizada + Sincronizando %1$d%% + Escaneando + En pausa + No sincronizada + No se pudieron sincronizar %1$d archivos + Bloqueada + Esta carpeta está fuera del almacenamiento de DroidDeck, así que DroidDeck la puso en pausa. Para sincronizar carpetas fuera de DroidDeck, usa la app Syncthing-Fork. + Quitar de Syncthing + Quitarla no borra ningún archivo. + Dispositivos + Aún no hay dispositivos. + Conectado + Sin conexión + Visto por última vez %1$s + ID de este dispositivo + En tu otro dispositivo, agrega un dispositivo en Syncthing y escanea este código o pega el ID. Después, acéptalo aquí en la interfaz web. + Copiar + Compartir + Listo + ¿Desactivar Syncthing? + Syncthing se detiene ahora y seguirá detenido hasta que vuelvas a activarlo. Los archivos sincronizados se quedan donde están. + Borrar también la configuración de Syncthing + Quita el ID de este dispositivo, las carpetas y los dispositivos. Al volver a activarlo, empieza desde cero. + Desactivar + Puertos + Sincronización + Descubrimiento local + Elegir puertos nuevos + Se vuelve a revisar en cada inicio. + Volver a elegir + Restablecer + Clave API + Regenerar + Restablecer Syncthing + ID de dispositivo nuevo; se quitan las carpetas y los dispositivos. Los archivos del almacenamiento de DroidDeck se conservan. + Restablecer + ¿Restablecer Syncthing? + Ejecución + Ejecutar + Mientras DroidDeck está abierto + En segundo plano + Solo con Wi-Fi + Pausar durante los juegos + Registros + Salida de Syncthing de esta sesión + Compartir + Syncthing + Configura primero el runtime de Linux. + Aceptar + Guardar “%1$s” en + Agregar carpeta + Sincroniza una carpeta del almacenamiento de DroidDeck + Elegir + Agregar + No se pudo agregar la carpeta. + Compartir con + Aún no hay dispositivos. Puedes compartir esta carpeta más tarde desde la interfaz web. + Usar esta carpeta + Carpeta nueva + Carpeta personal del contenedor + Carpeta Sync + Almacenamiento compartido de DroidDeck + DroidDeck en %1$s + Datos de la app DroidDeck + Las carpetas nuevas se guardan en la carpeta Sync de DroidDeck. Para sincronizar otra carpeta de DroidDeck, usa Agregar carpeta en la pestaña Syncthing. + Syncthing + Recargar + Syncthing no se está ejecutando. Steam se detuvo por una instrucción ilegal (SIGILL). Puede ser un problema de compatibilidad con instrucciones de la CPU o un fallo del cliente. Comparte los registros con los datos de tu dispositivo. Puedes probar Ajustes de Steam → Cliente de Steam → x86-64 (experimental). Requiere una descarga adicional y puede ir más lento. El controlador gráfico de Linux no encontró un dispositivo Vulkan utilizable. Comprueba que el controlador de GPU de Linux seleccionado sea compatible con esta GPU. diff --git a/app/src/main/res/values-bg/gestures.xml b/app/src/main/res/values-bg/gestures.xml index 15898ef0e..60d56796f 100644 --- a/app/src/main/res/values-bg/gestures.xml +++ b/app/src/main/res/values-bg/gestures.xml @@ -71,7 +71,7 @@ Backspace Enter Tab - Space + Интервал Delete Insert Home diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index eff7ca9b6..434621739 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -1597,7 +1597,7 @@ Ctrl Super Alt - Space + Интервал AltGr Tab %1$s ✓ @@ -1776,6 +1776,93 @@ Клавиатура + тракпад Терминал Захвати + тракпади на Deck + Табло + Все още няма нищо за показване. + Изтегляния + Няма активни изтегляния. + Steam + Изтегляне на кеша на шейдърите + На пауза + Изчаква стартиране + Известия + Долен екран на DroidDeck + Показвайте тук съобщения и други известия, докато играете. Те остават само на този екран и не се запазват. Android пита веднъж: страницата за достъп до известията се отваря на този екран; включете DroidDeck там и тази страница ще се върне сама. + Разрешаване на достъпа до известията + Няма известия. + Известия: %1$d + Изчисти + Екранни снимки + За FPS трябва HUD да е включен + Скорост + Педали + G-сила + Времена + Най-добра обиколка + Гуми °C + Газ и спирачка, последните 10 s + Скорост, %1$s + Предавка + Единици за скорост + Автоматично (%1$s) + Steam клиент ARM + Steam клиент x86 + Газ + Спирачка + Табло + Смяна на предавка + Ръчна спирачка + Ограничител за бокса + TC + ABS + Турбо %1$s bar + Двигател %1$s °C + Масло %1$s °C + Разпределител на енергия + Гориво, t + Основно %1$s t + Резерв %1$s t + Докоснете горната лента или таблото, за да смените стила му. + Изпращане на данни за таблото + Автоматично + Включва го за всяка игра от списъка тук, включително игри, открити по-късно. Игрите, които изключите, остават изключени. + Това променя собствената настройка на играта за извеждане на данни. Влиза в сила при следващото стартиране на играта; изключването връща старата настройка. + Затворете %1$s, за да промените това. + Настройките на %1$s не можаха да бъдат променени. + %1$s: включете %2$s в настройките на играта с IP 127.0.0.1 и порт %3$d. + Запазват се в Pictures/DroidDeck. Екранните снимки от Steam също се показват тук. + Направи екранна снимка + Екранната снимка е запазена. + Не можа да се направи екранна снимка. + Все още няма екранни снимки. + Чат и глас + Четете и отговаряйте на чата в Steam тук, докато играта продължава на основния екран. Използва се акаунтът, с който сте влезли в Steam. + Steam все още няма уеб вход за споделяне, затова влезте тук веднъж. + Android затвори страницата на чата, докато DroidDeck беше на заден фон. Тя ще се отвори отново, когато се върнете в DroidDeck. + Отвори Steam Chat + Затвори чата + Микрофонът е включен + Микрофонът е изключен + Микрофонът е изключен за тази игра. За да говорите, включете „Микрофон“ в настройките на Steam в DroidDeck (Звук) и стартирайте играта отново. + Сила на звука %1$d%% + Статистика в играта + FPS + CPU + GPU + Батерия + Обиколка + Позиция + Тази обиколка + Последна обиколка + Гориво + Товар + Енергия SYS · ENG · WEP + Правен статус + Баланс + War Thunder + Elite Dangerous + Включете извеждането на данни в състезателна игра (често наричано телеметрия или UDP), за да виждате тук табло на живо. Данните остават на това устройство. + Порт %1$d получава данни (%2$d пакета по %3$d байта), но във формат, който тази страница все още не може да чете. + Друга програма вече използва порт %1$s, затова тази игра не може да се показва тук. Премахване на средата на Linux @@ -3248,6 +3335,113 @@ Запазените изгледи могат да бъдат преизчислени. Започнете ново калибриране за заснемане в текущата координатна система за проследяване. Несъответствие на контролната сума на модела на ръката OpenCV не можа да се зареди + Syncthing + Syncthing + Синхронизиране със Syncthing + Базирана на Syncthing-Fork, това е реализация на Syncthing, която ви позволява да синхронизирате файлове във вътрешната файлова система на DroidDeck с други екземпляри на Syncthing. Това важи само за файлове, които съхранявате във вътрешната файлова система на DroidDeck. + Тази компилация не включва Syncthing. + Стартиране на Syncthing + Стартиране + Актуално + Синхронизиране + На пауза + Поставяне на пауза + Възобновяване + Изисква внимание + Syncthing спря + Опитай отново + На пауза до връзка с Wi-Fi + На пауза по време на играта + Папки: %1$d · свързани устройства: %2$d от %3$d + Остават %1$s + Отвори уеб интерфейса + Пауза + Продължи + Работи + Изключете, за да спрете Syncthing напълно. + Това устройство + Идентификатор на устройството + Покажи QR + Разширени + Портове, кога да работи, нулиране + Отвори + Очаква ви + %1$s иска да се свърже + %1$s иска да сподели „%2$s“ + Приемете, за да изберете къде да се запази в DroidDeck. + Преглед в уеб интерфейса + Папки + Все още няма папки. + %1$s · %2$s + Актуално + Синхронизиране %1$d%% + Сканиране + На пауза + Несинхронизирано + Файлове, които не можаха да се синхронизират: %1$d + Блокирано + Тази папка е извън хранилището на DroidDeck, затова DroidDeck я постави на пауза. За да синхронизирате папки извън DroidDeck, използвайте приложението Syncthing-Fork. + Премахни от Syncthing + Премахването не изтрива файлове. + Устройства + Все още няма устройства. + Свързано + Офлайн + Последно видяно %1$s + Идентификатор на това устройство + На другото си устройство добавете устройство в Syncthing и сканирайте този код или поставете идентификатора. След това го приемете тук в уеб интерфейса. + Копирай + Сподели + Готово + Изключване на Syncthing? + Syncthing спира сега и остава спрян, докато не бъде включен отново. Синхронизираните файлове остават на мястото си. + Изтрий и конфигурацията на Syncthing + Премахва идентификатора на това устройство, папките и устройствата. При повторно включване започва отначало. + Изключи + Портове + Синхронизиране + Локално откриване + Избор на нови портове + Проверява се отново при всяко стартиране. + Избери отново + Нулиране + API ключ + Генерирай нов + Нулиране на Syncthing + Нов идентификатор на устройството, папките и устройствата се премахват. Файловете в хранилището на DroidDeck се запазват. + Нулирай + Нулиране на Syncthing? + Работа + Изпълнение + Докато DroidDeck е отворен + На заден план + Само през Wi-Fi + Пауза по време на игри + Журнали + Изход на Syncthing за тази сесия + Сподели + Syncthing + Първо настройте средата на Linux. + Приеми + Запазване на „%1$s“ в + Добави папка + Синхронизиране на папка в хранилището на DroidDeck + Избери + Добави + Папката не можа да бъде добавена. + Споделяне с + Все още няма устройства. Можете да споделите тази папка по-късно от уеб интерфейса. + Използвай тази папка + Нова папка + Домашна папка на контейнера + Папка Sync + Споделено хранилище на DroidDeck + DroidDeck на %1$s + Данни на приложението DroidDeck + Новите папки се запазват в папката Sync на DroidDeck. За да синхронизирате друга папка на DroidDeck, използвайте „Добави папка“ в раздела Syncthing. + Syncthing + Презареди + Syncthing не работи. Steam спря с невалидна инструкция (SIGILL). Това може да е проблем със съвместимостта на CPU инструкциите или срив на клиента. Споделете дневниците с данни за устройството си. Можете да опитате Настройки на Steam → Клиент на Steam → x86-64 (експериментално). Изисква допълнително изтегляне и може да работи по-бавно. Графичният драйвер на Linux не намери използваемо устройство Vulkan. Проверете дали избраният GPU драйвер за Linux поддържа този GPU. diff --git a/app/src/main/res/values-cs/gestures.xml b/app/src/main/res/values-cs/gestures.xml index 3b1b30f3a..6252bf73e 100644 --- a/app/src/main/res/values-cs/gestures.xml +++ b/app/src/main/res/values-cs/gestures.xml @@ -71,7 +71,7 @@ Backspace Enter Tab - Space + Mezerník Delete Insert Home diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 272aa9171..e060b5590 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -1629,7 +1629,7 @@ Ctrl Super Alt - Space + Mezerník AltGr Tab %1$s ✓ @@ -1820,6 +1820,93 @@ Klávesnice + trackpad Terminál Rukojeti + trackpady Deck + Přehled + Zatím tu není nic k zobrazení. + Stahování + Neprobíhá žádné stahování. + Steam + Stahování mezipaměti shaderů + Pozastaveno + Čeká na spuštění + Oznámení + Spodní obrazovka DroidDeck + Zobrazujte zde zprávy a další oznámení během hraní. Zůstávají jen na této obrazovce a neukládají se. Android se zeptá jednou: na této obrazovce se otevře jeho stránka přístupu k oznámením; zapněte tam DroidDeck a tato stránka se sama vrátí. + Povolit přístup k oznámením + Žádná oznámení. + Oznámení: %1$d + Vymazat + Snímky obrazovky + FPS vyžaduje zapnutý HUD + Rychlost + Pedály + Přetížení (G) + Časy + Nejlepší kolo + Pneumatiky °C + Plyn a brzda, posledních 10 s + Rychlost, %1$s + Rychlostní stupeň + Jednotky rychlosti + Automaticky (%1$s) + Klient Steam ARM + Klient Steam x86 + Plyn + Brzda + Kontrolky + Řazení + Ruční brzda + Omezovač v boxech + TC + ABS + Turbo %1$s bar + Motor %1$s °C + Olej %1$s °C + Rozdělovač energie + Palivo, t + Hlavní %1$s t + Rezerva %1$s t + Klepnutím na horní pruh nebo palubní desku změníte jejich styl. + Odesílat data pro palubní desku + Automaticky + Zapne to pro každou zde uvedenou hru, včetně her nalezených později. Hry, které vypnete, zůstanou vypnuté. + Tím se mění vlastní nastavení výstupu dat ve hře. Projeví se při příštím spuštění hry; vypnutím se obnoví původní nastavení. + Pro změnu zavřete %1$s. + Nastavení hry %1$s se nepodařilo změnit. + %1$s: zapněte %2$s v nastavení hry s IP 127.0.0.1 a portem %3$d. + Ukládá se do Pictures/DroidDeck. Zobrazují se zde i snímky obrazovky ze Steamu. + Pořídit snímek obrazovky + Snímek obrazovky uložen. + Snímek obrazovky se nepodařilo pořídit. + Zatím žádné snímky obrazovky. + Chat a hlas + Čtěte chat Steamu a odpovídejte na něj zde, zatímco hra dál běží na hlavní obrazovce. Používá účet, kterým je Steam přihlášen. + Steam zatím nemá webové přihlášení ke sdílení, proto se zde jednou přihlaste. + Android zavřel stránku chatu, když byl DroidDeck na pozadí. Znovu se otevře, až se do DroidDecku vrátíte. + Otevřít Steam Chat + Zavřít chat + Mikrofon zapnut + Mikrofon vypnut + Mikrofon je pro tuto hru vypnutý. Chcete-li mluvit, zapněte Mikrofon v nastavení Steamu v DroidDecku (Zvuk) a spusťte hru znovu. + Hlasitost %1$d%% + Statistiky ve hře + FPS + CPU + GPU + Baterie + Kolo + Pozice + Aktuální kolo + Poslední kolo + Palivo + Náklad + Energie SYS · ENG · WEP + Právní stav + Zůstatek + War Thunder + Elite Dangerous + Zapněte v závodní hře výstup dat (často nazývaný telemetrie nebo UDP) a uvidíte zde živou palubní desku. Data zůstávají v tomto zařízení. + Port %1$d přijímá data (%2$d paketů po %3$d bajtech), ale ve formátu, který tato stránka zatím neumí přečíst. + Port %1$s již používá jiný program, takže se zde tato hra nemůže zobrazit. Odstraňování runtime Linuxu @@ -3320,6 +3407,113 @@ Uložená zobrazení lze přepočítat. Spusťte novou kalibraci pro zachycení v aktuálním sledovacím souřadnicovém systému. Neshoda kontrolního součtu modelu ruky OpenCV se nepodařilo načíst + Syncthing + Syncthing + Synchronizovat přes Syncthing + Jde o implementaci Syncthingu založenou na Syncthing-Fork, která umožňuje synchronizovat soubory v interním souborovém systému DroidDecku s dalšími instancemi Syncthingu. Týká se pouze souborů, které ukládáte v interním souborovém systému DroidDecku. + Toto sestavení neobsahuje Syncthing. + Spouští se Syncthing + Spouští se + Aktuální + Synchronizuje se + Pozastaveno + Pozastavuje se + Obnovuje se + Vyžaduje pozornost + Syncthing se zastavil + Zkusit znovu + Pozastaveno do připojení k Wi-Fi + Pozastaveno během hry + Složky: %1$d · připojená zařízení: %2$d z %3$d + Zbývá %1$s + Otevřít webové rozhraní + Pozastavit + Obnovit + Spuštěno + Vypnutím Syncthing úplně zastavíte. + Toto zařízení + ID zařízení + Zobrazit QR + Pokročilé + Porty, kdy spouštět, obnovení + Otevřít + Čeká na vás + %1$s se chce připojit + %1$s chce sdílet „%2$s“ + Po přijetí vyberete, kam se v DroidDecku uloží. + Zkontrolovat ve webovém rozhraní + Složky + Zatím žádné složky. + %1$s · %2$s + Aktuální + Synchronizace %1$d %% + Prohledává se + Pozastaveno + Nesynchronizováno + Nepodařilo se synchronizovat soubory: %1$d + Zablokováno + Tato složka je mimo úložiště DroidDecku, proto ji DroidDeck pozastavil. Ke synchronizaci složek mimo DroidDeck použijte aplikaci Syncthing-Fork. + Odebrat ze Syncthingu + Odebráním se žádné soubory nesmažou. + Zařízení + Zatím žádná zařízení. + Připojeno + Offline + Naposledy viděno %1$s + ID tohoto zařízení + Na druhém zařízení přidejte v Syncthingu zařízení a naskenujte tento kód nebo vložte ID. Potom ho zde přijměte ve webovém rozhraní. + Kopírovat + Sdílet + Hotovo + Vypnout Syncthing? + Syncthing se nyní zastaví a zůstane zastavený, dokud ho znovu nezapnete. Synchronizované soubory zůstanou, kde jsou. + Smazat také konfiguraci Syncthingu + Odebere ID tohoto zařízení, složky a zařízení. Po opětovném zapnutí začne od začátku. + Vypnout + Porty + Synchronizace + Místní vyhledávání + Vybrat nové porty + Kontroluje se znovu při každém spuštění. + Vybrat znovu + Obnovení + Klíč API + Vygenerovat znovu + Obnovit Syncthing + Nové ID zařízení, složky a zařízení budou odebrány. Soubory v úložišti DroidDecku zůstanou zachovány. + Obnovit + Obnovit Syncthing? + Spouštění + Spouštět + Když je DroidDeck otevřený + Na pozadí + Pouze Wi-Fi + Pozastavit během her + Protokoly + Výstup Syncthingu pro tuto relaci + Sdílet + Syncthing + Nejprve nastavte běhové prostředí Linuxu. + Přijmout + Uložit „%1$s“ do + Přidat složku + Synchronizovat složku v úložišti DroidDecku + Vybrat + Přidat + Složku se nepodařilo přidat. + Sdílet se + Zatím žádná zařízení. Tuto složku můžete sdílet později z webového rozhraní. + Použít tuto složku + Nová složka + Domovská složka kontejneru + Složka Sync + Sdílené úložiště DroidDecku + DroidDeck na %1$s + Data aplikace DroidDeck + Nové složky se ukládají do složky Sync v DroidDecku. Chcete-li synchronizovat jinou složku DroidDecku, použijte Přidat složku na kartě Syncthing. + Syncthing + Načíst znovu + Syncthing neběží. Steam se zastavil na neplatné instrukci (SIGILL). Může jít o problém s kompatibilitou instrukcí CPU nebo o pád klienta. Sdílejte protokoly s údaji o zařízení. Můžete zkusit Nastavení Steamu → Klient Steam → x86-64 (experimentální). Vyžaduje další stahování a může běžet pomaleji. Grafický ovladač Linuxu nenašel použitelné zařízení Vulkan. Zkontrolujte, zda vybraný ovladač GPU pro Linux toto GPU podporuje. diff --git a/app/src/main/res/values-da/gestures.xml b/app/src/main/res/values-da/gestures.xml index 1fd863e0d..022f14b97 100644 --- a/app/src/main/res/values-da/gestures.xml +++ b/app/src/main/res/values-da/gestures.xml @@ -71,7 +71,7 @@ Tilbage Enter Tab - Space + Mellemrum Delete Insert Home diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index eb774419e..fda22db68 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -1597,7 +1597,7 @@ Ctrl Super Alt - Space + Mellemrum AltGr Tab %1$s ✓ @@ -1776,6 +1776,93 @@ Tastatur + trackpad Terminal Deck-greb + trackpads + Dashboard + Intet at vise her endnu. + Downloads + Ingen downloads i gang. + Steam + Downloader shader-cache + På pause + Venter på at starte + Notifikationer + DroidDecks nederste skærm + Vis beskeder og andre notifikationer her, mens du spiller. De bliver kun på denne skærm og gemmes ikke. Android spørger én gang: siden for notifikationsadgang åbnes på denne skærm; slå DroidDeck til der, så vender denne side selv tilbage. + Tillad notifikationsadgang + Ingen notifikationer. + %1$d notifikationer + Ryd + Skærmbilleder + FPS kræver, at HUD er slået til + Hastighed + Pedaler + G-kraft + Tider + Bedste omgang + Dæk °C + Speeder og bremse, seneste 10 s + Hastighed, %1$s + Gear + Hastighedsenheder + Auto (%1$s) + Steam-klient ARM + Steam-klient x86 + Speeder + Bremse + Kontrollamper + Gearskift + Håndbremse + Pitbegrænser + TC + ABS + Turbo %1$s bar + Motor %1$s °C + Olie %1$s °C + Energifordeler + Brændstof, t + Hoved %1$s t + Reserve %1$s t + Tryk på den øverste bjælke eller instrumentbrættet for at skifte stil. + Send instrumentbrætdata + Auto + Slår det til for alle spil på listen her, også spil, der findes senere. Spil, du slår fra, forbliver slået fra. + Dette ændrer spillets egen indstilling for dataoutput. Det træder i kraft, næste gang spillet starter; slås det fra, gendannes den gamle indstilling. + Luk %1$s for at ændre dette. + Kunne ikke ændre indstillingerne for %1$s. + %1$s: slå %2$s til i spillets indstillinger med IP 127.0.0.1 og port %3$d. + Gemmes i Pictures/DroidDeck. Steam-skærmbilleder vises også her. + Tag skærmbillede + Skærmbillede gemt. + Kunne ikke tage et skærmbillede. + Ingen skærmbilleder endnu. + Chat og stemme + Læs og besvar Steam-chat her, mens spillet fortsætter på hovedskærmen. Den bruger den konto, Steam er logget ind med. + Steam har endnu ikke et weblogin at dele, så log ind her én gang. + Android lukkede chatsiden, mens DroidDeck var i baggrunden. Den åbner igen, når du vender tilbage til DroidDeck. + Åbn Steam Chat + Luk chat + Mikrofon til + Mikrofon fra + Mikrofonen er slået fra for dette spil. For at tale skal du slå Mikrofon til i DroidDecks Steam-indstillinger (Lyd) og starte spillet igen. + Lydstyrke %1$d%% + Statistik i spillet + FPS + CPU + GPU + Batteri + Omgang + Placering + Denne omgang + Sidste omgang + Brændstof + Last + Energi SYS · ENG · WEP + Juridisk status + Saldo + War Thunder + Elite Dangerous + Slå dataoutput til i et racerspil (ofte kaldet telemetri eller UDP) for at se et live-instrumentbræt her. Det forbliver på denne enhed. + Port %1$d modtager data (%2$d pakker á %3$d bytes), men i et format, som denne side endnu ikke kan læse. + Et andet program bruger allerede port %1$s, så det spil kan ikke vises her. Fjerner Linux-runtime @@ -2635,7 +2722,7 @@ Vis kun spillet på brillen. Fra: telefonen viser det, og Android spejler telefonen. Skærm Opdateringshastighed - 3D (side-by-side) + 3D (side om side) Skift smartbrillerne til side-om-side 3D. Hovedsporing styres separat. 3D-signalet blev ikke bekræftet. Prøv igen, når 2D-billedet er tilbage. Intet billede: brillen er i en tilstand, som telefonen ikke kan sende. Hold dens 2D/3D-knap inde, eller tag stikket ud og sæt det i igen. @@ -3248,6 +3335,113 @@ Gemte visninger kan genberegnes. Start en ny kalibrering for at fange i det aktuelle sporingskoordinatsystem. Håndmodel kontrolsum uoverensstemmelse OpenCV kunne ikke indlæse + Syncthing + Syncthing + Synkroniser med Syncthing + Baseret på Syncthing-Fork er dette en Syncthing-implementering, der lader dig synkronisere filer i DroidDecks interne filsystem med andre Syncthing-instanser. Det gælder kun filer, du gemmer i DroidDecks interne filsystem. + Denne build indeholder ikke Syncthing. + Starter Syncthing + Starter + Opdateret + Synkroniserer + Sat på pause + Sætter på pause + Genoptager + Kræver opmærksomhed + Syncthing er stoppet + Prøv igen + På pause indtil Wi-Fi + På pause under spillet + %1$d mapper · %2$d af %3$d enheder forbundet + %1$s tilbage + Åbn webgrænsefladen + Sæt på pause + Genoptag + Kører + Slå fra for at stoppe Syncthing helt. + Denne enhed + Enheds-ID + Vis QR + Avanceret + Porte, hvornår der køres, nulstilling + Åbn + Venter på dig + %1$s vil gerne forbinde + %1$s vil gerne dele »%2$s« + Accepter for at vælge, hvor den gemmes i DroidDeck. + Gennemse i webgrænsefladen + Mapper + Ingen mapper endnu. + %1$s · %2$s + Opdateret + Synkroniserer %1$d%% + Scanner + Sat på pause + Ikke synkroniseret + %1$d filer kunne ikke synkroniseres + Blokeret + Denne mappe ligger uden for DroidDecks lager, så DroidDeck har sat den på pause. Brug appen Syncthing-Fork til at synkronisere mapper uden for DroidDeck. + Fjern fra Syncthing + Fjernelse sletter ingen filer. + Enheder + Ingen enheder endnu. + Forbundet + Offline + Sidst set %1$s + Denne enheds ID + Tilføj en enhed i Syncthing på din anden enhed, og scan denne kode, eller indsæt ID\'et. Accepter den derefter her i webgrænsefladen. + Kopiér + Del + Færdig + Slå Syncthing fra? + Syncthing stopper nu og forbliver stoppet, indtil det slås til igen. Synkroniserede filer bliver, hvor de er. + Slet også Syncthings konfiguration + Fjerner denne enheds ID, mapper og enheder. Når det slås til igen, starter det forfra. + Slå fra + Porte + Synkronisering + Lokal opdagelse + Vælg nye porte + Kontrolleres igen ved hver start. + Vælg igen + Nulstil + API-nøgle + Generér ny + Nulstil Syncthing + Nyt enheds-ID, mapper og enheder fjernes. Filer i DroidDecks lager bevares. + Nulstil + Nulstil Syncthing? + Kørsel + Kør + Mens DroidDeck er åben + I baggrunden + Kun Wi-Fi + Sæt på pause under spil + Logfiler + Syncthing-output for denne session + Del + Syncthing + Konfigurer Linux-runtimen først. + Accepter + Gem »%1$s« i + Tilføj mappe + Synkroniser en mappe i DroidDecks lager + Vælg + Tilføj + Mappen kunne ikke tilføjes. + Del med + Ingen enheder endnu. Du kan dele mappen fra webgrænsefladen senere. + Brug denne mappe + Ny mappe + Containerens hjemmemappe + Sync-mappe + DroidDecks delte lager + DroidDeck på %1$s + DroidDecks appdata + Nye mapper gemmes i DroidDecks Sync-mappe. Brug Tilføj mappe på fanen Syncthing for at synkronisere en anden DroidDeck-mappe. + Syncthing + Genindlæs + Syncthing kører ikke. Steam stoppede med en ulovlig instruktion (SIGILL). Det kan være et kompatibilitetsproblem med CPU-instruktioner eller et klientnedbrud. Del logs med oplysninger om din enhed. Du kan prøve Steam-indstillinger → Steam-klient → x86-64 (eksperimentel). Det kræver en ekstra download og kan køre langsommere. Linux-grafikdriveren kunne ikke finde en brugbar Vulkan-enhed. Tjek, at den valgte Linux-GPU-driver understøtter denne GPU. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 559a2f727..05094d7eb 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1768,6 +1768,93 @@ Tastatur + Trackpad Terminal Deck-Griffe + Trackpads + Dashboard + Hier gibt es noch nichts anzuzeigen. + Downloads + Keine laufenden Downloads. + Steam + Shader-Cache wird heruntergeladen + Pausiert + Wartet auf Start + Benachrichtigungen + DroidDeck – unterer Bildschirm + Zeigt Nachrichten und andere Benachrichtigungen hier an, während du spielst. Sie bleiben nur auf diesem Bildschirm und werden nicht gespeichert. Android fragt einmal: Die Seite für den Benachrichtigungszugriff öffnet sich auf diesem Bildschirm; aktiviere dort DroidDeck, dann kehrt diese Seite von selbst zurück. + Benachrichtigungszugriff erlauben + Keine Benachrichtigungen. + %1$d Benachrichtigungen + Löschen + Screenshots + FPS benötigt das aktive HUD + Geschwindigkeit + Pedale + G-Kraft + Zeiten + Beste Runde + Reifen °C + Gas und Bremse, letzte 10 s + Geschwindigkeit, %1$s + Gang + Geschwindigkeitseinheit + Automatisch (%1$s) + Steam-Client ARM + Steam-Client x86 + Gas + Bremse + Kontrollleuchten + Schalten + Handbremse + Pit-Limiter + TC + ABS + Turbo %1$s bar + Motor %1$s °C + Öl %1$s °C + Energieverteiler + Treibstoff, t + Haupttank %1$s t + Reserve %1$s t + Tippe auf die obere Leiste oder die Instrumentenanzeige, um ihren Stil zu ändern. + Dashboard-Daten senden + Automatisch + Schaltet es für jedes hier aufgeführte Spiel ein, auch für später gefundene Spiele. Spiele, die du ausschaltest, bleiben aus. + Dies ändert die eigene Datenausgabe-Einstellung des Spiels. Es wirkt beim nächsten Start des Spiels; beim Ausschalten wird die alte Einstellung wiederhergestellt. + Schließe %1$s, um dies zu ändern. + Die Einstellungen von %1$s konnten nicht geändert werden. + %1$s: Aktiviere %2$s in den Spieleinstellungen mit IP 127.0.0.1 und Port %3$d. + Gespeichert in Pictures/DroidDeck. Steam-Screenshots werden hier ebenfalls angezeigt. + Screenshot aufnehmen + Screenshot gespeichert. + Screenshot konnte nicht aufgenommen werden. + Noch keine Screenshots. + Chat & Sprache + Lies und beantworte hier den Steam-Chat, während das Spiel auf dem Hauptbildschirm weiterläuft. Es wird das Konto verwendet, mit dem Steam angemeldet ist. + Steam hat noch keine Web-Anmeldung zum Teilen, melde dich also einmal hier an. + Android hat die Chat-Seite geschlossen, während DroidDeck im Hintergrund war. Sie öffnet sich wieder, wenn du zu DroidDeck zurückkehrst. + Steam Chat öffnen + Chat schließen + Mikro an + Mikro aus + Das Mikro ist für dieses Spiel aus. Um zu sprechen, aktiviere „Mikrofon“ in den Steam-Einstellungen von DroidDeck (Audio) und starte das Spiel neu. + Lautstärke %1$d%% + Spielstatistiken + FPS + CPU + GPU + Akku + Runde + Position + Aktuelle Runde + Letzte Runde + Treibstoff + Fracht + Pips SYS · ENG · WEP + Rechtsstatus + Guthaben + War Thunder + Elite Dangerous + Aktiviere die Datenausgabe eines Rennspiels (oft Telemetrie oder UDP genannt), um hier ein Live-Cockpit zu sehen. Die Daten bleiben auf diesem Gerät. + Port %1$d empfängt Daten (%2$d Pakete mit %3$d Bytes), aber in einem Format, das diese Seite noch nicht lesen kann. + Ein anderes Programm verwendet bereits Port %1$s, daher kann dieses Spiel hier nicht angezeigt werden. Linux-Laufzeit wird entfernt @@ -3248,6 +3335,113 @@ Gespeicherte Ansichten können neu berechnet werden. Starten Sie eine neue Kalibrierung zur Erfassung im aktuellen Tracking-Koordinatensystem. Die Prüfsumme des Handmodells stimmt nicht überein OpenCV konnte nicht geladen werden + Syncthing + Syncthing + Mit Syncthing synchronisieren + Basiert auf Syncthing-Fork: eine Syncthing-Implementierung, mit der du Dateien im internen Dateisystem von DroidDeck mit anderen Syncthing-Instanzen synchronisieren kannst. Gilt nur für Dateien, die du im internen Dateisystem von DroidDeck speicherst. + Dieser Build enthält kein Syncthing. + Syncthing wird gestartet + Wird gestartet + Aktuell + Synchronisiert + Pausiert + Wird pausiert + Wird fortgesetzt + Benötigt Aufmerksamkeit + Syncthing gestoppt + Erneut versuchen + Pausiert, bis Wi-Fi verfügbar ist + Während des Spiels pausiert + %1$d Ordner · %2$d von %3$d Geräten verbunden + Noch %1$s + Web-GUI öffnen + Pausieren + Fortsetzen + Aktiv + Ausschalten, um Syncthing vollständig zu beenden. + Dieses Gerät + Geräte-ID + QR-Code anzeigen + Erweitert + Ports, Ausführung, Zurücksetzen + Öffnen + Wartet auf dich + %1$s möchte sich verbinden + %1$s möchte „%2$s“ teilen + Annehmen, um zu wählen, wo es in DroidDeck gespeichert wird. + In der Web-GUI prüfen + Ordner + Noch keine Ordner. + %1$s · %2$s + Aktuell + Synchronisiere %1$d %% + Scanne + Pausiert + Nicht synchron + %1$d Dateien konnten nicht synchronisiert werden + Blockiert + Dieser Ordner liegt außerhalb des DroidDeck-Speichers, daher hat DroidDeck ihn pausiert. Um Ordner außerhalb von DroidDeck zu synchronisieren, nutze die App Syncthing-Fork. + Aus Syncthing entfernen + Beim Entfernen werden keine Dateien gelöscht. + Geräte + Noch keine Geräte. + Verbunden + Offline + Zuletzt gesehen %1$s + ID dieses Geräts + Füge auf deinem anderen Gerät in Syncthing ein Gerät hinzu und scanne diesen Code oder füge die ID ein. Nimm es dann hier in der Web-GUI an. + Kopieren + Teilen + Fertig + Syncthing ausschalten? + Syncthing wird jetzt beendet und bleibt aus, bis du es wieder einschaltest. Synchronisierte Dateien bleiben, wo sie sind. + Auch die Syncthing-Konfiguration löschen + Entfernt die ID dieses Geräts, Ordner und Geräte. Beim erneuten Einschalten beginnt alles von vorn. + Ausschalten + Ports + Synchronisierung + Lokale Gerätesuche + Neue Ports wählen + Wird bei jedem Start erneut geprüft. + Neu wählen + Zurücksetzen + API-Schlüssel + Neu generieren + Syncthing zurücksetzen + Neue Geräte-ID, Ordner und Geräte werden entfernt. Dateien im DroidDeck-Speicher bleiben erhalten. + Zurücksetzen + Syncthing zurücksetzen? + Ausführung + Ausführen + Während DroidDeck geöffnet ist + Im Hintergrund + Nur über Wi-Fi + Während Spielen pausieren + Logs + Syncthing-Ausgabe dieser Sitzung + Teilen + Syncthing + Richte zuerst die Linux-Laufzeit ein. + Annehmen + „%1$s“ speichern in + Ordner hinzufügen + Einen Ordner im DroidDeck-Speicher synchronisieren + Auswählen + Hinzufügen + Der Ordner konnte nicht hinzugefügt werden. + Teilen mit + Noch keine Geräte. Du kannst diesen Ordner später über die Web-GUI teilen. + Diesen Ordner verwenden + Neuer Ordner + Home-Verzeichnis des Containers + Sync-Ordner + Gemeinsamer DroidDeck-Speicher + DroidDeck auf %1$s + DroidDeck-App-Daten + Neue Ordner werden im Sync-Ordner von DroidDeck gespeichert. Um einen anderen DroidDeck-Ordner zu synchronisieren, nutze „Ordner hinzufügen“ im Tab „Syncthing“. + Syncthing + Neu laden + Syncthing läuft nicht. Steam wurde mit einer ungültigen Anweisung (SIGILL) beendet. Das kann ein Kompatibilitätsproblem mit CPU-Befehlen oder ein Absturz des Clients sein. Teile die Logs zusammen mit den Details deines Geräts. Du kannst Steam-Einstellungen → Steam-Client → x86-64 (experimentell) ausprobieren. Das erfordert einen zusätzlichen Download und kann langsamer laufen. Der Linux-Grafiktreiber hat kein nutzbares Vulkan-Gerät gefunden. Prüfe, ob der ausgewählte Linux-GPU-Treiber diese GPU unterstützt. diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 739608d37..48814a461 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -1776,6 +1776,93 @@ Πληκτρολόγιο + πινακίδα αφής Τερματικό Λαβές Deck + πινακίδες αφής + Πίνακας ελέγχου + Δεν υπάρχει τίποτα για εμφάνιση ακόμα. + Λήψεις + Δεν εκτελούνται λήψεις. + Steam + Λήψη cache shader + Σε παύση + Αναμονή για έναρξη + Ειδοποιήσεις + DroidDeck – κάτω οθόνη + Εμφάνιση μηνυμάτων και άλλων ειδοποιήσεων εδώ ενώ παίζετε. Παραμένουν μόνο σε αυτή την οθόνη και δεν αποθηκεύονται. Το Android ρωτά μία φορά: η σελίδα πρόσβασης ειδοποιήσεων ανοίγει σε αυτή την οθόνη· ενεργοποιήστε εκεί το DroidDeck και αυτή η σελίδα επιστρέφει μόνη της. + Αποδοχή πρόσβασης ειδοποιήσεων + Δεν υπάρχουν ειδοποιήσεις. + %1$d ειδοποιήσεις + Απαλοιφή + Στιγμιότυπα οθόνης + Τα FPS απαιτούν ενεργό HUD + Ταχύτητα + Πεντάλ + Δύναμη G + Χρονομέτρηση + Καλύτερος γύρος + Ελαστικά °C + Γκάζι και φρένο, τελευταία 10 s + Ταχύτητα, %1$s + Σχέση + Μονάδες ταχύτητας + Αυτόματα (%1$s) + Πελάτης Steam ARM + Πελάτης Steam x86 + Γκάζι + Φρένο + Ενδεικτικές λυχνίες + Αλλαγή + Χειρόφρενο + Περιοριστής pit + TC + ABS + Turbo %1$s bar + Κινητήρας %1$s °C + Λάδι %1$s °C + Διανομέας ισχύος + Καύσιμο, t + Κύριο %1$s t + Εφεδρικό %1$s t + Πατήστε τη λωρίδα στο πάνω μέρος ή τον πίνακα οργάνων για να αλλάξετε το στυλ του. + Αποστολή δεδομένων πίνακα + Αυτόματα + Το ενεργοποιεί για κάθε παιχνίδι της λίστας, μαζί και όσα βρεθούν αργότερα. Τα παιχνίδια που απενεργοποιείτε παραμένουν ανενεργά. + Αυτό αλλάζει τη ρύθμιση εξόδου δεδομένων του ίδιου του παιχνιδιού. Εφαρμόζεται την επόμενη φορά που ξεκινά το παιχνίδι· η απενεργοποίηση επαναφέρει την προηγούμενη ρύθμιση. + Κλείστε το %1$s για να το αλλάξετε. + Δεν ήταν δυνατή η αλλαγή των ρυθμίσεων του %1$s. + %1$s: ενεργοποιήστε το %2$s στις ρυθμίσεις του παιχνιδιού με IP 127.0.0.1 και θύρα %3$d. + Αποθηκεύονται στο Pictures/DroidDeck. Εδώ εμφανίζονται και τα στιγμιότυπα του Steam. + Λήψη στιγμιότυπου + Το στιγμιότυπο αποθηκεύτηκε. + Δεν ήταν δυνατή η λήψη στιγμιότυπου. + Δεν υπάρχουν στιγμιότυπα ακόμα. + Συνομιλία & φωνή + Διαβάστε και απαντήστε στη συνομιλία του Steam εδώ ενώ το παιχνίδι συνεχίζει στην κύρια οθόνη. Χρησιμοποιεί τον λογαριασμό με τον οποίο είναι συνδεδεμένο το Steam. + Το Steam δεν έχει ακόμα σύνδεση web για κοινή χρήση, οπότε συνδεθείτε εδώ μία φορά. + Το Android έκλεισε τη σελίδα συνομιλίας ενώ το DroidDeck ήταν στο παρασκήνιο. Ανοίγει ξανά όταν επιστρέψετε στο DroidDeck. + Άνοιγμα Steam Chat + Κλείσιμο συνομιλίας + Μικρόφωνο ανοιχτό + Μικρόφωνο κλειστό + Το μικρόφωνο είναι απενεργοποιημένο για αυτό το παιχνίδι. Για να μιλήσετε, ενεργοποιήστε το Μικρόφωνο στις ρυθμίσεις Steam του DroidDeck (Ήχος) και ξεκινήστε ξανά το παιχνίδι. + Ένταση %1$d%% + Στατιστικά παιχνιδιού + FPS + CPU + GPU + Μπαταρία + Γύρος + Θέση + Τρέχων γύρος + Προηγούμενος γύρος + Καύσιμο + Φορτίο + Pips SYS · ENG · WEP + Νομική κατάσταση + Υπόλοιπο + War Thunder + Elite Dangerous + Ενεργοποιήστε την έξοδο δεδομένων ενός αγωνιστικού παιχνιδιού (συχνά λέγεται τηλεμετρία ή UDP) για να δείτε εδώ ζωντανό πίνακα οργάνων. Τα δεδομένα μένουν σε αυτή τη συσκευή. + Η θύρα %1$d λαμβάνει δεδομένα (%2$d πακέτα των %3$d byte), αλλά σε μορφή που αυτή η σελίδα δεν μπορεί ακόμα να διαβάσει. + Ένα άλλο πρόγραμμα χρησιμοποιεί ήδη τη θύρα %1$s, οπότε αυτό το παιχνίδι δεν μπορεί να εμφανιστεί εδώ. Κατάργηση του Linux runtime @@ -2196,7 +2283,7 @@ Σε προσφορά Φόρτωση περισσότερων Οποιαδήποτε πλατφόρμα - Mobile web + Web για κινητά Προσθήκη ετικέτας Φιλτράρισμα ανά ετικέτα Δεν ήταν δυνατή η φόρτωση παιχνιδιών από το itch.io @@ -3248,6 +3335,113 @@ Οι αποθηκευμένες προβολές μπορούν να υπολογιστούν εκ νέου. Ξεκινήστε μια νέα βαθμονόμηση για αποτύπωση στο τρέχον σύστημα συντεταγμένων παρακολούθησης. Αναντιστοιχία αθροίσματος ελέγχου μοντέλου χεριού Δεν ήταν δυνατή η φόρτωση του OpenCV + Syncthing + Syncthing + Συγχρονισμός με Syncthing + Βασίζεται στο Syncthing-Fork: μια υλοποίηση του Syncthing που σας επιτρέπει να συγχρονίζετε αρχεία του εσωτερικού συστήματος αρχείων του DroidDeck με άλλες εγκαταστάσεις Syncthing. Αφορά μόνο αρχεία που αποθηκεύετε στο εσωτερικό σύστημα αρχείων του DroidDeck. + Αυτή η έκδοση δεν περιλαμβάνει το Syncthing. + Εκκίνηση του Syncthing + Εκκίνηση + Ενημερωμένο + Συγχρονισμός + Σε παύση + Γίνεται παύση + Γίνεται συνέχιση + Χρειάζεται προσοχή + Το Syncthing σταμάτησε + Δοκιμάστε ξανά + Σε παύση έως ότου υπάρξει Wi-Fi + Σε παύση κατά τη διάρκεια του παιχνιδιού + %1$d φάκελοι · %2$d από %3$d συσκευές συνδεδεμένες + Απομένουν %1$s + Άνοιγμα διεπαφής web + Παύση + Συνέχιση + Σε λειτουργία + Απενεργοποιήστε το για να σταματήσει εντελώς το Syncthing. + Αυτή η συσκευή + Ταυτότητα συσκευής + Εμφάνιση QR + Για προχωρημένους + Θύρες, πότε εκτελείται, επαναφορά + Άνοιγμα + Σας περιμένει + Η συσκευή %1$s θέλει να συνδεθεί + Η συσκευή %1$s θέλει να μοιραστεί τον φάκελο «%2$s» + Αποδεχτείτε για να επιλέξετε πού θα αποθηκευτεί στο DroidDeck. + Έλεγχος στη διεπαφή web + Φάκελοι + Δεν υπάρχουν ακόμα φάκελοι. + %1$s · %2$s + Ενημερωμένος + Συγχρονισμός %1$d%% + Σάρωση + Σε παύση + Μη συγχρονισμένος + Δεν ήταν δυνατός ο συγχρονισμός %1$d αρχείων + Αποκλεισμένος + Αυτός ο φάκελος βρίσκεται εκτός του αποθηκευτικού χώρου του DroidDeck, γι\' αυτό το DroidDeck τον έθεσε σε παύση. Για να συγχρονίσετε φακέλους εκτός DroidDeck, χρησιμοποιήστε την εφαρμογή Syncthing-Fork. + Αφαίρεση από το Syncthing + Η αφαίρεση δεν διαγράφει κανένα αρχείο. + Συσκευές + Δεν υπάρχουν ακόμα συσκευές. + Συνδεδεμένη + Εκτός σύνδεσης + Τελευταία εμφάνιση %1$s + Ταυτότητα αυτής της συσκευής + Στην άλλη σας συσκευή, προσθέστε μια συσκευή στο Syncthing και σαρώστε αυτόν τον κωδικό ή επικολλήστε την ταυτότητα. Στη συνέχεια, αποδεχτείτε την εδώ στη διεπαφή web. + Αντιγραφή + Κοινή χρήση + Έγινε + Απενεργοποίηση του Syncthing; + Το Syncthing σταματά τώρα και παραμένει σταματημένο μέχρι να το ενεργοποιήσετε ξανά. Τα συγχρονισμένα αρχεία παραμένουν εκεί που βρίσκονται. + Διαγραφή και των ρυθμίσεων του Syncthing + Αφαιρεί την ταυτότητα αυτής της συσκευής, τους φακέλους και τις συσκευές. Με την επανενεργοποίηση ξεκινά από την αρχή. + Απενεργοποίηση + Θύρες + Συγχρονισμός + Τοπική ανακάλυψη + Επιλογή νέων θυρών + Ελέγχεται ξανά σε κάθε εκκίνηση. + Νέα επιλογή + Επαναφορά + Κλειδί API + Αναδημιουργία + Επαναφορά του Syncthing + Νέα ταυτότητα συσκευής, αφαίρεση φακέλων και συσκευών. Τα αρχεία στον αποθηκευτικό χώρο του DroidDeck διατηρούνται. + Επαναφορά + Επαναφορά του Syncthing; + Λειτουργία + Εκτέλεση + Όσο το DroidDeck είναι ανοιχτό + Στο παρασκήνιο + Μόνο μέσω Wi-Fi + Παύση κατά τη διάρκεια παιχνιδιών + Αρχεία καταγραφής + Έξοδος του Syncthing για αυτή τη συνεδρία + Κοινή χρήση + Syncthing + Ρυθμίστε πρώτα το runtime Linux. + Αποδοχή + Αποθήκευση του «%1$s» σε + Προσθήκη φακέλου + Συγχρονισμός ενός φακέλου στον αποθηκευτικό χώρο του DroidDeck + Επιλογή + Προσθήκη + Δεν ήταν δυνατή η προσθήκη του φακέλου. + Κοινή χρήση με + Δεν υπάρχουν ακόμα συσκευές. Μπορείτε να μοιραστείτε αυτόν τον φάκελο αργότερα από τη διεπαφή web. + Χρήση αυτού του φακέλου + Νέος φάκελος + Αρχικός φάκελος του container + Φάκελος συγχρονισμού + Κοινόχρηστος χώρος DroidDeck + DroidDeck σε %1$s + Δεδομένα εφαρμογής DroidDeck + Οι νέοι φάκελοι αποθηκεύονται στον φάκελο συγχρονισμού του DroidDeck. Για να συγχρονίσετε άλλον φάκελο του DroidDeck, χρησιμοποιήστε την Προσθήκη φακέλου στην καρτέλα Syncthing. + Syncthing + Επαναφόρτωση + Το Syncthing δεν εκτελείται. Το Steam σταμάτησε με μη έγκυρη εντολή (SIGILL). Μπορεί να είναι πρόβλημα συμβατότητας εντολών CPU ή κατάρρευση του πελάτη. Μοιραστείτε τα αρχεία καταγραφής με τα στοιχεία της συσκευής σας. Μπορείτε να δοκιμάσετε Ρυθμίσεις Steam → Πελάτης Steam → x86-64 (πειραματικό). Απαιτεί επιπλέον λήψη και μπορεί να είναι πιο αργό. Το πρόγραμμα οδήγησης γραφικών Linux δεν βρήκε χρησιμοποιήσιμη συσκευή Vulkan. Ελέγξτε ότι το επιλεγμένο πρόγραμμα οδήγησης GPU Linux υποστηρίζει αυτή τη GPU. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 302930a53..05b2c8dd7 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1798,6 +1798,93 @@ Teclado + trackpad Terminal Agarres de Deck + trackpads + Panel + Todavía no hay nada que mostrar aquí. + Descargas + No hay descargas en curso. + Steam + Descargando caché de shaders + En pausa + Esperando para empezar + Notificaciones + DroidDeck – pantalla inferior + Muestra aquí los mensajes y otras notificaciones mientras juegas. Solo permanecen en esta pantalla y no se guardan. Android lo pregunta una vez: su página de acceso a notificaciones se abre en esta pantalla; activa DroidDeck ahí y esta página volverá sola. + Permitir acceso a notificaciones + No hay notificaciones. + %1$d notificaciones + Borrar + Capturas de pantalla + Los FPS requieren el HUD activado + Velocidad + Pedales + Fuerza G + Tiempos + Mejor vuelta + Neumáticos °C + Acelerador y freno, últimos 10 s + Velocidad, %1$s + Marcha + Unidades de velocidad + Automático (%1$s) + Cliente de Steam ARM + Cliente de Steam x86 + Acelerador + Freno + Testigos + Cambio + Freno de mano + Limitador de pit + TC + ABS + Turbo %1$s bar + Motor %1$s °C + Aceite %1$s °C + Distribuidor de energía + Combustible, t + Principal %1$s t + Reserva %1$s t + Toca la franja superior o el cuadro de instrumentos para cambiar su estilo. + Enviar datos al panel + Automático + Lo activa en todos los juegos de esta lista, incluidos los que se encuentren más adelante. Los juegos que desactives seguirán desactivados. + Esto cambia el ajuste de salida de datos del propio juego. Se aplica la próxima vez que se inicie el juego; al desactivarlo se restaura el ajuste anterior. + Cierra %1$s para cambiar esto. + No se han podido cambiar los ajustes de %1$s. + %1$s: activa %2$s en los ajustes del juego con la IP 127.0.0.1 y el puerto %3$d. + Se guardan en Pictures/DroidDeck. Las capturas de Steam también aparecen aquí. + Hacer captura de pantalla + Captura de pantalla guardada. + No se ha podido hacer la captura de pantalla. + Todavía no hay capturas de pantalla. + Chat y voz + Lee y responde el chat de Steam aquí mientras el juego sigue en la pantalla principal. Usa la cuenta con la que se ha iniciado sesión en Steam. + Steam todavía no tiene una sesión web que compartir, así que inicia sesión aquí una vez. + Android cerró la página del chat mientras DroidDeck estaba en segundo plano. Se volverá a abrir cuando regreses a DroidDeck. + Abrir Steam Chat + Cerrar chat + Micro activado + Micro desactivado + El micro está desactivado en este juego. Para hablar, activa Micrófono en los ajustes de Steam de DroidDeck (Audio) y vuelve a iniciar el juego. + Volumen %1$d%% + Estadísticas del juego + FPS + CPU + GPU + Batería + Vuelta + Posición + Vuelta actual + Última vuelta + Combustible + Carga + Pips SYS · ENG · WEP + Estado legal + Saldo + War Thunder + Elite Dangerous + Activa la salida de datos de un juego de carreras (a menudo llamada telemetría o UDP) para ver aquí un panel en directo. Los datos no salen de este dispositivo. + El puerto %1$d recibe datos (%2$d paquetes de %3$d bytes), pero en un formato que esta página aún no puede leer. + Otro programa ya usa el puerto %1$s, así que ese juego no se puede mostrar aquí. Eliminando el runtime de Linux @@ -3284,6 +3371,113 @@ Las vistas guardadas se pueden recalcular. Inicie una nueva calibración para capturar en el sistema de coordenadas de seguimiento actual. La suma de comprobación del modelo de mano no coincide OpenCV no se pudo cargar + Syncthing + Syncthing + Sincronizar con Syncthing + Basada en Syncthing-Fork, es una implementación de Syncthing que te permite sincronizar archivos del sistema de archivos interno de DroidDeck con otras instancias de Syncthing. Solo sirve para los archivos que guardas en el sistema de archivos interno de DroidDeck. + Esta compilación no incluye Syncthing. + Iniciando Syncthing + Iniciando + Actualizado + Sincronizando + En pausa + Pausando + Reanudando + Requiere atención + Syncthing se detuvo + Reintentar + En pausa hasta tener Wi-Fi + En pausa durante el juego + %1$d carpetas · %2$d de %3$d dispositivos conectados + Faltan %1$s + Abrir la interfaz web + Pausar + Reanudar + En ejecución + Desactívalo para detener Syncthing por completo. + Este dispositivo + ID del dispositivo + Mostrar QR + Avanzado + Puertos, cuándo se ejecuta, restablecer + Abrir + Esperando tu respuesta + %1$s quiere conectarse + %1$s quiere compartir “%2$s” + Acepta para elegir dónde se guarda en DroidDeck. + Revisar en la interfaz web + Carpetas + Aún no hay carpetas. + %1$s · %2$s + Actualizada + Sincronizando %1$d%% + Escaneando + En pausa + No sincronizada + No se pudieron sincronizar %1$d archivos + Bloqueada + Esta carpeta está fuera del almacenamiento de DroidDeck, así que DroidDeck la puso en pausa. Para sincronizar carpetas fuera de DroidDeck, usa la app Syncthing-Fork. + Quitar de Syncthing + Quitarla no borra ningún archivo. + Dispositivos + Aún no hay dispositivos. + Conectado + Sin conexión + Visto por última vez %1$s + ID de este dispositivo + En tu otro dispositivo, añade un dispositivo en Syncthing y escanea este código o pega el ID. Después, acéptalo aquí en la interfaz web. + Copiar + Compartir + Hecho + ¿Desactivar Syncthing? + Syncthing se detiene ahora y seguirá detenido hasta que vuelvas a activarlo. Los archivos sincronizados se quedan donde están. + Borrar también la configuración de Syncthing + Quita el ID de este dispositivo, las carpetas y los dispositivos. Al volver a activarlo, empieza desde cero. + Desactivar + Puertos + Sincronización + Descubrimiento local + Elegir puertos nuevos + Se vuelve a comprobar en cada inicio. + Volver a elegir + Restablecer + Clave API + Regenerar + Restablecer Syncthing + ID de dispositivo nuevo; se quitan las carpetas y los dispositivos. Los archivos del almacenamiento de DroidDeck se conservan. + Restablecer + ¿Restablecer Syncthing? + Ejecución + Ejecutar + Mientras DroidDeck está abierto + En segundo plano + Solo con Wi-Fi + Pausar durante los juegos + Registros + Salida de Syncthing de esta sesión + Compartir + Syncthing + Configura primero el runtime de Linux. + Aceptar + Guardar “%1$s” en + Añadir carpeta + Sincroniza una carpeta del almacenamiento de DroidDeck + Elegir + Añadir + No se pudo añadir la carpeta. + Compartir con + Aún no hay dispositivos. Puedes compartir esta carpeta más tarde desde la interfaz web. + Usar esta carpeta + Carpeta nueva + Carpeta personal del contenedor + Carpeta Sync + Almacenamiento compartido de DroidDeck + DroidDeck en %1$s + Datos de la app DroidDeck + Las carpetas nuevas se guardan en la carpeta Sync de DroidDeck. Para sincronizar otra carpeta de DroidDeck, usa Añadir carpeta en la pestaña Syncthing. + Syncthing + Recargar + Syncthing no se está ejecutando. Steam se detuvo por una instrucción ilegal (SIGILL). Puede ser un problema de compatibilidad con instrucciones de la CPU o un fallo del cliente. Comparte los registros con los datos de tu dispositivo. Puedes probar Ajustes de Steam → Cliente de Steam → x86-64 (experimental). Requiere una descarga adicional y puede ir más lento. El controlador gráfico de Linux no ha encontrado un dispositivo Vulkan utilizable. Comprueba que el controlador de GPU de Linux seleccionado sea compatible con esta GPU. diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index a6e509511..2fce2dfe4 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -1776,6 +1776,93 @@ Näppäimistö + kosketuslevy Pääte Deck-kahvat + kosketuslevyt + Kojelauta + Täällä ei ole vielä mitään näytettävää. + Lataukset + Latauksia ei ole käynnissä. + Steam + Ladataan sävytinvälimuistia + Keskeytetty + Odottaa alkamista + Ilmoitukset + DroidDeckin alanäyttö + Näytä viestit ja muut ilmoitukset täällä pelatessasi. Ne näkyvät vain tällä näytöllä, eikä niitä tallenneta. Android kysyy kerran: sen ilmoitusten käyttöoikeussivu avautuu tälle näytölle; kytke DroidDeck siellä päälle, niin tämä sivu palaa itsestään. + Salli ilmoitusten käyttö + Ei ilmoituksia. + Ilmoituksia: %1$d + Tyhjennä + Kuvakaappaukset + FPS vaatii HUD:n päälle + Nopeus + Polkimet + G-voima + Ajanotto + Paras kierros + Renkaat °C + Kaasu ja jarru, viimeiset 10 s + Nopeus, %1$s + Vaihde + Nopeuden yksiköt + Automaattinen (%1$s) + Steam-asiakasohjelma ARM + Steam-asiakasohjelma x86 + Kaasu + Jarru + Kojelauta + Vaihto + Käsijarru + Varikkorajoitin + TC + ABS + Turbo %1$s bar + Moottori %1$s °C + Öljy %1$s °C + Tehonjakaja + Polttoaine, t + Päätankki %1$s t + Varatankki %1$s t + Napauta yläpalkkia tai kojelautaa vaihtaaksesi sen tyyliä. + Lähetä kojelaudan tiedot + Automaattinen + Kytkee sen päälle jokaiselle tässä luetellulle pelille, myös myöhemmin löytyville. Pois kytkemäsi pelit pysyvät pois päältä. + Tämä muuttaa pelin omaa datan ulostulon asetusta. Muutos tulee voimaan, kun peli käynnistetään seuraavan kerran; pois kytkeminen palauttaa vanhan asetuksen. + Sulje %1$s muuttaaksesi tätä. + Pelin %1$s asetuksia ei voitu muuttaa. + %1$s: kytke pelin asetuksista päälle %2$s IP-osoitteella 127.0.0.1 ja portilla %3$d. + Tallennetaan kansioon Pictures/DroidDeck. Myös Steamin kuvakaappaukset näkyvät täällä. + Ota kuvakaappaus + Kuvakaappaus tallennettu. + Kuvakaappausta ei voitu ottaa. + Ei vielä kuvakaappauksia. + Chat & ääni + Lue Steam-chattia ja vastaa siihen täällä, kun peli jatkuu päänäytöllä. Se käyttää tiliä, jolla Steamiin on kirjauduttu. + Steamilla ei ole vielä jaettavaa verkkokirjautumista, joten kirjaudu tässä kerran. + Android sulki chat-sivun, kun DroidDeck oli taustalla. Se avautuu uudelleen, kun palaat DroidDeckiin. + Avaa Steam Chat + Sulje chat + Mikrofoni päällä + Mikrofoni pois + Mikrofoni on pois päältä tässä pelissä. Jos haluat puhua, kytke Mikrofoni päälle DroidDeckin Steam-asetuksissa (Ääni) ja käynnistä peli uudelleen. + Äänenvoimakkuus %1$d %% + Pelinsisäiset tilastot + FPS + CPU + GPU + Akku + Kierros + Sijoitus + Tämä kierros + Edellinen kierros + Polttoaine + Lasti + Tehopisteet SYS · ENG · WEP + Oikeudellinen asema + Saldo + War Thunder + Elite Dangerous + Kytke kilpa-ajopelin datan ulostulo päälle (usein nimeltään telemetria tai UDP), niin näet täällä reaaliaikaisen kojelaudan. Tiedot pysyvät tällä laitteella. + Portti %1$d vastaanottaa dataa (%2$d pakettia, kukin %3$d tavua), mutta muodossa, jota tämä sivu ei vielä osaa lukea. + Toinen ohjelma käyttää jo porttia %1$s, joten peliä ei voida näyttää täällä. Poistetaan Linux-ajoympäristöä @@ -3248,6 +3335,113 @@ Tallennetut näkymät voidaan laskea uudelleen. Aloita uusi kalibrointi kaapataksesi nykyiseen seurantakoordinaattijärjestelmään. Käsimallin tarkistussumman epäsuhta OpenCV:tä ei voitu ladata + Syncthing + Syncthing + Synkronoi Syncthingillä + Tämä Syncthing-Forkiin perustuva Syncthing-toteutus synkronoi DroidDeckin sisäisen tiedostojärjestelmän tiedostoja muiden Syncthing-instanssien kanssa. Se koskee vain tiedostoja, joita säilytät DroidDeckin sisäisessä tiedostojärjestelmässä. + Tämä versio ei sisällä Syncthingiä. + Käynnistetään Syncthingiä + Käynnistyy + Ajan tasalla + Synkronoidaan + Keskeytetty + Keskeytetään + Jatketaan + Vaatii huomiota + Syncthing pysähtyi + Yritä uudelleen + Keskeytetty, kunnes Wi-Fi on käytettävissä + Keskeytetty pelin ajaksi + Kansioita %1$d · %2$d/%3$d laitetta yhdistetty + %1$s jäljellä + Avaa web-käyttöliittymä + Keskeytä + Jatka + Käynnissä + Poista käytöstä pysäyttääksesi Syncthingin kokonaan. + Tämä laite + Laitteen tunniste + Näytä QR-koodi + Lisäasetukset + Portit, milloin ajetaan, nollaus + Avaa + Odottaa sinua + %1$s haluaa muodostaa yhteyden + %1$s haluaa jakaa kansion “%2$s” + Hyväksy ja valitse, mihin se tallennetaan DroidDeckissä. + Tarkista web-käyttöliittymässä + Kansiot + Ei vielä kansioita. + %1$s · %2$s + Ajan tasalla + Synkronoidaan %1$d %% + Skannataan + Keskeytetty + Ei synkronoitu + %1$d tiedostoa ei voitu synkronoida + Estetty + Tämä kansio on DroidDeckin tallennustilan ulkopuolella, joten DroidDeck keskeytti sen. Käytä Syncthing-Fork-sovellusta DroidDeckin ulkopuolisten kansioiden synkronointiin. + Poista Syncthingistä + Poistaminen ei poista yhtään tiedostoa. + Laitteet + Ei vielä laitteita. + Yhdistetty + Offline-tilassa + Viimeksi nähty %1$s + Tämän laitteen tunniste + Lisää toisella laitteellasi laite Syncthingissä ja skannaa tämä koodi tai liitä tunniste. Hyväksy se sitten täällä web-käyttöliittymässä. + Kopioi + Jaa + Valmis + Poistetaanko Syncthing käytöstä? + Syncthing pysähtyy nyt ja pysyy pysäytettynä, kunnes otat sen uudelleen käyttöön. Synkronoidut tiedostot pysyvät paikoillaan. + Poista myös Syncthingin asetukset + Poistaa tämän laitteen tunnisteen, kansiot ja laitteet. Kun otat sen uudelleen käyttöön, se aloittaa alusta. + Poista käytöstä + Portit + Synkronointi + Paikallinen etsintä + Valitse uudet portit + Tarkistetaan uudelleen joka käynnistyksessä. + Valitse uudelleen + Nollaus + API-avain + Luo uudelleen + Nollaa Syncthing + Uusi laitteen tunniste, kansiot ja laitteet poistetaan. DroidDeckin tallennustilan tiedostot säilytetään. + Nollaa + Nollataanko Syncthing? + Suoritus + Käynnissä + Kun DroidDeck on auki + Taustalla + Vain Wi-Fi + Keskeytä pelien ajaksi + Lokit + Syncthingin tuloste tältä istunnolta + Jaa + Syncthing + Asenna Linux-ajoympäristö ensin. + Hyväksy + Tallenna “%1$s” kohteeseen + Lisää kansio + Synkronoi kansio DroidDeckin tallennustilasta + Valitse + Lisää + Kansiota ei voitu lisätä. + Jaa laitteille + Ei vielä laitteita. Voit jakaa tämän kansion myöhemmin web-käyttöliittymästä. + Käytä tätä kansiota + Uusi kansio + Kontin kotikansio + Sync-kansio + DroidDeckin jaettu tallennustila + DroidDeck kohteessa %1$s + DroidDeckin sovellustiedot + Uudet kansiot tallennetaan DroidDeckin Sync-kansioon. Jos haluat synkronoida toisen DroidDeck-kansion, käytä Syncthing-välilehden Lisää kansio -toimintoa. + Syncthing + Lataa uudelleen + Syncthing ei ole käynnissä. Steam pysähtyi virheelliseen käskyyn (SIGILL). Syynä voi olla CPU-käskyjen yhteensopivuusongelma tai asiakasohjelman kaatuminen. Jaa lokit ja laitteesi tiedot. Voit kokeilla Steam-asetukset → Steam-asiakasohjelma → x86-64 (kokeellinen). Se vaatii lisälatauksen ja voi toimia hitaammin. Linux-grafiikka-ajuri ei löytänyt käyttökelpoista Vulkan-laitetta. Tarkista, että valittu Linux-GPU-ajuri tukee tätä GPU:ta. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index f9827384b..32d904252 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1798,6 +1798,93 @@ Clavier + pavé tactile Terminal Poignées Deck + pavés tactiles + Tableau de bord + Rien à afficher pour l\'instant. + Téléchargements + Aucun téléchargement en cours. + Steam + Téléchargement du cache de shaders + En pause + En attente de démarrage + Notifications + Écran inférieur DroidDeck + Affichez ici les messages et autres notifications pendant que vous jouez. Ils restent uniquement sur cet écran et ne sont pas enregistrés. Android ne le demande qu\'une fois : sa page d\'accès aux notifications s\'ouvre sur cet écran ; activez-y DroidDeck et cette page reviendra d\'elle-même. + Autoriser l\'accès aux notifications + Aucune notification. + %1$d notifications + Effacer + Captures d\'écran + Les FPS nécessitent d\'activer le HUD + Vitesse + Pédales + Force G + Chronométrage + Meilleur tour + Pneus °C + Accélérateur et frein, 10 dernières s + Vitesse, %1$s + Rapport + Unités de vitesse + Automatique (%1$s) + Client Steam ARM + Client Steam x86 + Accélérateur + Frein + Tableau de bord + Passage de rapport + Frein à main + Limiteur des stands + TC + ABS + Turbo %1$s bar + Moteur %1$s °C + Huile %1$s °C + Répartiteur d\'énergie + Carburant, t + Principal %1$s t + Réserve %1$s t + Appuyez sur la bande supérieure ou sur le tableau de bord pour changer son style. + Envoyer les données du tableau de bord + Automatique + L\'active pour chaque jeu listé ici, y compris les jeux trouvés plus tard. Les jeux que vous désactivez restent désactivés. + Cela modifie le paramètre de sortie de données propre au jeu. Le changement prend effet au prochain lancement du jeu ; le désactiver rétablit l\'ancien paramètre. + Fermez %1$s pour modifier ce réglage. + Impossible de modifier les paramètres de %1$s. + %1$s : activez %2$s dans les paramètres du jeu avec l\'IP 127.0.0.1 et le port %3$d. + Enregistrées dans Pictures/DroidDeck. Les captures d\'écran Steam s\'affichent aussi ici. + Prendre une capture d\'écran + Capture d\'écran enregistrée. + Impossible de prendre une capture d\'écran. + Aucune capture d\'écran pour l\'instant. + Chat et voix + Lisez le chat Steam et répondez-y ici pendant que le jeu continue de tourner sur l\'écran principal. Le compte connecté à Steam est utilisé. + Steam n\'a pas encore de connexion web à partager, connectez-vous donc ici une fois. + Android a fermé la page de chat pendant que DroidDeck était en arrière-plan. Elle se rouvre lorsque vous revenez sur DroidDeck. + Ouvrir Steam Chat + Fermer le chat + Micro activé + Micro désactivé + Le micro est désactivé pour ce jeu. Pour parler, activez Microphone dans les paramètres Steam de DroidDeck (Audio), puis relancez le jeu. + Volume %1$d %% + Statistiques en jeu + FPS + CPU + GPU + Batterie + Tour + Position + Tour actuel + Dernier tour + Carburant + Cargaison + Points SYS · ENG · WEP + Statut légal + Solde + War Thunder + Elite Dangerous + Activez la sortie de données d\'un jeu de course (souvent appelée télémétrie ou UDP) pour voir ici un tableau de bord en direct. Les données restent sur cet appareil. + Le port %1$d reçoit des données (%2$d paquets de %3$d octets), mais dans un format que cette page ne sait pas encore lire. + Un autre programme utilise déjà le port %1$s ; ce jeu ne peut donc pas s\'afficher ici. Suppression de l\'environnement d\'exécution Linux @@ -3284,6 +3371,113 @@ Les vues enregistrées peuvent être recalculées. Démarrez un nouvel étalonnage pour capturer dans le système de coordonnées de suivi actuel. Incohérence de la somme de contrôle du modèle manuel OpenCV n\'a pas pu charger + Syncthing + Syncthing + Synchroniser avec Syncthing + Basée sur Syncthing-Fork, cette implémentation de Syncthing vous permet de synchroniser les fichiers du système de fichiers interne de DroidDeck avec d\'autres instances Syncthing. Elle ne concerne que les fichiers que vous stockez dans le système de fichiers interne de DroidDeck. + Cette version n\'inclut pas Syncthing. + Démarrage de Syncthing + Démarrage + À jour + Synchronisation + En pause + Mise en pause + Reprise + Intervention requise + Syncthing s\'est arrêté + Réessayer + En pause jusqu\'à la connexion Wi-Fi + En pause pendant le jeu + %1$d dossiers · %2$d appareils connectés sur %3$d + %1$s restants + Ouvrir l\'interface web + Pause + Reprendre + En cours d\'exécution + Désactivez pour arrêter complètement Syncthing. + Cet appareil + ID de l\'appareil + Afficher le QR code + Avancé + Ports, moment d\'exécution, réinitialisation + Ouvrir + En attente de votre réponse + %1$s souhaite se connecter + %1$s souhaite partager « %2$s » + Acceptez pour choisir où l\'enregistrer dans DroidDeck. + Vérifier dans l\'interface web + Dossiers + Aucun dossier pour l\'instant. + %1$s · %2$s + À jour + Synchronisation %1$d %% + Analyse + En pause + Non synchronisé + Échec de la synchronisation de %1$d fichiers + Bloqué + Ce dossier se trouve en dehors du stockage de DroidDeck, DroidDeck l\'a donc mis en pause. Pour synchroniser des dossiers en dehors de DroidDeck, utilisez l\'application Syncthing-Fork. + Retirer de Syncthing + Le retirer ne supprime aucun fichier. + Appareils + Aucun appareil pour l\'instant. + Connecté + Hors ligne + Vu pour la dernière fois : %1$s + ID de cet appareil + Sur votre autre appareil, ajoutez un appareil dans Syncthing et scannez ce code ou collez l\'ID. Acceptez-le ensuite ici dans l\'interface web. + Copier + Partager + Terminé + Désactiver Syncthing ? + Syncthing s\'arrête maintenant et reste arrêté jusqu\'à ce que vous le réactiviez. Les fichiers synchronisés restent à leur place. + Supprimer aussi la configuration de Syncthing + Supprime l\'ID de cet appareil, les dossiers et les appareils. À la réactivation, tout repart de zéro. + Désactiver + Ports + Synchronisation + Découverte locale + Choisir de nouveaux ports + Vérifié de nouveau à chaque démarrage. + Choisir à nouveau + Réinitialisation + Clé d\'API + Régénérer + Réinitialiser Syncthing + Nouvel ID d\'appareil, dossiers et appareils supprimés. Les fichiers du stockage de DroidDeck sont conservés. + Réinitialiser + Réinitialiser Syncthing ? + Exécution + Exécuter + Quand DroidDeck est ouvert + En arrière-plan + Wi-Fi uniquement + Mettre en pause pendant les jeux + Journaux + Sortie de Syncthing pour cette session + Partager + Syncthing + Veuillez d\'abord installer l\'environnement d\'exécution Linux. + Accepter + Enregistrer « %1$s » dans + Ajouter un dossier + Synchroniser un dossier du stockage de DroidDeck + Choisir + Ajouter + Impossible d\'ajouter le dossier. + Partager avec + Aucun appareil pour l\'instant. Vous pourrez partager ce dossier plus tard depuis l\'interface web. + Utiliser ce dossier + Nouveau dossier + Dossier personnel du conteneur + Dossier Sync + Stockage partagé de DroidDeck + DroidDeck sur %1$s + Données de l\'application DroidDeck + Les nouveaux dossiers sont enregistrés dans le dossier Sync de DroidDeck. Pour synchroniser un autre dossier de DroidDeck, utilisez « Ajouter un dossier » dans l\'onglet Syncthing. + Syncthing + Recharger + Syncthing n\'est pas en cours d\'exécution. Steam s\'est arrêté sur une instruction illégale (SIGILL). Il peut s\'agir d\'un problème de compatibilité des instructions CPU ou d\'un plantage du client. Partagez les journaux avec les détails de votre appareil. Vous pouvez essayer Paramètres Steam → Client Steam → x86-64 (expérimental). Cela nécessite un téléchargement supplémentaire et peut être plus lent. Le pilote graphique Linux n\'a trouvé aucun périphérique Vulkan utilisable. Vérifiez que le pilote GPU Linux sélectionné prend en charge ce GPU. diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index ddac601c3..1e34f532f 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -1622,7 +1622,7 @@ कॉपी करें कट करें एक्शन - Favorites + पसंदीदा मौजूदा फ़ोल्डर पिन करें अभी कोई favorite नहीं है - किसी फ़ोल्डर को उसके ⋮ मेनू से पिन करें ताकि यहां जल्दी वापस आ सकें। favorites से हटाएं @@ -1638,7 +1638,7 @@ रीड-ओनली गेम (और Wine) को इस फ़ाइल को ओवरराइट या डिलीट करने से रोकता है। रीड-ओनली नहीं बदला जा सका - Hidden + छिपी हुई यह स्टोरेज hidden फ़्लैग सेव नहीं कर सकता। फ़ाइल को Windows में hidden के तौर पर चिह्नित करता है (Wine का hidden एट्रिब्यूट)। इस स्टोरेज पर Hidden नहीं बदला जा सका @@ -1732,7 +1732,7 @@ फ़ाइलें स्टोरेज क्विक - FAVORITES + पसंदीदा डाउनलोड डाउनलोड ROM @@ -1768,7 +1768,7 @@ D-pad नीचे D-pad बायां D-pad दायां - Hidden + छिपा हुआ ले जाने के लिए ड्रैग करें स्नैप सेव करें @@ -1776,6 +1776,93 @@ कीबोर्ड + ट्रैकपैड टर्मिनल Deck ग्रिप + ट्रैकपैड + डैशबोर्ड + यहां दिखाने के लिए अभी कुछ नहीं है। + डाउनलोड + कोई डाउनलोड नहीं चल रहा है। + Steam + शेडर कैश डाउनलोड हो रहा है + रुका हुआ + शुरू होने का इंतज़ार + नोटिफ़िकेशन + DroidDeck नीचे की स्क्रीन + खेलते समय मैसेज और अन्य नोटिफ़िकेशन यहां दिखाएं। वे सिर्फ़ इसी स्क्रीन पर रहते हैं और सेव नहीं होते। Android एक बार पूछता है: उसका नोटिफ़िकेशन ऐक्सेस पेज इसी स्क्रीन पर खुलता है; वहां DroidDeck चालू करें और यह पेज अपने-आप वापस आ जाएगा। + नोटिफ़िकेशन ऐक्सेस की अनुमति दें + कोई नोटिफ़िकेशन नहीं। + %1$d नोटिफ़िकेशन + साफ़ करें + स्क्रीनशॉट + FPS के लिए HUD चालू होना चाहिए + स्पीड + पैडल + G-फ़ोर्स + टाइमिंग + सबसे अच्छा लैप + टायर °C + थ्रॉटल और ब्रेक, पिछले 10 s + स्पीड, %1$s + गियर + स्पीड की इकाई + ऑटोमैटिक (%1$s) + Steam क्लाइंट ARM + Steam क्लाइंट x86 + थ्रॉटल + ब्रेक + डैशबोर्ड + शिफ़्ट + हैंडब्रेक + पिट लिमिटर + TC + ABS + टर्बो %1$s bar + इंजन %1$s °C + ऑयल %1$s °C + पावर डिस्ट्रीब्यूटर + फ़्यूल, t + मुख्य %1$s t + रिज़र्व %1$s t + स्टाइल बदलने के लिए ऊपर की पट्टी या डैश पर टैप करें। + डैशबोर्ड डेटा भेजें + ऑटोमैटिक + यहां दिए हर गेम के लिए इसे चालू करता है, बाद में मिलने वाले गेम भी शामिल हैं। जिन गेम को आप बंद करते हैं, वे बंद ही रहते हैं। + यह गेम की अपनी डेटा आउटपुट सेटिंग बदलता है। यह अगली बार गेम शुरू होने पर लागू होता है; इसे बंद करने पर पुरानी सेटिंग वापस आ जाती है। + इसे बदलने के लिए %1$s बंद करें। + %1$s की सेटिंग बदली नहीं जा सकीं। + %1$s: गेम की सेटिंग में IP 127.0.0.1 और पोर्ट %3$d के साथ %2$s चालू करें। + Pictures/DroidDeck में सेव होते हैं। Steam स्क्रीनशॉट भी यहां दिखते हैं। + स्क्रीनशॉट लें + स्क्रीनशॉट सेव हो गया। + स्क्रीनशॉट नहीं लिया जा सका। + अभी कोई स्क्रीनशॉट नहीं। + चैट और वॉइस + मुख्य स्क्रीन पर गेम चलते हुए यहां Steam चैट पढ़ें और जवाब दें। यह उसी अकाउंट का इस्तेमाल करता है जिससे Steam में साइन इन है। + Steam के पास अभी शेयर करने के लिए कोई वेब साइन-इन नहीं है, इसलिए यहां एक बार साइन इन करें। + DroidDeck बैकग्राउंड में था, तब Android ने चैट पेज बंद कर दिया। DroidDeck पर वापस आने पर यह फिर से खुल जाता है। + Steam Chat खोलें + चैट बंद करें + माइक चालू + माइक बंद + इस गेम के लिए माइक बंद है। बात करने के लिए DroidDeck की Steam सेटिंग (ऑडियो) में माइक्रोफ़ोन चालू करें और गेम फिर से शुरू करें। + वॉल्यूम %1$d%% + इन-गेम आंकड़े + FPS + CPU + GPU + बैटरी + लैप + पोज़िशन + यह लैप + पिछला लैप + फ़्यूल + कार्गो + पिप्स SYS · ENG · WEP + कानूनी स्थिति + बैलेंस + War Thunder + Elite Dangerous + यहां लाइव डैश देखने के लिए किसी रेसिंग गेम का डेटा आउटपुट (अक्सर टेलीमेट्री या UDP कहलाता है) चालू करें। यह इसी डिवाइस पर रहता है। + पोर्ट %1$d पर डेटा आ रहा है (%3$d बाइट के %2$d पैकेट), लेकिन ऐसे फ़ॉर्मैट में जिसे यह पेज अभी पढ़ नहीं सकता। + पोर्ट %1$s को कोई दूसरा प्रोग्राम पहले से इस्तेमाल कर रहा है, इसलिए वह गेम यहां नहीं दिख सकता। Linux रनटाइम हटाया जा रहा है @@ -2154,7 +2241,7 @@ डील फ़्री गेम ट्रेंडिंग - Play + खेलें इंस्टॉल करें Battle.net खोलें Blizzard का डाउनलोडर शुरू हो रहा है… @@ -3248,6 +3335,113 @@ सहेजे गए दृश्यों की पुनर्गणना की जा सकती है। वर्तमान ट्रैकिंग समन्वय प्रणाली में कैप्चर करने के लिए एक नया अंशांकन प्रारंभ करें। हैंड मॉडल चेकसम बेमेल OpenCV लोड नहीं हो सका + Syncthing + Syncthing + Syncthing से सिंक करें + Syncthing-Fork पर आधारित यह Syncthing इम्प्लीमेंटेशन आपको DroidDeck के इंटरनल फ़ाइल सिस्टम की फ़ाइलें दूसरे Syncthing इंस्टेंस के साथ सिंक करने देता है। यह सिर्फ़ उन फ़ाइलों के लिए है जिन्हें आप DroidDeck के इंटरनल फ़ाइल सिस्टम में रखते हैं। + इस बिल्ड में Syncthing शामिल नहीं है। + Syncthing शुरू हो रहा है + शुरू हो रहा है + अप टू डेट + सिंक हो रहा है + रोका गया + रोका जा रहा है + फिर से शुरू हो रहा है + ध्यान देने की ज़रूरत है + Syncthing बंद हो गया + फिर से कोशिश करें + Wi-Fi मिलने तक रोका गया + गेम के दौरान रोका गया + %1$d फ़ोल्डर · %3$d में से %2$d डिवाइस कनेक्टेड + %1$s बाकी + वेब UI खोलें + रोकें + फिर से शुरू करें + चल रहा है + Syncthing को पूरी तरह रोकने के लिए बंद करें। + यह डिवाइस + डिवाइस ID + QR दिखाएं + एडवांस्ड + पोर्ट, कब चलाना है, रीसेट + खोलें + आपका इंतज़ार है + %1$s कनेक्ट करना चाहता है + %1$s “%2$s” शेयर करना चाहता है + DroidDeck में इसे कहां सेव करना है, यह चुनने के लिए स्वीकार करें। + वेब UI में देखें + फ़ोल्डर + अभी कोई फ़ोल्डर नहीं। + %1$s · %2$s + अप टू डेट + सिंक हो रहा है %1$d%% + स्कैन हो रहा है + रोका गया + सिंक नहीं है + %1$d फ़ाइलें सिंक नहीं हो सकीं + ब्लॉक किया गया + यह फ़ोल्डर DroidDeck के स्टोरेज से बाहर है, इसलिए DroidDeck ने इसे रोक दिया। DroidDeck से बाहर के फ़ोल्डर सिंक करने के लिए Syncthing-Fork ऐप इस्तेमाल करें। + Syncthing से हटाएं + इसे हटाने से कोई फ़ाइल डिलीट नहीं होती। + डिवाइस + अभी कोई डिवाइस नहीं। + कनेक्टेड + ऑफ़लाइन + आख़िरी बार देखा गया: %1$s + इस डिवाइस की ID + अपने दूसरे डिवाइस पर Syncthing में एक डिवाइस जोड़ें और यह कोड स्कैन करें या ID पेस्ट करें। फिर यहां वेब UI में उसे स्वीकार करें। + कॉपी करें + शेयर करें + पूरा हुआ + Syncthing बंद करें? + Syncthing अभी रुक जाएगा और दोबारा चालू करने तक बंद रहेगा। सिंक की गई फ़ाइलें जहां हैं वहीं रहेंगी। + Syncthing का कॉन्फ़िग भी डिलीट करें + इस डिवाइस की ID, फ़ोल्डर और डिवाइस हटा देता है। दोबारा चालू करने पर सब नए सिरे से शुरू होगा। + बंद करें + पोर्ट + सिंक + लोकल डिस्कवरी + नए पोर्ट चुनें + हर बार शुरू होने पर फिर से जांचा जाता है। + फिर से चुनें + रीसेट + API कुंजी + फिर से बनाएं + Syncthing रीसेट करें + नई डिवाइस ID, फ़ोल्डर और डिवाइस हटा दिए जाते हैं। DroidDeck के स्टोरेज की फ़ाइलें रखी जाती हैं। + रीसेट करें + Syncthing रीसेट करें? + चलना + चलाएं + जब DroidDeck खुला हो + बैकग्राउंड में + सिर्फ़ Wi-Fi + गेम के दौरान रोकें + लॉग + इस सेशन का Syncthing आउटपुट + शेयर करें + Syncthing + पहले Linux रनटाइम सेट अप करें। + स्वीकार करें + “%1$s” यहां सेव करें + फ़ोल्डर जोड़ें + DroidDeck के स्टोरेज का कोई फ़ोल्डर सिंक करें + चुनें + जोड़ें + फ़ोल्डर नहीं जोड़ा जा सका। + इनके साथ शेयर करें + अभी कोई डिवाइस नहीं। आप बाद में वेब UI से यह फ़ोल्डर शेयर कर सकते हैं। + यह फ़ोल्डर इस्तेमाल करें + नया फ़ोल्डर + कंटेनर होम + Sync फ़ोल्डर + DroidDeck शेयर्ड स्टोरेज + %1$s पर DroidDeck + DroidDeck ऐप डेटा + नए फ़ोल्डर DroidDeck के Sync फ़ोल्डर में सेव होते हैं। DroidDeck का कोई दूसरा फ़ोल्डर सिंक करने के लिए Syncthing टैब पर फ़ोल्डर जोड़ें इस्तेमाल करें। + Syncthing + रीलोड करें + Syncthing नहीं चल रहा है। Steam एक अवैध निर्देश (SIGILL) के साथ रुक गया। यह CPU निर्देश कम्पैटिबिलिटी की समस्या या क्लाइंट क्रैश हो सकता है। अपने डिवाइस की जानकारी के साथ लॉग शेयर करें। आप Steam सेटिंग → Steam क्लाइंट → x86-64 (प्रयोगात्मक) आज़मा सकते हैं। इसके लिए अतिरिक्त डाउनलोड चाहिए और यह धीमा चल सकता है। Linux ग्राफ़िक्स ड्राइवर को इस्तेमाल लायक Vulkan डिवाइस नहीं मिला। जांचें कि चुना गया Linux GPU ड्राइवर इस GPU का समर्थन करता है। diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index da127047e..16f61c7b8 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -277,7 +277,7 @@ Méret Átlátszatlanság Gomb - Trigger + Ravasz Váltás Exportálás Kapszula @@ -1776,6 +1776,93 @@ Billentyűzet + érintőpad Terminál Deck fogók + érintőpadok + Irányítópult + Itt még nincs mit megjeleníteni. + Letöltések + Nincs folyamatban lévő letöltés. + Steam + Shader-gyorsítótár letöltése + Szüneteltetve + Indításra vár + Értesítések + DroidDeck alsó képernyő + Itt jelenítheti meg az üzeneteket és más értesítéseket játék közben. Csak ezen a képernyőn láthatók, és nem kerülnek mentésre. Az Android egyszer kérdez rá: az értesítési hozzáférés oldala ezen a képernyőn nyílik meg; kapcsolja be ott a DroidDecket, és ez az oldal magától visszatér. + Értesítési hozzáférés engedélyezése + Nincsenek értesítések. + %1$d értesítés + Törlés + Képernyőképek + Az FPS-hez be kell kapcsolni a HUD-ot + Sebesség + Pedálok + G-erő + Időmérés + Legjobb kör + Gumik °C + Gáz és fék, utolsó 10 s + Sebesség, %1$s + Fokozat + Sebesség mértékegysége + Automatikus (%1$s) + Steam-kliens ARM + Steam-kliens x86 + Gáz + Fék + Műszerfal + Váltás + Kézifék + Bokszutcai limiter + TC + ABS + Turbó %1$s bar + Motor %1$s °C + Olaj %1$s °C + Energiaelosztó + Üzemanyag, t + Fő %1$s t + Tartalék %1$s t + Koppintson a felső sávra vagy a műszerfalra a stílus módosításához. + Műszerfal-adatok küldése + Automatikus + Bekapcsolja az itt felsorolt összes játéknál, a később talált játékokat is beleértve. A kikapcsolt játékok kikapcsolva maradnak. + Ez a játék saját adatkimeneti beállítását módosítja. A játék következő indításakor lép érvénybe; kikapcsoláskor visszaáll a régi beállítás. + A módosításhoz zárja be ezt: %1$s. + Nem sikerült módosítani a következő beállításait: %1$s. + %1$s: kapcsolja be a(z) %2$s opciót a játék beállításaiban, 127.0.0.1 IP-címmel és %3$d porttal. + Mentés helye: Pictures/DroidDeck. A Steam-képernyőképek is itt jelennek meg. + Képernyőkép készítése + Képernyőkép mentve. + Nem sikerült képernyőképet készíteni. + Még nincsenek képernyőképek. + Csevegés és hang + Itt olvashatja a Steam-csevegést és válaszolhat rá, miközben a játék tovább fut a fő képernyőn. Azt a fiókot használja, amellyel a Steambe bejelentkezett. + A Steam még nem rendelkezik megosztható webes bejelentkezéssel, ezért egyszer jelentkezzen be itt. + Az Android bezárta a csevegőoldalt, amíg a DroidDeck a háttérben volt. Újra megnyílik, amikor visszatér a DroidDeckhez. + Steam Chat megnyitása + Csevegés bezárása + Mikrofon be + Mikrofon ki + A mikrofon ki van kapcsolva ennél a játéknál. A beszédhez kapcsolja be a Mikrofon opciót a DroidDeck Steam-beállításaiban (Hang), majd indítsa újra a játékot. + Hangerő %1$d%% + Játékon belüli statisztikák + FPS + CPU + GPU + Akkumulátor + Kör + Helyezés + Aktuális kör + Előző kör + Üzemanyag + Rakomány + Pontok SYS · ENG · WEP + Jogi státusz + Egyenleg + War Thunder + Elite Dangerous + Kapcsolja be egy autóversenyes játék adatkimenetét (gyakran telemetria vagy UDP néven szerepel), hogy itt élő műszerfalat lásson. Az adatok ezen az eszközön maradnak. + A(z) %1$d port adatokat fogad (%2$d csomag, egyenként %3$d bájt), de olyan formátumban, amelyet ez az oldal még nem tud olvasni. + A(z) %1$s portot már egy másik program használja, ezért az a játék itt nem jeleníthető meg. Linux futtatókörnyezet eltávolítása @@ -3246,6 +3333,113 @@ Az elmentett nézetek újraszámíthatók. Indítson el egy új kalibrálást, hogy rögzítse az aktuális követési koordináta-rendszerben. A kézi modell ellenőrzőösszegének eltérése Az OpenCV betöltése nem sikerült + Syncthing + Syncthing + Szinkronizálás Syncthinggel + Ez a Syncthing-Forkon alapuló Syncthing-megvalósítás lehetővé teszi, hogy a DroidDeck belső fájlrendszerében lévő fájlokat más Syncthing-példányokkal szinkronizálja. Csak a DroidDeck belső fájlrendszerében tárolt fájlokra vonatkozik. + Ez a kiadás nem tartalmazza a Syncthinget. + A Syncthing indítása + Indítás + Naprakész + Szinkronizálás + Szüneteltetve + Szüneteltetés + Folytatás + Beavatkozást igényel + A Syncthing leállt + Újrapróbálás + Szüneteltetve Wi-Fi-kapcsolatig + Szüneteltetve a játék alatt + %1$d mappa · %2$d/%3$d eszköz csatlakoztatva + Még %1$s + Webes felület megnyitása + Szüneteltetés + Folytatás + Fut + Kapcsolja ki a Syncthing teljes leállításához. + Ez az eszköz + Eszközazonosító + QR-kód megjelenítése + Speciális + Portok, futtatás ideje, visszaállítás + Megnyitás + Önre vár + %1$s csatlakozni szeretne + %1$s meg szeretné osztani ezt: „%2$s” + Fogadja el, hogy kiválaszthassa, hová mentse a DroidDeckben. + Áttekintés a webes felületen + Mappák + Még nincsenek mappák. + %1$s · %2$s + Naprakész + Szinkronizálás: %1$d%% + Vizsgálat + Szüneteltetve + Nincs szinkronban + %1$d fájlt nem sikerült szinkronizálni + Letiltva + Ez a mappa a DroidDeck tárhelyén kívül van, ezért a DroidDeck szüneteltette. A DroidDecken kívüli mappák szinkronizálásához használja a Syncthing-Fork alkalmazást. + Eltávolítás a Syncthingből + Az eltávolítás nem töröl fájlokat. + Eszközök + Még nincsenek eszközök. + Csatlakoztatva + Offline + Utoljára látva: %1$s + Az eszköz azonosítója + A másik eszközén adjon hozzá egy eszközt a Syncthingben, és olvassa be ezt a kódot, vagy illessze be az azonosítót. Ezután fogadja el itt, a webes felületen. + Másolás + Megosztás + Kész + Kikapcsolja a Syncthinget? + A Syncthing most leáll, és leállítva marad, amíg újra be nem kapcsolja. A szinkronizált fájlok a helyükön maradnak. + A Syncthing konfigurációjának törlése is + Eltávolítja az eszköz azonosítóját, a mappákat és az eszközöket. Újbóli bekapcsoláskor elölről kezdi. + Kikapcsolás + Portok + Szinkronizálás + Helyi felfedezés + Új portok választása + Minden indításkor újra ellenőrizve. + Újraválasztás + Visszaállítás + API-kulcs + Újragenerálás + A Syncthing visszaállítása + Új eszközazonosító, a mappák és eszközök eltávolítva. A DroidDeck tárhelyén lévő fájlok megmaradnak. + Visszaállítás + Visszaállítja a Syncthinget? + Futtatás + Futtatás + Amíg a DroidDeck nyitva van + A háttérben + Csak Wi-Fi + Szüneteltetés játék közben + Naplók + A Syncthing kimenete ebben a munkamenetben + Megosztás + Syncthing + Előbb állítsa be a Linux futtatókörnyezetet. + Elfogadás + „%1$s” mentése ide: + Mappa hozzáadása + Egy mappa szinkronizálása a DroidDeck tárhelyén + Kiválasztás + Hozzáadás + Nem sikerült hozzáadni a mappát. + Megosztás ezekkel: + Még nincsenek eszközök. Ezt a mappát később a webes felületről is megoszthatja. + Ennek a mappának a használata + Új mappa + Konténer saját mappája + Sync mappa + DroidDeck megosztott tárhely + DroidDeck itt: %1$s + DroidDeck alkalmazásadatok + Az új mappák a DroidDeck Sync mappájába kerülnek. Másik DroidDeck-mappa szinkronizálásához használja a Mappa hozzáadása lehetőséget a Syncthing lapon. + Syncthing + Újratöltés + A Syncthing nem fut. A Steam érvénytelen utasítással leállt (SIGILL). Ez CPU-utasítás-kompatibilitási probléma vagy a kliens összeomlása lehet. Ossza meg a naplókat az eszköz adataival együtt. Kipróbálhatja a Steam-beállítások → Steam-kliens → x86-64 (kísérleti) lehetőséget. További letöltést igényel, és lassabban futhat. A Linux grafikus illesztőprogram nem talált használható Vulkan-eszközt. Ellenőrizze, hogy a kiválasztott Linux GPU-illesztőprogram támogatja-e ezt a GPU-t. diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 960dddb10..31389966c 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -1754,6 +1754,93 @@ Keyboard + trackpad Terminal Pegangan Deck + trackpad + Dasbor + Belum ada yang bisa ditampilkan di sini. + Unduhan + Tidak ada unduhan yang berjalan. + Steam + Mengunduh cache shader + Dijeda + Menunggu dimulai + Notifikasi + Layar bawah DroidDeck + Tampilkan pesan dan notifikasi lain di sini saat Anda bermain. Notifikasi hanya muncul di layar ini dan tidak disimpan. Android hanya meminta sekali: halaman akses notifikasinya terbuka di layar ini; aktifkan DroidDeck di sana dan halaman ini akan kembali dengan sendirinya. + Izinkan akses notifikasi + Tidak ada notifikasi. + %1$d notifikasi + Hapus + Tangkapan layar + FPS memerlukan HUD aktif + Kecepatan + Pedal + Gaya G + Waktu + Lap terbaik + Ban °C + Gas dan rem, 10 s terakhir + Kecepatan, %1$s + Gigi + Satuan kecepatan + Otomatis (%1$s) + Klien Steam ARM + Klien Steam x86 + Gas + Rem + Lampu indikator + Pindah gigi + Rem tangan + Pembatas pit + TC + ABS + Turbo %1$s bar + Mesin %1$s °C + Oli %1$s °C + Distributor daya + Bahan bakar, t + Utama %1$s t + Cadangan %1$s t + Ketuk bilah atas atau dasbor untuk mengubah gayanya. + Kirim data dasbor + Otomatis + Mengaktifkannya untuk setiap gim yang tercantum di sini, termasuk gim yang ditemukan nanti. Gim yang Anda matikan tetap mati. + Ini mengubah setelan keluaran data milik gim itu sendiri. Perubahan berlaku saat gim dimulai berikutnya; mematikannya mengembalikan setelan lama. + Tutup %1$s untuk mengubah ini. + Tidak dapat mengubah setelan %1$s. + %1$s: aktifkan %2$s di setelan gim dengan IP 127.0.0.1 dan port %3$d. + Disimpan ke Pictures/DroidDeck. Tangkapan layar Steam juga muncul di sini. + Ambil tangkapan layar + Tangkapan layar disimpan. + Tidak dapat mengambil tangkapan layar. + Belum ada tangkapan layar. + Obrolan & suara + Baca dan balas obrolan Steam di sini sementara gim tetap berjalan di layar utama. Fitur ini menggunakan akun yang dipakai untuk masuk ke Steam. + Steam belum memiliki info masuk web untuk dibagikan, jadi masuklah di sini sekali. + Android menutup halaman obrolan saat DroidDeck berada di latar belakang. Halaman akan terbuka lagi saat Anda kembali ke DroidDeck. + Buka Steam Chat + Tutup obrolan + Mik aktif + Mik mati + Mik mati untuk gim ini. Untuk berbicara, aktifkan Mikrofon di setelan Steam DroidDeck (Audio) lalu mulai gim lagi. + Volume %1$d%% + Statistik dalam gim + FPS + CPU + GPU + Baterai + Lap + Posisi + Lap ini + Lap terakhir + Bahan bakar + Kargo + Pip SYS · ENG · WEP + Status hukum + Saldo + War Thunder + Elite Dangerous + Aktifkan keluaran data gim balap (sering disebut telemetri atau UDP) untuk melihat dasbor langsung di sini. Data tetap berada di perangkat ini. + Port %1$d menerima data (%2$d paket berukuran %3$d byte), tetapi dalam format yang belum dapat dibaca halaman ini. + Program lain sudah menggunakan port %1$s, jadi gim tersebut tidak dapat ditampilkan di sini. Menghapus runtime Linux @@ -2607,7 +2694,7 @@ Game di kacamata Tampilkan game hanya di kacamata. Nonaktif: ponsel menampilkannya dan Android mencerminkan layar ponsel. Tampilan - Refresh rate + Kecepatan refresh 3D (berdampingan) Alihkan kacamata pintar ke 3D berdampingan. Pelacakan kepala dikontrol secara terpisah. Sinyal 3D belum terkonfirmasi. Coba lagi setelah gambar 2D kembali. @@ -3221,6 +3308,113 @@ Tampilan yang disimpan dapat dihitung ulang. Mulai kalibrasi baru untuk menangkap sistem koordinat pelacakan saat ini. Ketidakcocokan checksum model tangan OpenCV tidak dapat memuat + Syncthing + Syncthing + Sinkronkan dengan Syncthing + Berbasis Syncthing-Fork, ini adalah implementasi Syncthing yang memungkinkan Anda menyinkronkan file di sistem file internal DroidDeck dengan instans Syncthing lain. Ini hanya untuk file yang Anda simpan di dalam sistem file internal DroidDeck. + Build ini tidak menyertakan Syncthing. + Memulai Syncthing + Memulai + Terbaru + Menyinkronkan + Dijeda + Menjeda + Melanjutkan + Perlu perhatian + Syncthing berhenti + Coba lagi + Dijeda hingga ada Wi-Fi + Dijeda selama gim berjalan + %1$d folder · %2$d dari %3$d perangkat terhubung + %1$s lagi + Buka UI web + Jeda + Lanjutkan + Berjalan + Matikan untuk menghentikan Syncthing sepenuhnya. + Perangkat ini + ID Perangkat + Tampilkan QR + Lanjutan + Port, kapan berjalan, atur ulang + Buka + Menunggu Anda + %1$s ingin terhubung + %1$s ingin berbagi “%2$s” + Terima untuk memilih lokasi penyimpanannya di DroidDeck. + Tinjau di UI web + Folder + Belum ada folder. + %1$s · %2$s + Terbaru + Menyinkronkan %1$d%% + Memindai + Dijeda + Tidak sinkron + %1$d file tidak dapat disinkronkan + Diblokir + Folder ini berada di luar penyimpanan DroidDeck, jadi DroidDeck menjedanya. Untuk menyinkronkan folder di luar DroidDeck, gunakan aplikasi Syncthing-Fork. + Hapus dari Syncthing + Menghapusnya tidak akan menghapus file apa pun. + Perangkat + Belum ada perangkat. + Terhubung + Offline + Terakhir terlihat %1$s + ID perangkat ini + Di perangkat Anda yang lain, tambahkan perangkat di Syncthing lalu pindai kode ini atau tempel ID-nya. Kemudian terima di sini melalui UI web. + Salin + Bagikan + Selesai + Matikan Syncthing? + Syncthing berhenti sekarang dan tetap berhenti hingga dinyalakan kembali. File yang telah disinkronkan tetap di tempatnya. + Hapus juga konfigurasi Syncthing + Menghapus ID, folder, dan perangkat milik perangkat ini. Menyalakannya lagi akan memulai dari awal. + Matikan + Port + Sinkronisasi + Penemuan lokal + Pilih port baru + Diperiksa lagi setiap kali dimulai. + Pilih ulang + Atur ulang + Kunci API + Buat ulang + Atur ulang Syncthing + ID perangkat baru, folder dan perangkat dihapus. File di penyimpanan DroidDeck tetap disimpan. + Atur ulang + Atur ulang Syncthing? + Berjalan + Jalankan + Saat DroidDeck terbuka + Di latar belakang + Hanya Wi-Fi + Jeda selama bermain gim + Log + Output Syncthing untuk sesi ini + Bagikan + Syncthing + Siapkan runtime Linux terlebih dahulu. + Terima + Simpan “%1$s” ke + Tambah folder + Sinkronkan folder di penyimpanan DroidDeck + Pilih + Tambah + Folder tidak dapat ditambahkan. + Bagikan dengan + Belum ada perangkat. Anda dapat membagikan folder ini dari UI web nanti. + Gunakan folder ini + Folder baru + Folder home kontainer + Folder Sync + Penyimpanan bersama DroidDeck + DroidDeck di %1$s + Data aplikasi DroidDeck + Folder baru disimpan di folder Sync DroidDeck. Untuk menyinkronkan folder DroidDeck lain, gunakan Tambah folder di tab Syncthing. + Syncthing + Muat ulang + Syncthing tidak berjalan. Steam berhenti karena instruksi ilegal (SIGILL). Ini bisa berupa masalah kompatibilitas instruksi CPU atau klien mengalami crash. Bagikan log beserta detail perangkat Anda. Anda dapat mencoba Setelan Steam → Klien Steam → x86-64 (eksperimental). Ini memerlukan unduhan tambahan dan mungkin berjalan lebih lambat. Driver grafis Linux tidak dapat menemukan perangkat Vulkan yang dapat digunakan. Pastikan driver GPU Linux yang dipilih mendukung GPU ini. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 81706b171..7e2e5158c 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1790,6 +1790,93 @@ Tastiera + trackpad Terminale Impugnature Deck + trackpad + Dashboard + Ancora niente da mostrare qui. + Download + Nessun download in corso. + Steam + Download della cache degli shader + In pausa + In attesa di avvio + Notifiche + Schermo inferiore DroidDeck + Mostra qui messaggi e altre notifiche mentre giochi. Restano solo su questo schermo e non vengono salvate. Android lo chiede una sola volta: la sua pagina di accesso alle notifiche si apre su questo schermo; attiva DroidDeck lì e questa pagina torna da sola. + Consenti accesso alle notifiche + Nessuna notifica. + %1$d notifiche + Cancella + Screenshot + Gli FPS richiedono l\'HUD attivo + Velocità + Pedali + Forza G + Tempi + Giro migliore + Pneumatici °C + Acceleratore e freno, ultimi 10 s + Velocità, %1$s + Marcia + Unità di velocità + Automatico (%1$s) + Client Steam ARM + Client Steam x86 + Acceleratore + Freno + Spie + Cambio marcia + Freno a mano + Limitatore pit + TC + ABS + Turbo %1$s bar + Motore %1$s °C + Olio %1$s °C + Distribuzione energia + Carburante, t + Principale %1$s t + Riserva %1$s t + Tocca la barra superiore o il cruscotto per cambiarne lo stile. + Invia dati al cruscotto + Automatico + Lo attiva per ogni gioco elencato qui, compresi i giochi trovati in seguito. I giochi che disattivi restano disattivati. + Modifica l\'impostazione di output dati del gioco stesso. Ha effetto al prossimo avvio del gioco; disattivandola viene ripristinata l\'impostazione precedente. + Chiudi %1$s per modificarlo. + Impossibile modificare le impostazioni di %1$s. + %1$s: attiva %2$s nelle impostazioni del gioco con IP 127.0.0.1 e porta %3$d. + Salvati in Pictures/DroidDeck. Qui compaiono anche gli screenshot di Steam. + Acquisisci screenshot + Screenshot salvato. + Impossibile acquisire uno screenshot. + Ancora nessuno screenshot. + Chat & voce + Leggi e rispondi alla chat di Steam qui mentre il gioco continua sullo schermo principale. Usa l\'account con cui è stato effettuato l\'accesso a Steam. + Steam non ha ancora un accesso web da condividere, quindi accedi qui una volta. + Android ha chiuso la pagina della chat mentre DroidDeck era in background. Si riapre quando torni su DroidDeck. + Apri Steam Chat + Chiudi chat + Microfono attivo + Microfono disattivato + Il microfono è disattivato per questo gioco. Per parlare, attiva Microfono nelle impostazioni Steam di DroidDeck (Audio) e riavvia il gioco. + Volume %1$d%% + Statistiche di gioco + FPS + CPU + GPU + Batteria + Giro + Posizione + Giro attuale + Ultimo giro + Carburante + Carico + Pip SYS · ENG · WEP + Stato legale + Saldo + War Thunder + Elite Dangerous + Attiva l\'output dati di un gioco di corse (spesso chiamato telemetria o UDP) per vedere qui un cruscotto in tempo reale. I dati restano su questo dispositivo. + La porta %1$d riceve dati (%2$d pacchetti da %3$d byte), ma in un formato che questa pagina non sa ancora leggere. + Un altro programma usa già la porta %1$s, quindi quel gioco non può essere mostrato qui. Rimozione del runtime Linux @@ -3282,6 +3369,113 @@ Le visualizzazioni salvate possono essere ricalcolate. Avvia una nuova calibrazione per acquisire il sistema di coordinate di tracciamento corrente. Mancata corrispondenza del checksum del modello di mano Impossibile caricare OpenCV + Syncthing + Syncthing + Sincronizza con Syncthing + Basata su Syncthing-Fork, è un\'implementazione di Syncthing che ti permette di sincronizzare i file del file system interno di DroidDeck con altre istanze di Syncthing. Vale solo per i file che conservi all\'interno del file system interno di DroidDeck. + Questa build non include Syncthing. + Avvio di Syncthing + Avvio in corso + Aggiornato + Sincronizzazione + In pausa + Messa in pausa + Ripresa in corso + Richiede attenzione + Syncthing si è fermato + Riprova + In pausa fino al Wi-Fi + In pausa durante il gioco + %1$d cartelle · %2$d di %3$d dispositivi connessi + %1$s rimanenti + Apri l\'interfaccia web + Pausa + Riprendi + In esecuzione + Disattiva per fermare completamente Syncthing. + Questo dispositivo + ID dispositivo + Mostra QR + Avanzate + Porte, quando eseguire, ripristino + Apri + In attesa di te + %1$s vuole connettersi + %1$s vuole condividere “%2$s” + Accetta per scegliere dove salvarla in DroidDeck. + Controlla nell\'interfaccia web + Cartelle + Ancora nessuna cartella. + %1$s · %2$s + Aggiornata + Sincronizzazione %1$d%% + Scansione + In pausa + Non sincronizzata + Impossibile sincronizzare %1$d file + Bloccata + Questa cartella è fuori dall\'archiviazione di DroidDeck, quindi DroidDeck l\'ha messa in pausa. Per sincronizzare cartelle fuori da DroidDeck, usa l\'app Syncthing-Fork. + Rimuovi da Syncthing + La rimozione non elimina alcun file. + Dispositivi + Ancora nessun dispositivo. + Connesso + Offline + Ultima connessione %1$s + ID di questo dispositivo + Sull\'altro dispositivo, aggiungi un dispositivo in Syncthing e scansiona questo codice o incolla l\'ID. Poi accettalo qui nell\'interfaccia web. + Copia + Condividi + Fatto + Disattivare Syncthing? + Syncthing si ferma ora e resta fermo finché non lo riattivi. I file sincronizzati restano dove sono. + Elimina anche la configurazione di Syncthing + Rimuove l\'ID, le cartelle e i dispositivi di questo dispositivo. Riattivandolo si riparte da zero. + Disattiva + Porte + Sincronizzazione + Rilevamento locale + Scegli nuove porte + Ricontrollate a ogni avvio. + Riscegli + Ripristino + Chiave API + Rigenera + Ripristina Syncthing + Nuovo ID dispositivo, cartelle e dispositivi rimossi. I file nell\'archiviazione di DroidDeck vengono mantenuti. + Ripristina + Ripristinare Syncthing? + Esecuzione + Esegui + Mentre DroidDeck è aperto + In background + Solo Wi-Fi + Pausa durante i giochi + Log + Output di Syncthing per questa sessione + Condividi + Syncthing + Configura prima il runtime Linux. + Accetta + Salva “%1$s” in + Aggiungi cartella + Sincronizza una cartella nell\'archiviazione di DroidDeck + Scegli + Aggiungi + Impossibile aggiungere la cartella. + Condividi con + Ancora nessun dispositivo. Potrai condividere questa cartella dall\'interfaccia web in seguito. + Usa questa cartella + Nuova cartella + Home del container + Cartella Sync + Archiviazione condivisa di DroidDeck + DroidDeck su %1$s + Dati dell\'app DroidDeck + Le nuove cartelle vengono salvate nella cartella Sync di DroidDeck. Per sincronizzare un\'altra cartella di DroidDeck, usa Aggiungi cartella nella scheda Syncthing. + Syncthing + Ricarica + Syncthing non è in esecuzione. Steam si è arrestato con un\'istruzione non valida (SIGILL). Può trattarsi di un problema di compatibilità delle istruzioni della CPU o di un crash del client. Condividi i log con i dettagli del tuo dispositivo. Puoi provare Impostazioni di Steam → Client Steam → x86-64 (sperimentale). Richiede un download aggiuntivo e potrebbe essere più lento. Il driver grafico Linux non ha trovato un dispositivo Vulkan utilizzabile. Verifica che il driver GPU Linux selezionato supporti questa GPU. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 432caa199..a1c5a5b34 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -88,6 +88,7 @@ 1に設定すると、このゲームでdroiddeck-fsyncを使用しません。パックがある場合はdroiddeck-esync、ない場合はdroiddeck-ntsyncを使用します。 1に設定するとこのゲームをdroiddeck-fsyncで実行します。0に設定すると、Protonに合うパックがある場合はdroiddeck-esyncを優先します。 0に設定すると、このゲームのProtonコントローラーナビゲーション補助を無効にします。 + デフォルトでオンです。ゲームがオフラインプレイに対応している場合、Easy Anti-Cheatのゲームをアンチチートなしで起動します。オフにすると、通常どおりEasy Anti-Cheat経由で起動します。Easy Anti-Cheatが必要なオンラインモードは引き続き利用できません。 D3D9~11のDXVKオーバーレイ。fpsはフレームレートを表示します。設定を解除するとオーバーレイが非表示になります。 セミコロンで区切られたDXVK設定。最近のバージョンではdxvk.maxFrameRateでフレームレートを制限します。通常のプレイにはセッションフレームレートリミッターを推奨します。 このゲームで使用するCPUコア(例:0-4、0,2,4)。セッションのゲームコアの代わりに使用されます。コアが少ないほど発熱が抑えられます。 @@ -328,6 +329,12 @@ 最新です · %1$s ローダー 最新バージョンをインストール + アップデートチャンネル + インストールするDecky Loaderのリリースです。Decky自身のアップデーターも同じチャンネルを使用します。 + 安定版 + プレリリース版 + テスト版 + Deckyのアップデートチャンネルを保存できませんでした Steamと共に起動します。有効にすると、このデバイス上の他のアプリがSteamを制御できる場合があります。 オフ。Steamのリモートデバッグポートは閉じたままになります。 Decky Loaderをアンインストールしますか? @@ -337,6 +344,8 @@ Deckモードでゲームがクラッシュしたり画面が真っ暗になる場合は、これをオフにしてください。次回のセッションで有効になります。 ゲーム終了後にDroidDeckへ戻る DroidDeckや他のランチャーから起動したゲームを閉じると、Steamが正しく終了し(クラウドセーブの同期を先に完了)、起動元に戻ります。オフ:Steamに留まります。 + シェーダーの事前キャッシュ + Steamがゲーム用にビルド済みのシェーダーをダウンロードしてバックグラウンドで処理するため、ゲームのカクつきが減ります。オフにするとデータ通信量、ストレージ、バッテリーを節約できますが、ゲームはプレイ中にシェーダーをビルドします。Steamの「ダウンロード」>「シェーダーの事前キャッシュ」と同じ設定です。次回のセッションから適用されます。 Steamクライアントを修復 次回Steamの起動時にSteamクライアントを再ダウンロードします。Steamが読み込み画面のまま進まない場合に使用してください。ゲーム、ログイン情報、設定は保持されます。 次回Steamの起動時にSteamクライアントを再ダウンロードします。 @@ -593,6 +602,8 @@ prootファストパス prootのseccompフィルターが必要です。 ファイル検索が速くなり、スタッターが減ります。プログラムがファイルを見つけられない場合は、これをオフにしてください。 + gamescope内でカーソルを描画 + ポインターをAndroidに任せず、Gamescope自身が描画します。コントローラーで操作するカーソルにはオンにしてください。スマートフォンではオフの方が高速です。次回のセッションから適用されます。 セッションの識別情報 ホスト名 Steamがこのデバイスを認識する名前。空にすると%1$sに戻ります。 @@ -1184,6 +1195,9 @@ 他のGPUのドライバーを非表示 他のGPUのドライバーを表示 詳細設定 + UBWC互換性 + SteamやAndroidで縞模様やちらつきが出る場合は、オンまたはオフを試してください。DroidDeckの再起動後、両方のドライバーに適用されます。 + 適用するにはDroidDeckを再起動してください。 ディスプレイドライバーと一緒に設定:%1$s は両方を含みます。 ランタイムドライバーと一緒に設定:%1$s は両方を含みます。 ゲームとデスクトップで使用されます。 @@ -1292,6 +1306,8 @@ 自動でピクチャーインピクチャー 開いた状態の画面のコントロール 開いた状態では、ゲームがSteam Deckのトラックパッドと背面グリップの上に表示されます。折りたたんだ状態では、ゲームが全画面で表示されます。 + コントローラー接続時も表示 + コントローラーが接続されている間も、これらのタッチ操作を表示します。 一時停止中 セッションの準備ができたらピクチャーインピクチャーを使用できます。 Android設定でDroidDeckのピクチャーインピクチャーを許可してください。 @@ -1358,6 +1374,9 @@ インストールする Linux実行環境を削除 実行環境、その中のSteamクライアント、そこにインストールされたすべてのゲームが削除されます。 + Linux実行環境 + 「修復」は実行環境のシステムファイルを再インストールし、Steam、ゲーム、設定は保持します。デスクトップなどの追加機能は、次に使用するときに再度インストールされます。「削除」はすべてを消去します。 + 実行環境を修復 適用しました:%1$s メイン画面で%1$sを開けませんでした %2$sで%1$sを開けませんでした @@ -1484,6 +1503,10 @@ 実行環境のカタログに接続できませんでした。接続を確認して、もう一度Playをタップしてください。 Linux実行環境をインストールできませんでした。「設定」で実行環境を確認して、もう一度Playをタップしてください。 デスクトップをインストールできませんでした(%1$s)。接続を確認して、もう一度「デスクトップ」をタップしてください。 + 中断されたインストールを完了できませんでした(%1$s)。接続と空き容量を確認して、もう一度お試しください。 + + Linux実行環境が破損しています。%1$d個のライブラリリンクが存在しないファイルを指しています(%2$sなど)。通常はインストールが中断された後に発生します。修復してもゲームと設定は保持されます。 + %1$sが予期せず停止しました。バグレポートにログを添付して共有するか、もう一度お試しください。 プログラムが予期せず停止しました。バグレポートにログを添付して共有するか、もう一度お試しください。 デスクトップが予期せず停止しました。バグレポートにログを添付して共有するか、もう一度お試しください。 @@ -1497,6 +1520,12 @@ 次回のSteamセッション用にプラグインのzipをインストールします。 ダウンロード中… zipからインストール + サードパーティ製プラグインのインストール + このプラグインはDecky Loaderチームによる審査を受けていません。Steamアカウントを盗んだりデバイスに害を与えたりするソフトウェアなど、マルウェアが含まれている可能性があります。このプラグインをインストールすることで、デバイスに関するすべてのリスクを負うことに同意したものとみなされます。 + 続行 + + %1$d秒お待ちください + ゲーム オーディオ・ビデオ グラフィック @@ -1725,6 +1754,93 @@ キーボード + トラックパッド ターミナル Deckグリップ + トラックパッド + ダッシュボード + まだ表示するものはありません。 + ダウンロード + 実行中のダウンロードはありません。 + Steam + シェーダーキャッシュをダウンロード中 + 一時停止中 + 開始待ち + 通知 + DroidDeck下部画面 + プレイ中、メッセージなどの通知をここに表示します。通知はこの画面にのみ表示され、保存されません。Androidによる確認は一度だけです。この画面に通知へのアクセスのページが開くので、そこでDroidDeckをオンにすると、このページに自動的に戻ります。 + 通知へのアクセスを許可 + 通知はありません。 + %1$d件の通知 + クリア + スクリーンショット + FPSの表示にはHUDをオンにする必要があります + 速度 + ペダル + Gフォース + タイム + ベストラップ + タイヤ °C + スロットルとブレーキ(直近10 s) + 速度(%1$s) + ギア + 速度の単位 + 自動(%1$s) + Steamクライアント ARM + Steamクライアント x86 + スロットル + ブレーキ + インジケーター + シフト + サイドブレーキ + ピットリミッター + TC + ABS + ターボ %1$s bar + エンジン %1$s °C + 油温 %1$s °C + パワーディストリビューター + 燃料(t) + メイン %1$s t + 予備 %1$s t + 上部のバーまたはダッシュをタップすると、表示スタイルを変更できます。 + ダッシュボード用データを送信 + 自動 + ここに表示されているすべてのゲーム(後から見つかったゲームを含む)でオンにします。オフにしたゲームはオフのままです。 + ゲーム自体のデータ出力設定を変更します。次回ゲームを起動したときに反映されます。オフにすると元の設定に戻ります。 + 変更するには%1$sを終了してください。 + %1$sの設定を変更できませんでした。 + %1$s:ゲームの設定で%2$sをオンにし、IP 127.0.0.1、ポート%3$dを指定してください。 + Pictures/DroidDeckに保存されます。Steamのスクリーンショットもここに表示されます。 + スクリーンショットを撮る + スクリーンショットを保存しました。 + スクリーンショットを撮れませんでした。 + スクリーンショットはまだありません。 + チャット&ボイス + ゲームをメイン画面で実行したまま、ここでSteamチャットを読んで返信できます。Steamにサインインしているアカウントを使用します。 + Steamにはまだ共有できるWebサインインがないため、ここで一度サインインしてください。 + DroidDeckがバックグラウンドにある間に、Androidがチャットページを閉じました。DroidDeckに戻ると再度開きます。 + Steamチャットを開く + チャットを閉じる + マイク オン + マイク オフ + このゲームではマイクがオフです。話すには、DroidDeckのSteam設定(オーディオ)でマイクをオンにして、ゲームを再起動してください。 + 音量 %1$d%% + ゲーム内統計 + FPS + CPU + GPU + バッテリー + ラップ + 順位 + 現在のラップ + 前回のラップ + 燃料 + 積荷 + 電力配分 SYS · ENG · WEP + 法的状態 + 残高 + War Thunder + Elite Dangerous + レースゲームのデータ出力(テレメトリーやUDPと呼ばれることが多い)をオンにすると、ここにライブダッシュが表示されます。データはこのデバイス内にとどまります。 + ポート%1$dでデータを受信していますが(%3$dバイトのパケット%2$d個)、このページではまだ読み取れない形式です。 + 別のプログラムがすでにポート%1$sを使用しているため、そのゲームはここに表示できません。 Linux実行環境を削除中 @@ -1957,6 +2073,7 @@ Adreno GPUではありません サポート対象 実験的:ドライバーはテストビルドです + Adreno 850の実験的サポート:互換性のある実験版のディスプレイドライバーとゲームドライバーが必要です。このデバイスでは未検証です テスト済みハードウェア(Adreno 725以降)未満:動作しない可能性があります サポート対象外:DroidDeckにはAdreno(Snapdragon)GPUが必要です @@ -2293,6 +2410,9 @@ ダウンロードマネージャー 同時ダウンロード数 インストール先 + ゲームの言語 + GOGとEpicはこの言語でインストールします。「システム」はアプリの言語に従います。 + システム %1$sをインストール: 内部ストレージ 空き %1$s @@ -2598,6 +2718,22 @@ 「自動」では、この 2D ゲームは視界の中で固定されます。ヘッドトラッキングを使うには、「表示」で「仮想スクリーン」を選んでください。 ヘッドトラッキングは、仮想スクリーンまたは実行中の SteamVR ゲームに適用されます。 デバイス + コントローラー + SteamVRコントローラー + VRでのゲームパッド + コントローラーが担当する2つのVRの手です。変更はSteamVRの実行中でもすぐに反映されます。Joy-Conやトラッキングされた手は、引き続きそれぞれの側を優先して操作します。 + 通常のゲームパッド + Valve Indexコントローラー + Meta Quest 3コントローラー + HTC Viveワンド + スティックの初期動作 + VR中に両方のスティックを同時に押し込むと切り替わります。 + ゲーム内で移動 + 手を動かす + ボタン + トリガーはトリガー、バンパーはグリップ、A/BとX/Yはそれぞれの手のフェイスボタン、スティック押し込みはサムスティックのクリック、ガイドボタンはSteamVRメニューを開きます。十字キーの上下で手を近づけたり遠ざけたりします。 + スティックはトラックパッドとして動作し、A/Xまたはスティック押し込みで押します。トリガーはトリガー、バンパーはグリップ、StartとSelectはメニュー、ガイドボタンはSteamVRメニューを開きます。 + VRの手はありません。ゲームパッドに対応したゲームとSteamVRメニューでは、通常どおりコントローラーを使用できます。 明るさ 音量 サンブロック @@ -2655,6 +2791,12 @@ カメラの位置 プレイ前に、カメラにあなたが映っているか確認してください。 確認 + ハンドトラッキングのダウンロード + ハンドトラッキングには%1$sの1回限りのダウンロードが必要です。 + ダウンロード + 再試行 + ハンドトラッキングをダウンロード中… %1$d%% + ハンドトラッキングをダウンロードできませんでした。接続を確認して、もう一度お試しください。 カメラの位置 デバイスを胸の高さくらいで1.5〜2m離れたところに立て、背面カメラがあなたの方を向くようにしてください。そのあと、手を広げて画面内に入ってください。 デバイスを胸の高さくらいで1.5〜2m離れたところに立て、画面があなたの方を向くようにしてください。そのあと、手を広げて画面内に入ってください。 @@ -3164,6 +3306,113 @@ 保存されたビューは再計算できます。新しいキャリブレーションを開始して、現在のトラッキング座標系でキャプチャします。 ハンドモデルのチェックサムの不一致 OpenCVをロードできませんでした + Syncthing + Syncthing + Syncthingで同期 + Syncthing-ForkをベースにしたSyncthingの実装で、DroidDeckの内部ファイルシステム内のファイルを他のSyncthingインスタンスと同期できます。DroidDeckの内部ファイルシステム内に保存しているファイル専用です。 + このビルドにはSyncthingが含まれていません。 + Syncthingを起動中 + 起動中 + 最新 + 同期中 + 一時停止中 + 一時停止しています + 再開しています + 要確認 + Syncthingが停止しました + 再試行 + Wi-Fi接続まで一時停止中 + ゲーム中は一時停止中 + %1$d個のフォルダ · %3$d台中%2$d台のデバイスが接続中 + 残り%1$s + Web UIを開く + 一時停止 + 再開 + 実行中 + オフにするとSyncthingを完全に停止します。 + このデバイス + デバイスID + QRコードを表示 + 詳細設定 + ポート、実行タイミング、リセット + 開く + 確認待ち + %1$sが接続を求めています + %1$sが「%2$s」を共有しようとしています + 承認すると、DroidDeck内の保存先を選択できます。 + Web UIで確認 + フォルダ + フォルダはまだありません。 + %1$s · %2$s + 最新 + 同期中 %1$d%% + スキャン中 + 一時停止中 + 未同期 + %1$d個のファイルを同期できませんでした + ブロック済み + このフォルダはDroidDeckのストレージ外にあるため、DroidDeckが一時停止しました。DroidDeck外のフォルダを同期するには、Syncthing-Forkアプリを使用してください。 + Syncthingから削除 + 削除してもファイルは消去されません。 + デバイス + デバイスはまだありません。 + 接続中 + オフライン + 最終接続:%1$s + このデバイスのID + もう一方のデバイスのSyncthingでデバイスを追加し、このコードをスキャンするかIDを貼り付けてください。その後、ここのWeb UIで承認してください。 + コピー + 共有 + 完了 + Syncthingをオフにしますか? + Syncthingはすぐに停止し、再びオンにするまで停止したままになります。同期済みのファイルはそのまま残ります。 + Syncthingの設定も削除 + このデバイスのID、フォルダ、デバイスを削除します。再びオンにすると最初からやり直しになります。 + オフにする + ポート + 同期 + ローカル探索 + 新しいポートを選択 + 起動のたびに再確認されます。 + 選び直す + リセット + APIキー + 再生成 + Syncthingをリセット + デバイスIDが新しくなり、フォルダとデバイスは削除されます。DroidDeckのストレージ内のファイルは保持されます。 + リセット + Syncthingをリセットしますか? + 実行 + 実行タイミング + DroidDeckを開いている間 + バックグラウンドで + Wi-Fiのみ + ゲーム中は一時停止 + ログ + このセッションのSyncthing出力 + 共有 + Syncthing + 先にLinux実行環境をセットアップしてください。 + 承認 + 「%1$s」の保存先 + フォルダを追加 + DroidDeckのストレージ内のフォルダを同期 + 選択 + 追加 + フォルダを追加できませんでした。 + 共有先 + デバイスはまだありません。このフォルダは後でWeb UIから共有できます。 + このフォルダを使用 + 新しいフォルダ + コンテナのホーム + Syncフォルダ + DroidDeckの共有ストレージ + %1$s上のDroidDeck + DroidDeckのアプリデータ + 新しいフォルダはDroidDeckのSyncフォルダに保存されます。DroidDeckの別のフォルダを同期するには、「Syncthing」タブの「フォルダを追加」を使用してください。 + Syncthing + 再読み込み + Syncthingは実行されていません。 Steamが不正な命令(SIGILL)で停止しました。CPU命令の互換性の問題か、クライアントのクラッシュの可能性があります。デバイスの詳細とあわせてログを共有してください。 Steamの設定 → Steamクライアント → x86-64(試験運用)を試すことができます。追加のダウンロードが必要で、動作が遅くなる場合があります。 Linuxグラフィックスドライバーが使用可能なVulkanデバイスを見つけられませんでした。選択したLinux GPUドライバーがこのGPUに対応しているか確認してください。 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index cf5a1a723..79cd6826c 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1754,6 +1754,93 @@ 키보드 + 터치패드 터미널 Deck 그립 + 터치패드 + 대시보드 + 아직 표시할 내용이 없습니다. + 다운로드 + 진행 중인 다운로드가 없습니다. + Steam + 셰이더 캐시 다운로드 중 + 일시 중지됨 + 시작 대기 중 + 알림 + DroidDeck 하단 화면 + 플레이하는 동안 메시지와 기타 알림을 여기에 표시합니다. 알림은 이 화면에만 표시되며 저장되지 않습니다. Android에서 한 번만 묻습니다. 이 화면에 알림 접근 권한 페이지가 열리면 DroidDeck을 켜세요. 그러면 이 페이지로 자동으로 돌아옵니다. + 알림 접근 허용 + 알림이 없습니다. + 알림 %1$d개 + 지우기 + 스크린샷 + FPS를 보려면 HUD를 켜야 합니다 + 속도 + 페달 + G-포스 + 타이밍 + 베스트 랩 + 타이어 °C + 스로틀 및 브레이크, 최근 10 s + 속도, %1$s + 기어 + 속도 단위 + 자동(%1$s) + Steam 클라이언트 ARM + Steam 클라이언트 x86 + 스로틀 + 브레이크 + 계기판 표시등 + 변속 + 핸드브레이크 + 피트 리미터 + TC + ABS + 터보 %1$s bar + 엔진 %1$s °C + 오일 %1$s °C + 파워 분배기 + 연료, t + 메인 %1$s t + 예비 %1$s t + 상단 막대나 계기판을 탭하면 스타일이 바뀝니다. + 계기판 데이터 보내기 + 자동 + 나중에 발견된 게임을 포함해 여기에 나열된 모든 게임에서 켭니다. 직접 끈 게임은 꺼진 상태로 유지됩니다. + 게임 자체의 데이터 출력 설정을 변경합니다. 다음에 게임을 시작할 때 적용되며, 끄면 이전 설정으로 되돌립니다. + 이 설정을 변경하려면 %1$s을(를) 종료하세요. + %1$s의 설정을 변경할 수 없습니다. + %1$s: 게임 설정에서 IP 127.0.0.1, 포트 %3$d(으)로 %2$s을(를) 켜세요. + Pictures/DroidDeck에 저장됩니다. Steam 스크린샷도 여기에 표시됩니다. + 스크린샷 찍기 + 스크린샷이 저장되었습니다. + 스크린샷을 찍을 수 없습니다. + 아직 스크린샷이 없습니다. + 채팅 및 음성 + 게임이 메인 화면에서 계속 실행되는 동안 여기에서 Steam 채팅을 읽고 답장하세요. Steam에 로그인된 계정을 사용합니다. + Steam에 아직 공유할 웹 로그인이 없으므로 여기에서 한 번 로그인하세요. + DroidDeck이 백그라운드에 있는 동안 Android가 채팅 페이지를 닫았습니다. DroidDeck으로 돌아오면 다시 열립니다. + Steam Chat 열기 + 채팅 닫기 + 마이크 켜짐 + 마이크 꺼짐 + 이 게임에서는 마이크가 꺼져 있습니다. 대화하려면 DroidDeck의 Steam 설정(오디오)에서 마이크를 켜고 게임을 다시 시작하세요. + 볼륨 %1$d%% + 게임 내 통계 + FPS + CPU + GPU + 배터리 + 랩 + 순위 + 현재 랩 + 지난 랩 + 연료 + 화물 + 파워 SYS · ENG · WEP + 법적 상태 + 잔액 + War Thunder + Elite Dangerous + 레이싱 게임의 데이터 출력(흔히 텔레메트리 또는 UDP라고 함)을 켜면 여기에서 실시간 계기판을 볼 수 있습니다. 데이터는 이 기기에만 남습니다. + 포트 %1$d에서 데이터를 받고 있지만(%3$d바이트 패킷 %2$d개), 이 페이지가 아직 읽을 수 없는 형식입니다. + 다른 프로그램이 이미 포트 %1$s을(를) 사용하고 있어 해당 게임을 여기에 표시할 수 없습니다. Linux 런타임 제거 중 @@ -3136,6 +3223,113 @@ 거리 확인이 통과되었습니다. 손 모델 체크섬 불일치 OpenCV를 로드할 수 없습니다. + Syncthing + Syncthing + Syncthing으로 동기화 + Syncthing-Fork를 기반으로 한 Syncthing 구현으로, DroidDeck 내부 파일 시스템의 파일을 다른 Syncthing 인스턴스와 동기화할 수 있습니다. DroidDeck 내부 파일 시스템에 저장한 파일에만 적용됩니다. + 이 빌드에는 Syncthing이 포함되어 있지 않습니다. + Syncthing 시작 중 + 시작 중 + 최신 상태 + 동기화 중 + 일시 중지됨 + 일시 중지 중 + 재개 중 + 확인 필요 + Syncthing이 중지됨 + 다시 시도 + Wi-Fi 연결 시까지 일시 중지됨 + 게임 중 일시 중지됨 + 폴더 %1$d개 · 기기 %3$d대 중 %2$d대 연결됨 + %1$s 남음 + 웹 UI 열기 + 일시 중지 + 재개 + 실행 중 + 끄면 Syncthing이 완전히 중지됩니다. + 이 기기 + 기기 ID + QR 코드 표시 + 고급 + 포트, 실행 시점, 초기화 + 열기 + 확인 대기 중 + %1$s에서 연결을 요청합니다 + %1$s에서 “%2$s” 폴더를 공유하려고 합니다 + 수락하면 DroidDeck에서 저장할 위치를 선택할 수 있습니다. + 웹 UI에서 검토 + 폴더 + 아직 폴더가 없습니다. + %1$s · %2$s + 최신 상태 + 동기화 중 %1$d%% + 스캔 중 + 일시 중지됨 + 동기화되지 않음 + 파일 %1$d개를 동기화하지 못했습니다 + 차단됨 + 이 폴더는 DroidDeck 저장 공간 밖에 있어서 DroidDeck이 일시 중지했습니다. DroidDeck 밖의 폴더를 동기화하려면 Syncthing-Fork 앱을 사용하세요. + Syncthing에서 제거 + 제거해도 파일은 삭제되지 않습니다. + 기기 + 아직 기기가 없습니다. + 연결됨 + 오프라인 + 마지막 접속: %1$s + 이 기기의 ID + 다른 기기의 Syncthing에서 기기를 추가하고 이 코드를 스캔하거나 ID를 붙여넣으세요. 그런 다음 여기 웹 UI에서 수락하세요. + 복사 + 공유 + 완료 + Syncthing을 끄시겠습니까? + Syncthing이 지금 중지되며 다시 켤 때까지 중지된 상태로 유지됩니다. 동기화된 파일은 그대로 유지됩니다. + Syncthing 구성도 삭제 + 이 기기의 ID, 폴더 및 기기를 제거합니다. 다시 켜면 처음부터 시작합니다. + 끄기 + 포트 + 동기화 + 로컬 탐색 + 새 포트 선택 + 시작할 때마다 다시 확인합니다. + 다시 선택 + 초기화 + API 키 + 다시 생성 + Syncthing 초기화 + 새 기기 ID가 생성되고 폴더와 기기가 제거됩니다. DroidDeck 저장 공간의 파일은 유지됩니다. + 초기화 + Syncthing을 초기화하시겠습니까? + 실행 + 실행 + DroidDeck이 열려 있는 동안 + 백그라운드에서 + Wi-Fi에서만 + 게임 중 일시 중지 + 로그 + 이 세션의 Syncthing 출력 + 공유 + Syncthing + 먼저 Linux 런타임을 설치하세요. + 수락 + “%1$s” 저장 위치 + 폴더 추가 + DroidDeck 저장 공간의 폴더 동기화 + 선택 + 추가 + 폴더를 추가할 수 없습니다. + 공유 대상 + 아직 기기가 없습니다. 나중에 웹 UI에서 이 폴더를 공유할 수 있습니다. + 이 폴더 사용 + 새 폴더 + 컨테이너 홈 + Sync 폴더 + DroidDeck 공유 저장 공간 + %1$s의 DroidDeck + DroidDeck 앱 데이터 + 새 폴더는 DroidDeck의 Sync 폴더에 저장됩니다. 다른 DroidDeck 폴더를 동기화하려면 Syncthing 탭에서 \"폴더 추가\"를 사용하세요. + Syncthing + 새로고침 + Syncthing이 실행 중이 아닙니다. Steam이 잘못된 명령어(SIGILL)로 중지되었습니다. CPU 명령어 호환성 문제이거나 클라이언트 충돌일 수 있습니다. 기기 정보와 함께 로그를 공유하세요. Steam 설정 → Steam 클라이언트 → x86-64(실험적)를 시도해 볼 수 있습니다. 추가 다운로드가 필요하며 더 느리게 실행될 수 있습니다. Linux 그래픽 드라이버가 사용 가능한 Vulkan 장치를 찾지 못했습니다. 선택한 Linux GPU 드라이버가 이 GPU를 지원하는지 확인하세요. diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 466180f52..69a8545f4 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -1754,6 +1754,93 @@ Papan kekunci + trackpad Terminal Pemegang Deck + trackpad + Papan pemuka + Belum ada apa-apa untuk dipaparkan di sini. + Muat turun + Tiada muat turun yang sedang berjalan. + Steam + Memuat turun cache shader + Dijeda + Menunggu untuk bermula + Pemberitahuan + Skrin bawah DroidDeck + Paparkan mesej dan pemberitahuan lain di sini semasa anda bermain. Ia hanya kekal pada skrin ini dan tidak disimpan. Android bertanya sekali sahaja: halaman akses pemberitahuannya dibuka pada skrin ini; hidupkan DroidDeck di sana dan halaman ini akan kembali dengan sendirinya. + Benarkan akses pemberitahuan + Tiada pemberitahuan. + %1$d pemberitahuan + Kosongkan + Tangkapan skrin + FPS memerlukan HUD dihidupkan + Kelajuan + Pedal + Daya G + Masa + Pusingan terbaik + Tayar °C + Pendikit dan brek, 10 s terakhir + Kelajuan, %1$s + Gear + Unit kelajuan + Automatik (%1$s) + Klien Steam ARM + Klien Steam x86 + Pendikit + Brek + Lampu penunjuk + Tukar gear + Brek tangan + Pengehad pit + TC + ABS + Turbo %1$s bar + Enjin %1$s °C + Minyak %1$s °C + Pengagih kuasa + Bahan api, t + Utama %1$s t + Simpanan %1$s t + Ketik jalur atas atau papan pemuka untuk menukar gayanya. + Hantar data papan pemuka + Automatik + Menghidupkannya untuk setiap permainan yang disenaraikan di sini, termasuk permainan yang ditemui kemudian. Permainan yang anda matikan kekal mati. + Ini menukar tetapan output data permainan itu sendiri. Ia berkuat kuasa apabila permainan dimulakan seterusnya; mematikannya memulihkan tetapan lama. + Tutup %1$s untuk menukar ini. + Tidak dapat menukar tetapan %1$s. + %1$s: hidupkan %2$s dalam tetapan permainan dengan IP 127.0.0.1 dan port %3$d. + Disimpan ke Pictures/DroidDeck. Tangkapan skrin Steam juga dipaparkan di sini. + Ambil tangkapan skrin + Tangkapan skrin disimpan. + Tidak dapat mengambil tangkapan skrin. + Belum ada tangkapan skrin. + Sembang & suara + Baca dan balas sembang Steam di sini sementara permainan terus berjalan pada skrin utama. Ia menggunakan akaun yang digunakan untuk log masuk ke Steam. + Steam belum mempunyai log masuk web untuk dikongsi, jadi log masuk di sini sekali. + Android menutup halaman sembang semasa DroidDeck berada di latar belakang. Ia dibuka semula apabila anda kembali ke DroidDeck. + Buka Steam Chat + Tutup sembang + Mikrofon hidup + Mikrofon mati + Mikrofon dimatikan untuk permainan ini. Untuk bercakap, hidupkan Mikrofon dalam tetapan Steam DroidDeck (Audio) dan mulakan permainan semula. + Kelantangan %1$d%% + Statistik dalam permainan + FPS + CPU + GPU + Bateri + Pusingan + Kedudukan + Pusingan ini + Pusingan terakhir + Bahan api + Kargo + Pip SYS · ENG · WEP + Status undang-undang + Baki + War Thunder + Elite Dangerous + Hidupkan output data permainan lumba (sering dipanggil telemetri atau UDP) untuk melihat papan pemuka langsung di sini. Data kekal pada peranti ini. + Port %1$d menerima data (%2$d paket bersaiz %3$d bait), tetapi dalam format yang belum dapat dibaca oleh halaman ini. + Program lain sudah menggunakan port %1$s, jadi permainan itu tidak dapat dipaparkan di sini. Mengalih keluar masa jalan Linux @@ -3219,6 +3306,113 @@ Pandangan yang disimpan boleh dikira semula. Mulakan penentukuran baharu untuk ditangkap dalam sistem koordinat penjejakan semasa. Ketakpadanan checksum model tangan OpenCV tidak dapat dimuatkan + Syncthing + Syncthing + Segerakkan dengan Syncthing + Berasaskan Syncthing-Fork, ini ialah pelaksanaan Syncthing yang membolehkan anda menyegerakkan fail dalam sistem fail dalaman DroidDeck dengan tika Syncthing lain. Ini hanya untuk fail yang anda simpan dalam sistem fail dalaman DroidDeck. + Binaan ini tidak menyertakan Syncthing. + Memulakan Syncthing + Memulakan + Terkini + Menyegerakkan + Dijeda + Menjeda + Menyambung semula + Perlu perhatian + Syncthing berhenti + Cuba lagi + Dijeda sehingga ada Wi-Fi + Dijeda semasa permainan + %1$d folder · %2$d daripada %3$d peranti disambungkan + %1$s lagi + Buka UI web + Jeda + Sambung semula + Berjalan + Matikan untuk menghentikan Syncthing sepenuhnya. + Peranti ini + ID Peranti + Tunjukkan QR + Lanjutan + Port, bila hendak dijalankan, tetapkan semula + Buka + Menunggu anda + %1$s mahu bersambung + %1$s mahu berkongsi “%2$s” + Terima untuk memilih tempat ia disimpan dalam DroidDeck. + Semak dalam UI web + Folder + Belum ada folder. + %1$s · %2$s + Terkini + Menyegerakkan %1$d%% + Mengimbas + Dijeda + Tidak segerak + %1$d fail tidak dapat disegerakkan + Disekat + Folder ini berada di luar storan DroidDeck, jadi DroidDeck menjedanya. Untuk menyegerakkan folder di luar DroidDeck, gunakan apl Syncthing-Fork. + Alih keluar daripada Syncthing + Mengalih keluarnya tidak memadamkan sebarang fail. + Peranti + Belum ada peranti. + Disambungkan + Luar talian + Kali terakhir dilihat %1$s + ID peranti ini + Pada peranti anda yang lain, tambah peranti dalam Syncthing dan imbas kod ini atau tampal ID. Kemudian terimanya di sini dalam UI web. + Salin + Kongsi + Selesai + Matikan Syncthing? + Syncthing berhenti sekarang dan kekal berhenti sehingga dihidupkan semula. Fail yang disegerakkan kekal di tempatnya. + Padamkan juga konfigurasi Syncthing + Mengalih keluar ID, folder dan peranti bagi peranti ini. Menghidupkannya semula akan bermula dari awal. + Matikan + Port + Penyegerakan + Penemuan setempat + Pilih port baharu + Disemak semula pada setiap permulaan. + Pilih semula + Tetapkan semula + Kunci API + Jana semula + Tetapkan semula Syncthing + ID peranti baharu, folder dan peranti dialih keluar. Fail dalam storan DroidDeck dikekalkan. + Tetapkan semula + Tetapkan semula Syncthing? + Berjalan + Jalankan + Semasa DroidDeck dibuka + Di latar belakang + Wi-Fi sahaja + Jeda semasa permainan + Log + Output Syncthing untuk sesi ini + Kongsi + Syncthing + Sediakan runtime Linux dahulu. + Terima + Simpan “%1$s” ke + Tambah folder + Segerakkan folder dalam storan DroidDeck + Pilih + Tambah + Folder tidak dapat ditambah. + Kongsi dengan + Belum ada peranti. Anda boleh berkongsi folder ini daripada UI web kemudian. + Gunakan folder ini + Folder baharu + Folder rumah kontena + Folder Sync + Storan kongsi DroidDeck + DroidDeck pada %1$s + Data apl DroidDeck + Folder baharu disimpan dalam folder Sync DroidDeck. Untuk menyegerakkan folder DroidDeck yang lain, gunakan Tambah folder pada tab Syncthing. + Syncthing + Muat semula + Syncthing tidak berjalan. Steam berhenti dengan arahan tidak sah (SIGILL). Ini mungkin masalah keserasian arahan CPU atau ranap klien. Kongsi log bersama butiran peranti anda. Anda boleh cuba Tetapan Steam → Klien Steam → x86-64 (eksperimen). Ia memerlukan muat turun tambahan dan mungkin berjalan lebih perlahan. Pemacu grafik Linux tidak menemui peranti Vulkan yang boleh digunakan. Pastikan pemacu GPU Linux yang dipilih menyokong GPU ini. diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index f2e20bbe2..e47b64970 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -761,7 +761,7 @@ Sesjonsoppførsel Gjelder nå og for fremtidige sesjoner i denne modusen. Neste sesjon - Support + Støtte Sesjonslogger Legg ved en feilrapport Del logger @@ -1776,6 +1776,93 @@ Tastatur + styreflate Terminal Deck-grep + styreflater + Dashbord + Ingenting å vise her ennå. + Nedlastinger + Ingen nedlastinger pågår. + Steam + Laster ned shaderbuffer + Pauset + Venter på å starte + Varslinger + DroidDeck nedre skjerm + Vis meldinger og andre varslinger her mens du spiller. De vises bare på denne skjermen og lagres ikke. Android spør én gang: siden for varseltilgang åpnes på denne skjermen; slå på DroidDeck der, så kommer denne siden tilbake av seg selv. + Tillat varseltilgang + Ingen varslinger. + %1$d varslinger + Fjern + Skjermbilder + FPS krever at HUD-en er på + Fart + Pedaler + G-kraft + Tider + Beste runde + Dekk °C + Gass og brems, siste 10 s + Fart, %1$s + Gir + Fartsenheter + Automatisk (%1$s) + Steam-klient ARM + Steam-klient x86 + Gass + Brems + Varsellamper + Girskift + Håndbrekk + Pitbegrenser + TC + ABS + Turbo %1$s bar + Motor %1$s °C + Olje %1$s °C + Kraftfordeler + Drivstoff, t + Hoved %1$s t + Reserve %1$s t + Trykk på den øverste stripen eller dashbordet for å endre stilen. + Send dashborddata + Automatisk + Slår det på for alle spill som er oppført her, også spill som blir funnet senere. Spill du slår av, forblir av. + Dette endrer spillets egen innstilling for datautgang. Det trer i kraft neste gang spillet starter; slår du det av, settes den gamle innstillingen tilbake. + Lukk %1$s for å endre dette. + Kunne ikke endre innstillingene for %1$s. + %1$s: slå på %2$s i spillets innstillinger med IP 127.0.0.1 og port %3$d. + Lagret i Pictures/DroidDeck. Skjermbilder fra Steam vises også her. + Ta skjermbilde + Skjermbildet er lagret. + Kunne ikke ta et skjermbilde. + Ingen skjermbilder ennå. + Chat & tale + Les og svar på Steam-chat her mens spillet fortsetter på hovedskjermen. Den bruker kontoen Steam er logget inn med. + Steam har ingen nettinnlogging å dele ennå, så logg inn her én gang. + Android lukket chatsiden mens DroidDeck var i bakgrunnen. Den åpnes igjen når du kommer tilbake til DroidDeck. + Åpne Steam Chat + Lukk chat + Mikrofon på + Mikrofon av + Mikrofonen er av for dette spillet. For å snakke, slå på Mikrofon i DroidDecks Steam-innstillinger (Lyd) og start spillet på nytt. + Volum %1$d%% + Statistikk i spillet + FPS + CPU + GPU + Batteri + Runde + Plassering + Denne runden + Forrige runde + Drivstoff + Last + Pips SYS · ENG · WEP + Juridisk status + Saldo + War Thunder + Elite Dangerous + Slå på datautgangen i et racingspill (ofte kalt telemetri eller UDP) for å se et direkte dashbord her. Dataene blir på denne enheten. + Port %1$d mottar data (%2$d pakker på %3$d byte), men i et format denne siden ikke kan lese ennå. + Et annet program bruker allerede port %1$s, så det spillet kan ikke vises her. Fjerner Linux-kjøretid @@ -3248,6 +3335,113 @@ Lagrede visninger kan beregnes på nytt. Start en ny kalibrering for å fange opp i gjeldende sporingskoordinatsystem. Uoverensstemmelse med håndmodellsjekksum OpenCV kunne ikke laste + Syncthing + Syncthing + Synkroniser med Syncthing + Dette er en Syncthing-implementasjon basert på Syncthing-Fork som lar deg synkronisere filer i DroidDecks interne filsystem med andre Syncthing-instanser. Dette gjelder bare filer du lagrer i DroidDecks interne filsystem. + Denne versjonen inneholder ikke Syncthing. + Starter Syncthing + Starter + Oppdatert + Synkroniserer + Pauset + Pauser + Gjenopptar + Trenger oppmerksomhet + Syncthing stoppet + Prøv igjen + Pauset til Wi-Fi er tilgjengelig + Pauset under spillet + %1$d mapper · %2$d av %3$d enheter tilkoblet + %1$s igjen + Åpne nettgrensesnittet + Pause + Gjenoppta + Kjører + Slå av for å stoppe Syncthing helt. + Denne enheten + Enhets-ID + Vis QR + Avansert + Porter, når den skal kjøre, nullstilling + Åpne + Venter på deg + %1$s vil koble til + %1$s vil dele «%2$s» + Godta for å velge hvor den lagres i DroidDeck. + Se gjennom i nettgrensesnittet + Mapper + Ingen mapper ennå. + %1$s · %2$s + Oppdatert + Synkroniserer %1$d%% + Skanner + Pauset + Ikke synkronisert + %1$d filer kunne ikke synkroniseres + Blokkert + Denne mappen ligger utenfor DroidDecks lagring, så DroidDeck har pauset den. Bruk Syncthing-Fork-appen for å synkronisere mapper utenfor DroidDeck. + Fjern fra Syncthing + Fjerning sletter ingen filer. + Enheter + Ingen enheter ennå. + Tilkoblet + Frakoblet + Sist sett %1$s + Denne enhetens ID + Legg til en enhet i Syncthing på den andre enheten din, og skann denne koden eller lim inn ID-en. Godta den deretter her i nettgrensesnittet. + Kopier + Del + Ferdig + Slå av Syncthing? + Syncthing stopper nå og forblir stoppet til den slås på igjen. Synkroniserte filer blir liggende der de er. + Slett også Syncthing-konfigurasjonen + Fjerner denne enhetens ID, mapper og enheter. Når den slås på igjen, starter den på nytt fra bunnen av. + Slå av + Porter + Synkronisering + Lokal oppdaging + Velg nye porter + Kontrolleres på nytt ved hver oppstart. + Velg på nytt + Nullstill + API-nøkkel + Generer på nytt + Nullstill Syncthing + Ny enhets-ID, mapper og enheter fjernes. Filer i DroidDecks lagring beholdes. + Nullstill + Nullstille Syncthing? + Kjøring + Kjør + Mens DroidDeck er åpen + I bakgrunnen + Bare Wi-Fi + Pause under spilling + Logger + Syncthing-utdata for denne sesjonen + Del + Syncthing + Konfigurer Linux-kjøretiden først. + Godta + Lagre «%1$s» i + Legg til mappe + Synkroniser en mappe i DroidDecks lagring + Velg + Legg til + Mappen kunne ikke legges til. + Del med + Ingen enheter ennå. Du kan dele denne mappen fra nettgrensesnittet senere. + Bruk denne mappen + Ny mappe + Hjemmemappe i containeren + Sync-mappe + DroidDecks delte lagring + DroidDeck på %1$s + DroidDeck-appdata + Nye mapper lagres i DroidDecks Sync-mappe. For å synkronisere en annen DroidDeck-mappe, bruk Legg til mappe på Syncthing-fanen. + Syncthing + Last på nytt + Syncthing kjører ikke. Steam stoppet med en ulovlig instruksjon (SIGILL). Dette kan være et kompatibilitetsproblem med CPU-instruksjoner eller et klientkrasj. Del loggene sammen med detaljer om enheten din. Du kan prøve Steam-innstillinger → Steam-klient → x86-64 (eksperimentell). Det krever en ekstra nedlasting og kan kjøre tregere. Linux-grafikkdriveren fant ingen brukbar Vulkan-enhet. Sjekk at den valgte Linux-GPU-driveren støtter denne GPU-en. diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index a5f072b2e..99483ded8 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -948,7 +948,7 @@ Installeren… Installeren Stel eerst Flatpak in - %1$s download + %1$s te downloaden ● Geïnstalleerd Kan de Flathub-pagina van deze app niet laden. Laden vanuit Flathub… @@ -1776,6 +1776,93 @@ Toetsenbord + trackpad Terminal Deck-grepen + trackpads + Dashboard + Hier is nog niets te zien. + Downloads + Er lopen geen downloads. + Steam + Shadercache downloaden + Gepauzeerd + Wacht om te starten + Meldingen + DroidDeck onderste scherm + Toon berichten en andere meldingen hier terwijl je speelt. Ze blijven alleen op dit scherm en worden niet opgeslagen. Android vraagt dit één keer: de pagina voor meldingstoegang opent op dit scherm; zet DroidDeck daar aan en deze pagina komt vanzelf terug. + Meldingstoegang toestaan + Geen meldingen. + %1$d meldingen + Wissen + Screenshots + Voor FPS moet de HUD aan staan + Snelheid + Pedalen + G-kracht + Tijden + Snelste ronde + Banden °C + Gas en rem, laatste 10 s + Snelheid, %1$s + Versnelling + Snelheidseenheid + Automatisch (%1$s) + Steam-client ARM + Steam-client x86 + Gas + Rem + Dashboard + Schakelen + Handrem + Pitlimiter + TC + ABS + Turbo %1$s bar + Motor %1$s °C + Olie %1$s °C + Energieverdeler + Brandstof, t + Hoofdtank %1$s t + Reserve %1$s t + Tik op de bovenste balk of het dashboard om de stijl te wijzigen. + Dashboardgegevens verzenden + Automatisch + Zet het aan voor elke game in deze lijst, ook voor games die later worden gevonden. Games die je uitzet, blijven uit. + Dit wijzigt de eigen instelling voor gegevensuitvoer van de game. Het werkt vanaf de volgende keer dat de game start; uitzetten zet de oude instelling terug. + Sluit %1$s om dit te wijzigen. + Kan de instellingen van %1$s niet wijzigen. + %1$s: zet %2$s aan in de instellingen van de game met IP 127.0.0.1 en poort %3$d. + Opgeslagen in Pictures/DroidDeck. Steam-screenshots verschijnen hier ook. + Screenshot maken + Screenshot opgeslagen. + Kan geen screenshot maken. + Nog geen screenshots. + Chat & spraak + Lees en beantwoord Steam-chats hier terwijl de game op het hoofdscherm doorloopt. Het gebruikt het account waarmee Steam is ingelogd. + Steam heeft nog geen webaanmelding om te delen, dus log hier één keer in. + Android heeft de chatpagina gesloten terwijl DroidDeck op de achtergrond was. Hij opent weer wanneer je terugkeert naar DroidDeck. + Steam Chat openen + Chat sluiten + Microfoon aan + Microfoon uit + De microfoon staat uit voor deze game. Zet Microfoon aan in de Steam-instellingen van DroidDeck (Audio) en start de game opnieuw om te praten. + Volume %1$d%% + In-game statistieken + FPS + CPU + GPU + Accu + Ronde + Positie + Deze ronde + Vorige ronde + Brandstof + Lading + Pips SYS · ENG · WEP + Juridische status + Saldo + War Thunder + Elite Dangerous + Zet de gegevensuitvoer van een racegame aan (vaak telemetrie of UDP genoemd) om hier een live dashboard te zien. De gegevens blijven op dit apparaat. + Poort %1$d ontvangt gegevens (%2$d pakketten van %3$d bytes), maar in een formaat dat deze pagina nog niet kan lezen. + Een ander programma gebruikt poort %1$s al, dus die game kan hier niet worden getoond. Linux-runtime verwijderen @@ -3246,6 +3333,113 @@ Opgeslagen weergaven kunnen opnieuw worden berekend. Start een nieuwe kalibratie om vast te leggen in het huidige volgcoördinatensysteem. Controlesom handmodel komt niet overeen OpenCV kon niet laden + Syncthing + Syncthing + Synchroniseren met Syncthing + Dit is een Syncthing-implementatie op basis van Syncthing-Fork waarmee je bestanden in het interne bestandssysteem van DroidDeck synchroniseert met andere Syncthing-installaties. Dit geldt alleen voor bestanden die je in het interne bestandssysteem van DroidDeck bewaart. + Deze build bevat geen Syncthing. + Syncthing starten + Starten + Bijgewerkt + Synchroniseren + Gepauzeerd + Pauzeren + Hervatten + Aandacht nodig + Syncthing is gestopt + Opnieuw proberen + Gepauzeerd tot er Wi-Fi is + Gepauzeerd tijdens de game + %1$d mappen · %2$d van %3$d apparaten verbonden + Nog %1$s + Webinterface openen + Pauzeren + Hervatten + Actief + Zet dit uit om Syncthing volledig te stoppen. + Dit apparaat + Apparaat-ID + QR-code tonen + Geavanceerd + Poorten, wanneer uitvoeren, resetten + Openen + Wacht op jou + %1$s wil verbinding maken + %1$s wil “%2$s” delen + Accepteer om te kiezen waar het in DroidDeck wordt opgeslagen. + Bekijken in webinterface + Mappen + Nog geen mappen. + %1$s · %2$s + Bijgewerkt + Synchroniseren %1$d%% + Scannen + Gepauzeerd + Niet gesynchroniseerd + %1$d bestanden konden niet worden gesynchroniseerd + Geblokkeerd + Deze map staat buiten de opslag van DroidDeck, dus DroidDeck heeft hem gepauzeerd. Gebruik de Syncthing-Fork-app om mappen buiten DroidDeck te synchroniseren. + Verwijderen uit Syncthing + Hierbij worden geen bestanden verwijderd. + Apparaten + Nog geen apparaten. + Verbonden + Offline + Laatst gezien: %1$s + ID van dit apparaat + Voeg op je andere apparaat in Syncthing een apparaat toe en scan deze code of plak de ID. Accepteer het daarna hier in de webinterface. + Kopiëren + Delen + Klaar + Syncthing uitzetten? + Syncthing stopt nu en blijft gestopt tot je het weer aanzet. Gesynchroniseerde bestanden blijven waar ze zijn. + Ook de configuratie van Syncthing verwijderen + Verwijdert de ID, mappen en apparaten van dit apparaat. Als je het weer aanzet, begint het opnieuw. + Uitzetten + Poorten + Synchronisatie + Lokale detectie + Nieuwe poorten kiezen + Wordt bij elke start opnieuw gecontroleerd. + Opnieuw kiezen + Resetten + API-sleutel + Opnieuw genereren + Syncthing resetten + Nieuwe apparaat-ID, mappen en apparaten verwijderd. Bestanden in de opslag van DroidDeck blijven bewaard. + Resetten + Syncthing resetten? + Uitvoering + Uitvoeren + Zolang DroidDeck open is + Op de achtergrond + Alleen via Wi-Fi + Pauzeren tijdens games + Logs + Syncthing-uitvoer van deze sessie + Delen + Syncthing + Stel eerst de Linux-runtime in. + Accepteren + “%1$s” opslaan in + Map toevoegen + Een map in de opslag van DroidDeck synchroniseren + Kiezen + Toevoegen + De map kan niet worden toegevoegd. + Delen met + Nog geen apparaten. Je kunt deze map later via de webinterface delen. + Deze map gebruiken + Nieuwe map + Thuismap van container + Sync-map + Gedeelde opslag van DroidDeck + DroidDeck op %1$s + App-gegevens van DroidDeck + Nieuwe mappen worden opgeslagen in de Sync-map van DroidDeck. Gebruik Map toevoegen op het tabblad Syncthing om een andere DroidDeck-map te synchroniseren. + Syncthing + Opnieuw laden + Syncthing is niet actief. Steam is gestopt met een ongeldige instructie (SIGILL). Dit kan een compatibiliteitsprobleem met CPU-instructies of een crash van de client zijn. Deel de logs met de gegevens van je apparaat. Je kunt Steam-instellingen → Steam-client → x86-64 (experimenteel) proberen. Dit vereist een extra download en kan trager draaien. De Linux-grafische driver kon geen bruikbaar Vulkan-apparaat vinden. Controleer of de gekozen Linux-GPU-driver deze GPU ondersteunt. diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 1e58d4ff2..9e75c495d 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1820,6 +1820,93 @@ Klawiatura + trackpad Terminal Uchwyty Deck + trackpady + Panel + Na razie nie ma tu nic do pokazania. + Pobieranie + Nic nie jest pobierane. + Steam + Pobieranie pamięci podręcznej shaderów + Wstrzymano + Oczekuje na rozpoczęcie + Powiadomienia + Dolny ekran DroidDeck + Wyświetlaj tu wiadomości i inne powiadomienia podczas gry. Pozostają tylko na tym ekranie i nie są zapisywane. Android zapyta raz: na tym ekranie otworzy się jego strona dostępu do powiadomień; włącz tam DroidDeck, a ta strona wróci sama. + Zezwól na dostęp do powiadomień + Brak powiadomień. + Powiadomienia: %1$d + Wyczyść + Zrzuty ekranu + FPS wymaga włączonego HUD + Prędkość + Pedały + Przeciążenie G + Czasy + Najlepsze okrążenie + Opony °C + Gaz i hamulec, ostatnie 10 s + Prędkość, %1$s + Bieg + Jednostki prędkości + Automatycznie (%1$s) + Klient Steam ARM + Klient Steam x86 + Gaz + Hamulec + Kontrolki + Zmiana biegu + Hamulec ręczny + Ogranicznik pit + TC + ABS + Turbo %1$s bar + Silnik %1$s °C + Olej %1$s °C + Rozdzielacz mocy + Paliwo, t + Główne %1$s t + Rezerwa %1$s t + Dotknij górnego paska lub zegarów, aby zmienić ich wygląd. + Wysyłaj dane do panelu + Automatycznie + Włącza wysyłanie dla każdej gry z tej listy, także dla gier znalezionych później. Gry, które wyłączysz, pozostaną wyłączone. + To zmienia ustawienie wyjścia danych w samej grze. Zadziała przy następnym uruchomieniu gry; wyłączenie przywraca poprzednie ustawienie. + Zamknij %1$s, aby to zmienić. + Nie udało się zmienić ustawień %1$s. + %1$s: włącz %2$s w ustawieniach gry, podając IP 127.0.0.1 i port %3$d. + Zapisywane w Pictures/DroidDeck. Zrzuty ekranu ze Steam też się tu pojawiają. + Zrób zrzut ekranu + Zapisano zrzut ekranu. + Nie udało się zrobić zrzutu ekranu. + Brak zrzutów ekranu. + Czat i głos + Czytaj czat Steam i odpowiadaj tutaj, a gra działa dalej na głównym ekranie. Używane jest konto, na które zalogowany jest Steam. + Steam nie ma jeszcze logowania przez przeglądarkę, które można udostępnić, więc zaloguj się tu raz. + Android zamknął stronę czatu, gdy DroidDeck działał w tle. Otworzy się ponownie, gdy wrócisz do DroidDeck. + Otwórz Steam Chat + Zamknij czat + Mikrofon wł. + Mikrofon wył. + Mikrofon jest wyłączony dla tej gry. Aby rozmawiać, włącz Mikrofon w ustawieniach Steam w DroidDeck (Dźwięk) i uruchom grę ponownie. + Głośność %1$d%% + Statystyki w grze + FPS + CPU + GPU + Bateria + Okrążenie + Pozycja + Bieżące okrążenie + Ostatnie okrążenie + Paliwo + Ładunek + Punkty mocy SYS · ENG · WEP + Status prawny + Saldo + War Thunder + Elite Dangerous + Włącz wyjście danych w grze wyścigowej (często nazywane telemetrią lub UDP), aby zobaczyć tu zegary na żywo. Dane nie opuszczają tego urządzenia. + Port %1$d odbiera dane (%2$d pakietów po %3$d bajtów), ale w formacie, którego ta strona jeszcze nie odczytuje. + Port %1$s jest już używany przez inny program, więc tej gry nie da się tu pokazać. Usuwanie środowiska Linux @@ -3316,6 +3403,113 @@ Zapisane widoki można przeliczyć. Rozpocznij nową kalibrację, aby przechwycić bieżący układ współrzędnych śledzenia. Niezgodność sumy kontrolnej modelu ręki Nie można załadować OpenCV + Syncthing + Syncthing + Synchronizuj przez Syncthing + Oparta na Syncthing-Fork implementacja Syncthing, która pozwala synchronizować pliki z wewnętrznego systemu plików DroidDeck z innymi instancjami Syncthing. Dotyczy tylko plików przechowywanych w wewnętrznym systemie plików DroidDeck. + Ta wersja nie zawiera Syncthing. + Uruchamianie Syncthing + Uruchamianie + Aktualne + Synchronizowanie + Wstrzymane + Wstrzymywanie + Wznawianie + Wymaga uwagi + Syncthing zatrzymany + Spróbuj ponownie + Wstrzymane do połączenia z Wi-Fi + Wstrzymane na czas gry + Foldery: %1$d · połączone urządzenia: %2$d z %3$d + Pozostało: %1$s + Otwórz interfejs WWW + Wstrzymaj + Wznów + Działa + Wyłącz, aby całkowicie zatrzymać Syncthing. + To urządzenie + ID urządzenia + Pokaż kod QR + Zaawansowane + Porty, kiedy uruchamiać, reset + Otwórz + Czeka na Ciebie + %1$s chce się połączyć + %1$s chce udostępnić „%2$s” + Zaakceptuj, aby wybrać miejsce zapisu w DroidDeck. + Sprawdź w interfejsie WWW + Foldery + Brak folderów. + %1$s · %2$s + Aktualny + Synchronizowanie %1$d%% + Skanowanie + Wstrzymany + Niezsynchronizowany + Nie udało się zsynchronizować plików: %1$d + Zablokowany + Ten folder znajduje się poza pamięcią DroidDeck, więc DroidDeck go wstrzymał. Aby synchronizować foldery spoza DroidDeck, użyj aplikacji Syncthing-Fork. + Usuń z Syncthing + Usunięcie nie kasuje żadnych plików. + Urządzenia + Brak urządzeń. + Połączone + Offline + Ostatnio widziane: %1$s + ID tego urządzenia + Na drugim urządzeniu dodaj urządzenie w Syncthing i zeskanuj ten kod lub wklej ID. Następnie zaakceptuj je tutaj w interfejsie WWW. + Kopiuj + Udostępnij + Gotowe + Wyłączyć Syncthing? + Syncthing zatrzyma się teraz i pozostanie zatrzymany, dopóki nie zostanie ponownie włączony. Zsynchronizowane pliki pozostaną na miejscu. + Usuń też konfigurację Syncthing + Usuwa ID tego urządzenia, foldery i urządzenia. Po ponownym włączeniu Syncthing zacznie od nowa. + Wyłącz + Porty + Synchronizacja + Wykrywanie lokalne + Wybierz nowe porty + Sprawdzane ponownie przy każdym uruchomieniu. + Wybierz ponownie + Reset + Klucz API + Wygeneruj ponownie + Zresetuj Syncthing + Nowe ID urządzenia, foldery i urządzenia zostaną usunięte. Pliki w pamięci DroidDeck zostaną zachowane. + Resetuj + Zresetować Syncthing? + Działanie + Uruchamiaj + Gdy DroidDeck jest otwarty + W tle + Tylko przez Wi-Fi + Wstrzymuj podczas gier + Logi + Dane wyjściowe Syncthing z tej sesji + Udostępnij + Syncthing + Najpierw skonfiguruj środowisko Linux. + Akceptuj + Zapisz „%1$s” w + Dodaj folder + Synchronizuj folder w pamięci DroidDeck + Wybierz + Dodaj + Nie udało się dodać folderu. + Udostępnij dla + Brak urządzeń. Ten folder możesz udostępnić później w interfejsie WWW. + Użyj tego folderu + Nowy folder + Katalog domowy kontenera + Folder Sync + Pamięć współdzielona DroidDeck + DroidDeck na %1$s + Dane aplikacji DroidDeck + Nowe foldery są zapisywane w folderze Sync DroidDeck. Aby synchronizować inny folder DroidDeck, użyj opcji Dodaj folder na karcie Syncthing. + Syncthing + Odśwież + Syncthing nie działa. Steam zatrzymał się na niedozwolonej instrukcji (SIGILL). Może to być problem ze zgodnością instrukcji CPU lub awaria klienta. Udostępnij logi wraz z danymi urządzenia. Możesz spróbować Ustawienia Steam → Klient Steam → x86-64 (eksperymentalny). Wymaga dodatkowego pobrania i może działać wolniej. Sterownik graficzny Linux nie znalazł używalnego urządzenia Vulkan. Sprawdź, czy wybrany sterownik GPU Linux obsługuje to GPU. diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 04cf83fed..811793d45 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -1790,6 +1790,93 @@ Teclado + trackpad Terminal Pegas + trackpads do Deck + Painel + Ainda não há nada para mostrar aqui. + Transferências + Nenhuma transferência em curso. + Steam + A transferir a cache de shaders + Em pausa + A aguardar início + Notificações + Ecrã inferior do DroidDeck + Mostre aqui mensagens e outras notificações enquanto joga. Ficam apenas neste ecrã e não são guardadas. O Android pergunta uma vez: a página de acesso às notificações abre neste ecrã; ative lá o DroidDeck e esta página volta automaticamente. + Permitir acesso às notificações + Sem notificações. + %1$d notificações + Limpar + Capturas de ecrã + O FPS requer o HUD ativado + Velocidade + Pedais + Força G + Tempos + Melhor volta + Pneus °C + Acelerador e travão, últimos 10 s + Velocidade, %1$s + Mudança + Unidade de velocidade + Automático (%1$s) + Cliente Steam ARM + Cliente Steam x86 + Acelerador + Travão + Painel + Engrenar mudança + Travão de mão + Limitador de pit + TC + ABS + Turbo %1$s bar + Motor %1$s °C + Óleo %1$s °C + Distribuidor de energia + Combustível, t + Principal %1$s t + Reserva %1$s t + Toque na faixa superior ou no painel para alterar o estilo. + Enviar dados para o painel + Automático + Ativa para todos os jogos aqui listados, incluindo os encontrados mais tarde. Os jogos que desativar permanecem desativados. + Isto altera a definição de saída de dados do próprio jogo. Aplica-se da próxima vez que o jogo iniciar; desativar repõe a definição anterior. + Feche %1$s para alterar isto. + Não foi possível alterar as definições de %1$s. + %1$s: ative %2$s nas definições do jogo com o IP 127.0.0.1 e a porta %3$d. + Guardadas em Pictures/DroidDeck. As capturas de ecrã do Steam também aparecem aqui. + Fazer captura de ecrã + Captura de ecrã guardada. + Não foi possível fazer a captura de ecrã. + Ainda não há capturas de ecrã. + Chat e voz + Leia e responda ao chat do Steam aqui enquanto o jogo continua a correr no ecrã principal. Utiliza a conta com que o Steam tem sessão iniciada. + O Steam ainda não tem uma sessão web para partilhar, por isso inicie sessão aqui uma vez. + O Android fechou a página do chat enquanto o DroidDeck estava em segundo plano. Volta a abrir quando regressar ao DroidDeck. + Abrir o Steam Chat + Fechar chat + Microfone ligado + Microfone desligado + O microfone está desligado neste jogo. Para falar, ative Microfone nas definições do Steam do DroidDeck (Áudio) e volte a iniciar o jogo. + Volume %1$d%% + Estatísticas no jogo + FPS + CPU + GPU + Bateria + Volta + Posição + Volta atual + Última volta + Combustível + Carga + Pips SYS · ENG · WEP + Estado legal + Saldo + War Thunder + Elite Dangerous + Ative a saída de dados de um jogo de corridas (muitas vezes chamada telemetria ou UDP) para ver aqui um painel em direto. Os dados ficam neste dispositivo. + A porta %1$d está a receber dados (%2$d pacotes de %3$d bytes), mas num formato que esta página ainda não consegue ler. + Outro programa já utiliza a porta %1$s, por isso esse jogo não pode aparecer aqui. A remover o runtime Linux @@ -3283,6 +3370,113 @@ As visualizações guardadas podem ser recalculadas. Inicie uma nova calibração para captar no sistema de coordenadas de seguimento atual. Incompatibilidade de checksum do modelo de mãos O OpenCV não conseguiu carregar + Syncthing + Syncthing + Sincronizar com o Syncthing + Baseada no Syncthing-Fork, esta é uma implementação do Syncthing que lhe permite sincronizar ficheiros do sistema de ficheiros interno do DroidDeck com outras instâncias do Syncthing. Aplica-se apenas aos ficheiros que guarda no sistema de ficheiros interno do DroidDeck. + Esta compilação não inclui o Syncthing. + A iniciar o Syncthing + A iniciar + Atualizado + A sincronizar + Em pausa + A pausar + A retomar + Requer atenção + O Syncthing parou + Tentar novamente + Em pausa até haver Wi-Fi + Em pausa durante o jogo + %1$d pastas · %2$d de %3$d dispositivos ligados + Faltam %1$s + Abrir interface web + Pausar + Retomar + Em execução + Desative para parar o Syncthing por completo. + Este dispositivo + ID do dispositivo + Mostrar QR + Avançado + Portas, quando executar, repor + Abrir + À sua espera + %1$s quer ligar-se + %1$s quer partilhar “%2$s” + Aceite para escolher onde é guardada no DroidDeck. + Rever na interface web + Pastas + Ainda não há pastas. + %1$s · %2$s + Atualizada + A sincronizar %1$d%% + A analisar + Em pausa + Dessincronizada + Não foi possível sincronizar %1$d ficheiros + Bloqueada + Esta pasta está fora do armazenamento do DroidDeck, por isso o DroidDeck colocou-a em pausa. Para sincronizar pastas fora do DroidDeck, utilize a app Syncthing-Fork. + Remover do Syncthing + Removê-la não elimina nenhum ficheiro. + Dispositivos + Ainda não há dispositivos. + Ligado + Offline + Visto pela última vez: %1$s + ID deste dispositivo + No outro dispositivo, adicione um dispositivo no Syncthing e leia este código ou cole o ID. Depois, aceite-o aqui na interface web. + Copiar + Partilhar + Concluído + Desativar o Syncthing? + O Syncthing para agora e continua parado até voltar a ser ativado. Os ficheiros sincronizados ficam onde estão. + Eliminar também a configuração do Syncthing + Remove o ID deste dispositivo, as pastas e os dispositivos. Ao voltar a ativá-lo, começa do zero. + Desativar + Portas + Sincronização + Descoberta local + Escolher novas portas + Verificadas novamente em cada início. + Escolher novamente + Repor + Chave da API + Regenerar + Repor o Syncthing + Novo ID do dispositivo; pastas e dispositivos removidos. Os ficheiros no armazenamento do DroidDeck são mantidos. + Repor + Repor o Syncthing? + Execução + Executar + Enquanto o DroidDeck estiver aberto + Em segundo plano + Apenas Wi-Fi + Pausar durante os jogos + Registos + Saída do Syncthing nesta sessão + Partilhar + Syncthing + Configure primeiro o ambiente de execução Linux. + Aceitar + Guardar “%1$s” em + Adicionar pasta + Sincronizar uma pasta no armazenamento do DroidDeck + Escolher + Adicionar + Não foi possível adicionar a pasta. + Partilhar com + Ainda não há dispositivos. Pode partilhar esta pasta mais tarde a partir da interface web. + Utilizar esta pasta + Nova pasta + Pasta pessoal do contentor + Pasta Sync + Armazenamento partilhado do DroidDeck + DroidDeck em %1$s + Dados da app DroidDeck + As novas pastas são guardadas na pasta Sync do DroidDeck. Para sincronizar outra pasta do DroidDeck, utilize Adicionar pasta no separador Syncthing. + Syncthing + Recarregar + O Syncthing não está em execução. O Steam parou com uma instrução ilegal (SIGILL). Pode ser um problema de compatibilidade de instruções da CPU ou uma falha do cliente. Partilhe os registos com os detalhes do seu dispositivo. Pode experimentar Definições do Steam → Cliente Steam → x86-64 (experimental). Requer uma transferência adicional e pode ser mais lento. O controlador gráfico Linux não encontrou um dispositivo Vulkan utilizável. Verifique se o controlador de GPU Linux selecionado suporta esta GPU. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 661354b3a..bef3c3302 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1790,6 +1790,93 @@ Teclado + trackpad Terminal Botões traseiros + trackpads do Deck + Painel + Nada para mostrar aqui ainda. + Downloads + Nenhum download em andamento. + Steam + Baixando o cache de shaders + Pausado + Aguardando para começar + Notificações + Tela inferior do DroidDeck + Mostre aqui mensagens e outras notificações enquanto você joga. Elas ficam só nesta tela e não são salvas. O Android pergunta uma vez: a página de acesso a notificações dele abre nesta tela; ative o DroidDeck lá e esta página volta sozinha. + Permitir acesso a notificações + Nenhuma notificação. + %1$d notificações + Limpar + Capturas de tela + O FPS precisa do HUD ativado + Velocidade + Pedais + Força G + Tempos + Melhor volta + Pneus °C + Acelerador e freio, últimos 10 s + Velocidade, %1$s + Marcha + Unidade de velocidade + Automático (%1$s) + Cliente Steam ARM + Cliente Steam x86 + Acelerador + Freio + Painel + Troca de marcha + Freio de mão + Limitador de pit + TC + ABS + Turbo %1$s bar + Motor %1$s °C + Óleo %1$s °C + Distribuidor de energia + Combustível, t + Principal %1$s t + Reserva %1$s t + Toque na faixa superior ou no painel para mudar o estilo. + Enviar dados para o painel + Automático + Ativa para todos os jogos listados aqui, incluindo os encontrados depois. Os jogos que você desativar continuam desativados. + Isso altera a configuração de saída de dados do próprio jogo. Vale a partir da próxima vez que o jogo iniciar; desativar restaura a configuração anterior. + Feche %1$s para alterar isso. + Não foi possível alterar as configurações de %1$s. + %1$s: ative %2$s nas configurações do jogo com o IP 127.0.0.1 e a porta %3$d. + Salvas em Pictures/DroidDeck. As capturas de tela da Steam também aparecem aqui. + Fazer captura de tela + Captura de tela salva. + Não foi possível fazer a captura de tela. + Nenhuma captura de tela ainda. + Chat e voz + Leia e responda o chat da Steam aqui enquanto o jogo continua rodando na tela principal. Usa a conta com que a Steam está conectada. + A Steam ainda não tem um login web para compartilhar, então entre aqui uma vez. + O Android fechou a página do chat enquanto o DroidDeck estava em segundo plano. Ela abre de novo quando você voltar ao DroidDeck. + Abrir o Steam Chat + Fechar chat + Microfone ligado + Microfone desligado + O microfone está desligado neste jogo. Para falar, ative Microfone nas configurações da Steam do DroidDeck (Áudio) e inicie o jogo de novo. + Volume %1$d%% + Estatísticas no jogo + FPS + CPU + GPU + Bateria + Volta + Posição + Volta atual + Última volta + Combustível + Carga + Pips SYS · ENG · WEP + Situação legal + Saldo + War Thunder + Elite Dangerous + Ative a saída de dados de um jogo de corrida (muitas vezes chamada de telemetria ou UDP) para ver um painel ao vivo aqui. Os dados ficam neste dispositivo. + A porta %1$d está recebendo dados (%2$d pacotes de %3$d bytes), mas em um formato que esta página ainda não consegue ler. + Outro programa já usa a porta %1$s, então esse jogo não pode aparecer aqui. Removendo o runtime Linux @@ -3283,6 +3370,113 @@ As visualizações salvas podem ser recalculadas. Inicie uma nova calibração para capturar no sistema de coordenadas de rastreamento atual. Incompatibilidade de soma de verificação do modelo de mãos OpenCV não pôde carregar + Syncthing + Syncthing + Sincronizar com o Syncthing + Baseada no Syncthing-Fork, esta é uma implementação do Syncthing que permite sincronizar arquivos do sistema de arquivos interno do DroidDeck com outras instâncias do Syncthing. Vale apenas para arquivos que você guarda no sistema de arquivos interno do DroidDeck. + Esta versão não inclui o Syncthing. + Iniciando o Syncthing + Iniciando + Atualizado + Sincronizando + Pausado + Pausando + Retomando + Requer atenção + O Syncthing parou + Tentar de novo + Pausado até conectar ao Wi-Fi + Pausado durante o jogo + %1$d pastas · %2$d de %3$d dispositivos conectados + Faltam %1$s + Abrir interface web + Pausar + Retomar + Em execução + Desative para parar o Syncthing completamente. + Este dispositivo + ID do dispositivo + Mostrar QR + Avançado + Portas, quando executar, redefinir + Abrir + Aguardando você + %1$s quer se conectar + %1$s quer compartilhar “%2$s” + Aceite para escolher onde salvar no DroidDeck. + Revisar na interface web + Pastas + Nenhuma pasta ainda. + %1$s · %2$s + Atualizada + Sincronizando %1$d%% + Verificando + Pausada + Fora de sincronia + %1$d arquivos não puderam ser sincronizados + Bloqueada + Esta pasta está fora do armazenamento do DroidDeck, então o DroidDeck a pausou. Para sincronizar pastas fora do DroidDeck, use o app Syncthing-Fork. + Remover do Syncthing + Remover não apaga nenhum arquivo. + Dispositivos + Nenhum dispositivo ainda. + Conectado + Offline + Visto por último: %1$s + ID deste dispositivo + No outro dispositivo, adicione um dispositivo no Syncthing e escaneie este código ou cole o ID. Depois, aceite-o aqui na interface web. + Copiar + Compartilhar + Concluído + Desativar o Syncthing? + O Syncthing para agora e continua parado até ser ativado de novo. Os arquivos sincronizados ficam onde estão. + Também apagar a configuração do Syncthing + Remove o ID deste dispositivo, as pastas e os dispositivos. Ao ativar de novo, ele começa do zero. + Desativar + Portas + Sincronização + Descoberta local + Escolher novas portas + Verificadas novamente a cada início. + Escolher de novo + Redefinir + Chave da API + Gerar novamente + Redefinir o Syncthing + Novo ID do dispositivo; pastas e dispositivos removidos. Os arquivos no armazenamento do DroidDeck são mantidos. + Redefinir + Redefinir o Syncthing? + Execução + Executar + Enquanto o DroidDeck estiver aberto + Em segundo plano + Somente Wi-Fi + Pausar durante os jogos + Logs + Saída do Syncthing nesta sessão + Compartilhar + Syncthing + Configure primeiro o runtime do Linux. + Aceitar + Salvar “%1$s” em + Adicionar pasta + Sincronizar uma pasta no armazenamento do DroidDeck + Escolher + Adicionar + Não foi possível adicionar a pasta. + Compartilhar com + Nenhum dispositivo ainda. Você pode compartilhar esta pasta pela interface web depois. + Usar esta pasta + Nova pasta + Pasta pessoal do contêiner + Pasta Sync + Armazenamento compartilhado do DroidDeck + DroidDeck em %1$s + Dados do app DroidDeck + Novas pastas são salvas na pasta Sync do DroidDeck. Para sincronizar outra pasta do DroidDeck, use Adicionar pasta na aba Syncthing. + Syncthing + Recarregar + O Syncthing não está em execução. A Steam parou com uma instrução ilegal (SIGILL). Pode ser um problema de compatibilidade de instruções da CPU ou uma falha do cliente. Compartilhe os logs com os detalhes do seu dispositivo. Você pode tentar Configurações da Steam → Cliente Steam → x86-64 (experimental). Requer um download adicional e pode rodar mais devagar. O driver gráfico do Linux não encontrou um dispositivo Vulkan utilizável. Verifique se o driver de GPU Linux selecionado é compatível com esta GPU. diff --git a/app/src/main/res/values-ro/gestures.xml b/app/src/main/res/values-ro/gestures.xml index 50ab4e0ab..da13d5f14 100644 --- a/app/src/main/res/values-ro/gestures.xml +++ b/app/src/main/res/values-ro/gestures.xml @@ -71,7 +71,7 @@ Backspace Enter Tab - Space + Spațiu Delete Insert Home diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 92e37b8ab..f832025ac 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -1613,7 +1613,7 @@ Ctrl Super Alt - Space + Spațiu AltGr Tab %1$s ✓ @@ -1798,6 +1798,93 @@ Tastatură + trackpad Terminal Mânere + trackpaduri Deck + Tablou de bord + Nimic de afișat aici încă. + Descărcări + Nicio descărcare în curs. + Steam + Se descarcă cache-ul de shadere + Întrerupt + Așteaptă să înceapă + Notificări + Ecranul de jos DroidDeck + Afișează aici mesajele și alte notificări în timp ce te joci. Rămân doar pe acest ecran și nu sunt salvate. Android întreabă o singură dată: pagina sa de acces la notificări se deschide pe acest ecran; activează DroidDeck acolo, iar această pagină revine singură. + Permite accesul la notificări + Nicio notificare. + Notificări: %1$d + Șterge + Capturi de ecran + FPS necesită HUD-ul activat + Viteză + Pedale + Forță G + Cronometrare + Cel mai bun tur + Anvelope °C + Accelerație și frână, ultimele 10 s + Viteză, %1$s + Treaptă + Unități de viteză + Automat (%1$s) + Client Steam ARM + Client Steam x86 + Accelerație + Frână + Bord + Schimbă treapta + Frână de mână + Limitator de boxe + TC + ABS + Turbo %1$s bar + Motor %1$s °C + Ulei %1$s °C + Distribuitor de putere + Combustibil, t + Principal %1$s t + Rezervă %1$s t + Atinge banda de sus sau bordul pentru a-i schimba stilul. + Trimite date pentru tabloul de bord + Automat + Activează trimiterea pentru fiecare joc din listă, inclusiv pentru jocurile găsite mai târziu. Jocurile pe care le dezactivezi rămân dezactivate. + Aceasta modifică setarea de ieșire a datelor din joc. Intră în vigoare la următoarea pornire a jocului; dezactivarea readuce setarea veche. + Închide %1$s pentru a schimba asta. + Nu s-au putut modifica setările pentru %1$s. + %1$s: activează %2$s în setările jocului, cu IP-ul 127.0.0.1 și portul %3$d. + Salvate în Pictures/DroidDeck. Capturile de ecran Steam apar și ele aici. + Fă o captură de ecran + Captură de ecran salvată. + Nu s-a putut face o captură de ecran. + Încă nu există capturi de ecran. + Chat și voce + Citește și răspunde aici la chatul Steam, în timp ce jocul continuă să ruleze pe ecranul principal. Folosește contul cu care este conectat Steam. + Steam nu are încă o conectare web de partajat, așa că conectează-te aici o dată. + Android a închis pagina de chat cât timp DroidDeck era în fundal. Se redeschide când revii în DroidDeck. + Deschide Steam Chat + Închide chatul + Microfon pornit + Microfon oprit + Microfonul este oprit pentru acest joc. Pentru a vorbi, activează Microfon în setările Steam din DroidDeck (Sunet) și pornește din nou jocul. + Volum %1$d%% + Statistici în joc + FPS + CPU + GPU + Baterie + Tur + Poziție + Turul curent + Ultimul tur + Combustibil + Încărcătură + Puncte SYS · ENG · WEP + Statut legal + Sold + War Thunder + Elite Dangerous + Activează ieșirea de date a unui joc de curse (numită adesea telemetrie sau UDP) pentru a vedea aici un bord în timp real. Datele rămân pe acest dispozitiv. + Portul %1$d primește date (%2$d pachete de %3$d octeți), dar într-un format pe care această pagină nu îl poate citi încă. + Alt program folosește deja portul %1$s, așa că acel joc nu poate fi afișat aici. Se elimină mediul de execuție Linux @@ -2749,7 +2836,7 @@ Nu acum Urmărirea mâinilor cu camera Folosește camera telefonului pentru gesturi cu mâinile goale sau pentru a poziționa controlerele Joy-Con asociate în VR. Controlerele Joy-Con oferă comenzi de rotație și comenzi de butoane. Pune pauză în timp ce ochelarii inteligenți urmăresc mâinile sau controlerele Joy-Con. Imaginile sunt procesate numai pe acest dispozitiv. - Camera + Cameră Acces la cameră Permite accesul la cameră pentru a putea începe urmărirea mâinilor. Accesul la cameră este dezactivat. Activează-l din setările Android. @@ -3284,6 +3371,113 @@ Vizualizările salvate pot fi recalculate. Începe o nouă calibrare pentru a captura în sistemul de coordonate de urmărire curent. Nepotrivire a sumei de control a modelului de mâini OpenCV nu s-a putut încărca + Syncthing + Syncthing + Sincronizează cu Syncthing + Bazată pe Syncthing-Fork, aceasta este o implementare Syncthing care îți permite să sincronizezi fișierele din sistemul de fișiere intern al DroidDeck cu alte instanțe Syncthing. Este doar pentru fișierele pe care le păstrezi în sistemul de fișiere intern al DroidDeck. + Această versiune nu include Syncthing. + Se pornește Syncthing + Se pornește + Actualizat + Se sincronizează + Întrerupt + Se întrerupe + Se reia + Necesită atenție + Syncthing s-a oprit + Încearcă din nou + Întrerupt până la Wi-Fi + Întrerupt în timpul jocului + Foldere: %1$d · dispozitive conectate: %2$d din %3$d + Rămas: %1$s + Deschide interfața web + Întrerupe + Reia + Rulează + Dezactivează pentru a opri complet Syncthing. + Acest dispozitiv + ID dispozitiv + Arată codul QR + Avansat + Porturi, când să ruleze, resetare + Deschide + Te așteaptă + %1$s vrea să se conecteze + %1$s vrea să partajeze „%2$s” + Acceptă pentru a alege unde este salvat în DroidDeck. + Verifică în interfața web + Foldere + Niciun folder încă. + %1$s · %2$s + Actualizat + Se sincronizează %1$d%% + Se scanează + Întrerupt + Nesincronizat + Fișiere care nu s-au putut sincroniza: %1$d + Blocat + Acest folder se află în afara spațiului de stocare DroidDeck, așa că DroidDeck l-a întrerupt. Pentru a sincroniza foldere din afara DroidDeck, folosește aplicația Syncthing-Fork. + Elimină din Syncthing + Eliminarea nu șterge niciun fișier. + Dispozitive + Niciun dispozitiv încă. + Conectat + Offline + Văzut ultima dată %1$s + ID-ul acestui dispozitiv + Pe celălalt dispozitiv, adaugă un dispozitiv în Syncthing și scanează acest cod sau lipește ID-ul. Apoi acceptă-l aici, în interfața web. + Copiază + Partajează + Gata + Dezactivezi Syncthing? + Syncthing se oprește acum și rămâne oprit până când îl activezi din nou. Fișierele sincronizate rămân unde sunt. + Șterge și configurația Syncthing + Elimină ID-ul acestui dispozitiv, folderele și dispozitivele. La reactivare, pornește de la zero. + Dezactivează + Porturi + Sincronizare + Descoperire locală + Alege porturi noi + Verificate din nou la fiecare pornire. + Alege din nou + Resetare + Cheie API + Regenerează + Resetează Syncthing + ID de dispozitiv nou, folderele și dispozitivele sunt eliminate. Fișierele din spațiul de stocare DroidDeck sunt păstrate. + Resetează + Resetezi Syncthing? + Rulare + Rulează + Cât timp DroidDeck este deschis + În fundal + Doar prin Wi-Fi + Întrerupe în timpul jocurilor + Jurnale + Ieșirea Syncthing pentru această sesiune + Partajează + Syncthing + Instalează mai întâi mediul de execuție Linux. + Acceptă + Salvează „%1$s” în + Adaugă folder + Sincronizează un folder din spațiul de stocare DroidDeck + Alege + Adaugă + Folderul nu a putut fi adăugat. + Partajează cu + Niciun dispozitiv încă. Poți partaja acest folder mai târziu din interfața web. + Folosește acest folder + Folder nou + Directorul home al containerului + Folderul Sync + Stocarea partajată DroidDeck + DroidDeck pe %1$s + Datele aplicației DroidDeck + Folderele noi sunt salvate în folderul Sync al DroidDeck. Pentru a sincroniza alt folder DroidDeck, folosește Adaugă folder din fila Syncthing. + Syncthing + Reîncarcă + Syncthing nu rulează. Steam s-a oprit cu o instrucțiune ilegală (SIGILL). Poate fi o problemă de compatibilitate a instrucțiunilor CPU sau o blocare a clientului. Partajează jurnalele împreună cu detaliile dispozitivului. Poți încerca Setări Steam → Client Steam → x86-64 (experimental). Necesită o descărcare suplimentară și poate rula mai lent. Driverul grafic Linux nu a găsit un dispozitiv Vulkan utilizabil. Verifică dacă driverul GPU Linux selectat acceptă acest GPU. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 211c1df95..a6a1d134e 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1820,6 +1820,93 @@ Клавиатура + тачпад Терминал Грипсы и тачпады Deck + Дашборд + Здесь пока ничего нет. + Загрузки + Нет активных загрузок. + Steam + Загрузка кэша шейдеров + Приостановлено + Ожидает начала + Уведомления + Нижний экран DroidDeck + Показывает сообщения и другие уведомления здесь, пока вы играете. Они отображаются только на этом экране и не сохраняются. Android спросит один раз: на этом экране откроется страница доступа к уведомлениям; включите там DroidDeck, и эта страница вернется сама. + Разрешить доступ к уведомлениям + Нет уведомлений. + Уведомлений: %1$d + Очистить + Скриншоты + Для FPS нужно включить HUD + Скорость + Педали + Перегрузка + Время + Лучший круг + Шины, °C + Газ и тормоз за последние 10 s + Скорость, %1$s + Передача + Единицы скорости + Автоматически (%1$s) + Клиент Steam ARM + Клиент Steam x86 + Газ + Тормоз + Приборная панель + Переключение + Ручник + Ограничитель скорости + TC + ABS + Турбо %1$s bar + Двигатель %1$s °C + Масло %1$s °C + Распределитель энергии + Топливо, t + Основное %1$s t + Резерв %1$s t + Коснитесь верхней полосы или панели, чтобы сменить ее стиль. + Передавать данные на дашборд + Автоматически + Включает передачу для всех игр в этом списке, в том числе найденных позже. Игры, для которых вы ее выключили, остаются выключенными. + Это меняет собственную настройку вывода данных в игре. Изменение вступит в силу при следующем запуске игры; при выключении восстанавливается прежняя настройка. + Закройте %1$s, чтобы изменить это. + Не удалось изменить настройки %1$s. + %1$s: включите %2$s в настройках игры, указав IP 127.0.0.1 и порт %3$d. + Сохраняются в Pictures/DroidDeck. Скриншоты Steam тоже отображаются здесь. + Сделать скриншот + Скриншот сохранен. + Не удалось сделать скриншот. + Скриншотов пока нет. + Чат и голос + Читайте чат Steam и отвечайте в нем здесь, пока игра продолжает работать на основном экране. Используется аккаунт, в который выполнен вход в Steam. + У Steam пока нет веб-входа, которым можно поделиться, поэтому войдите здесь один раз. + Android закрыл страницу чата, пока DroidDeck работал в фоне. Она откроется снова, когда вы вернетесь в DroidDeck. + Открыть Steam Chat + Закрыть чат + Микрофон вкл. + Микрофон выкл. + Микрофон для этой игры выключен. Чтобы говорить, включите «Микрофон» в настройках Steam в DroidDeck (Аудио) и запустите игру снова. + Громкость %1$d%% + Игровая статистика + FPS + CPU + GPU + Батарея + Круг + Позиция + Текущий круг + Прошлый круг + Топливо + Груз + Деления SYS · ENG · WEP + Правовой статус + Баланс + War Thunder + Elite Dangerous + Включите вывод данных в гоночной игре (часто он называется телеметрией или UDP), чтобы видеть здесь приборную панель в реальном времени. Данные остаются на этом устройстве. + Порт %1$d получает данные (пакетов: %2$d, размер: %3$d байт), но в формате, который эта страница пока не умеет читать. + Порт %1$s уже занят другой программой, поэтому эта игра не может отображаться здесь. Удаление среды выполнения Linux @@ -3316,6 +3403,113 @@ Сохраненные представления можно пересчитать. Запустите новую калибровку для захвата текущей системы координат отслеживания. Несоответствие контрольной суммы модели руки. OpenCV не удалось загрузить. + Syncthing + Syncthing + Синхронизация через Syncthing + Это реализация Syncthing на основе Syncthing-Fork, которая позволяет синхронизировать файлы из внутренней файловой системы DroidDeck с другими экземплярами Syncthing. Она работает только с файлами, которые хранятся во внутренней файловой системе DroidDeck. + Эта сборка не включает Syncthing. + Запуск Syncthing + Запуск + Актуально + Синхронизация + Приостановлено + Приостановка + Возобновление + Требует внимания + Syncthing остановлен + Повторить + Приостановлено до подключения к Wi-Fi + Приостановлено на время игры + Папок: %1$d · подключено устройств: %2$d из %3$d + Осталось: %1$s + Открыть веб-интерфейс + Приостановить + Возобновить + Работает + Выключите, чтобы полностью остановить Syncthing. + Это устройство + ID устройства + Показать QR-код + Дополнительно + Порты, режим работы, сброс + Открыть + Ожидает вашего ответа + %1$s хочет подключиться + %1$s хочет предоставить доступ к папке «%2$s» + Примите, чтобы выбрать, куда сохранить ее в DroidDeck. + Просмотреть в веб-интерфейсе + Папки + Папок пока нет. + %1$s · %2$s + Актуально + Синхронизация: %1$d%% + Сканирование + Приостановлено + Не синхронизировано + Не удалось синхронизировать файлов: %1$d + Заблокировано + Эта папка находится вне хранилища DroidDeck, поэтому DroidDeck приостановил ее. Чтобы синхронизировать папки вне DroidDeck, используйте приложение Syncthing-Fork. + Удалить из Syncthing + При удалении файлы не удаляются. + Устройства + Устройств пока нет. + Подключено + Не в сети + Последний раз в сети: %1$s + ID этого устройства + На другом устройстве добавьте устройство в Syncthing и отсканируйте этот код или вставьте ID. Затем примите его здесь, в веб-интерфейсе. + Копировать + Поделиться + Готово + Выключить Syncthing? + Syncthing остановится сейчас и не запустится, пока вы снова его не включите. Синхронизированные файлы останутся на месте. + Также удалить конфигурацию Syncthing + Удаляет ID этого устройства, папки и устройства. При повторном включении все начнется с нуля. + Выключить + Порты + Синхронизация + Локальное обнаружение + Выбрать новые порты + Проверяются заново при каждом запуске. + Выбрать заново + Сброс + Ключ API + Создать заново + Сбросить Syncthing + Новый ID устройства, папки и устройства удаляются. Файлы в хранилище DroidDeck сохраняются. + Сбросить + Сбросить Syncthing? + Работа + Работать + Пока DroidDeck открыт + В фоновом режиме + Только по Wi-Fi + Приостанавливать во время игр + Журналы + Вывод Syncthing за эту сессию + Поделиться + Syncthing + Сначала установите среду выполнения Linux. + Принять + Сохранить «%1$s» в + Добавить папку + Синхронизировать папку в хранилище DroidDeck + Выбрать + Добавить + Не удалось добавить папку. + Поделиться с + Устройств пока нет. Вы сможете предоставить доступ к этой папке позже в веб-интерфейсе. + Использовать эту папку + Новая папка + Домашняя папка контейнера + Папка Sync + Общее хранилище DroidDeck + DroidDeck на %1$s + Данные приложения DroidDeck + Новые папки сохраняются в папку Sync DroidDeck. Чтобы синхронизировать другую папку DroidDeck, используйте «Добавить папку» на вкладке Syncthing. + Syncthing + Перезагрузить + Syncthing не запущен. Steam остановился из-за недопустимой инструкции (SIGILL). Это может быть проблема совместимости инструкций CPU или сбой клиента. Отправьте журналы вместе с данными о вашем устройстве. Можно попробовать «Настройки Steam → Клиент Steam → x86-64 (экспериментально)». Потребуется дополнительная загрузка, и работа может быть медленнее. Графический драйвер Linux не нашёл подходящее устройство Vulkan. Проверьте, что выбранный драйвер GPU для Linux поддерживает этот GPU. diff --git a/app/src/main/res/values-sv/gestures.xml b/app/src/main/res/values-sv/gestures.xml index e8034b112..064ee4168 100644 --- a/app/src/main/res/values-sv/gestures.xml +++ b/app/src/main/res/values-sv/gestures.xml @@ -71,7 +71,7 @@ Backsteg Retur Tab - Space + Mellanslag Delete Insert Home diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 86cd982e7..a10876132 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -298,7 +298,7 @@ DualSense Xbox 360-kontroll 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. - Back + Bakåt Håll Bakåt för Steam-genvägar Håll Bakåt för att öppna Snabbåtkomst eller göra Steam-kombinationer med andra knappar. Stäng av för att ignorera hållen Bakåt; tryck och dubbeltryck fungerar fortfarande. Håll Back i 700 ms, eller håll Back och tryck på en annan knapp för Steam-genvägar. @@ -640,10 +640,10 @@ Batteritemperatur Prestandagraf Färg - Mint + Mintgrön Vit Himmelsblå - Amber + Bärnsten Lila Opacitet Storlek @@ -1597,7 +1597,7 @@ Ctrl Super Alt - Space + Mellanslag AltGr Tab %1$s ✓ @@ -1749,7 +1749,7 @@ Vit Lila Grön - Amber + Bärnsten Röd Rosa A-knappen @@ -1776,6 +1776,93 @@ Tangentbord + styrplatta Terminal Deck-grepp + styrplattor + Översikt + Inget att visa här än. + Nedladdningar + Inga nedladdningar pågår. + Steam + Laddar ned shadercache + Pausad + Väntar på att starta + Aviseringar + DroidDecks nedre skärm + Visa meddelanden och andra aviseringar här medan du spelar. De visas bara på den här skärmen och sparas inte. Android frågar en gång: sidan för aviseringsåtkomst öppnas på den här skärmen; slå på DroidDeck där så kommer den här sidan tillbaka av sig själv. + Tillåt aviseringsåtkomst + Inga aviseringar. + %1$d aviseringar + Rensa + Skärmbilder + FPS kräver att HUD:en är på + Hastighet + Pedaler + G-kraft + Tidtagning + Bästa varv + Däck °C + Gas och broms, senaste 10 s + Hastighet, %1$s + Växel + Hastighetsenhet + Automatisk (%1$s) + Steam-klient ARM + Steam-klient x86 + Gas + Broms + Instrumentpanel + Växla + Handbroms + Depåbegränsare + TC + ABS + Turbo %1$s bar + Motor %1$s °C + Olja %1$s °C + Kraftfördelare + Bränsle, t + Huvudtank %1$s t + Reserv %1$s t + Tryck på den övre remsan eller instrumentpanelen för att byta stil. + Skicka data till instrumentpanelen + Automatiskt + Slår på det för alla spel i listan, även spel som hittas senare. Spel som du stänger av förblir avstängda. + Detta ändrar spelets egen inställning för datautmatning. Ändringen gäller nästa gång spelet startar; när du stänger av den återställs den gamla inställningen. + Stäng %1$s för att ändra detta. + Det gick inte att ändra inställningarna för %1$s. + %1$s: slå på %2$s i spelets inställningar med IP 127.0.0.1 och port %3$d. + Sparas i Pictures/DroidDeck. Steam-skärmbilder visas också här. + Ta skärmbild + Skärmbilden har sparats. + Det gick inte att ta en skärmbild. + Inga skärmbilder än. + Chatt och röst + Läs och svara på Steam-chatten här medan spelet fortsätter att köras på huvudskärmen. Kontot som Steam är inloggat med används. + Steam har ingen webbinloggning att dela än, så logga in här en gång. + Android stängde chattsidan medan DroidDeck var i bakgrunden. Den öppnas igen när du kommer tillbaka till DroidDeck. + Öppna Steam Chat + Stäng chatten + Mikrofon på + Mikrofon av + Mikrofonen är avstängd för det här spelet. Slå på Mikrofon i DroidDecks Steam-inställningar (Ljud) och starta spelet igen för att prata. + Volym %1$d%% + Statistik i spelet + FPS + CPU + GPU + Batteri + Varv + Placering + Detta varv + Senaste varv + Bränsle + Last + Pips SYS · ENG · WEP + Rättslig status + Saldo + War Thunder + Elite Dangerous + Slå på datautmatning i ett racingspel (kallas ofta telemetri eller UDP) för att se en liveinstrumentpanel här. Datan stannar på den här enheten. + Port %1$d tar emot data (%2$d paket på %3$d byte), men i ett format som den här sidan inte kan läsa än. + Ett annat program använder redan port %1$s, så det spelet kan inte visas här. Tar bort Linux-körmiljön @@ -3247,6 +3334,113 @@ Sparade vyer kan räknas om. Starta en ny kalibrering för att fånga i det aktuella spårningskoordinatsystemet. Kontrollsumman för handmodellen inte överensstämmer OpenCV kunde inte laddas + Syncthing + Syncthing + Synkronisera med Syncthing + Det här är en Syncthing-implementering baserad på Syncthing-Fork som låter dig synkronisera filer i DroidDecks interna filsystem med andra Syncthing-instanser. Den gäller bara filer som du lagrar i DroidDecks interna filsystem. + Den här versionen innehåller inte Syncthing. + Startar Syncthing + Startar + Aktuell + Synkroniserar + Pausad + Pausar + Återupptar + Behöver åtgärdas + Syncthing har stoppats + Försök igen + Pausad tills Wi-Fi finns + Pausad under spelet + %1$d mappar · %2$d av %3$d enheter anslutna + %1$s kvar + Öppna webbgränssnittet + Pausa + Återuppta + Körs + Stäng av för att stoppa Syncthing helt. + Den här enheten + Enhets-ID + Visa QR-kod + Avancerat + Portar, när den ska köras, återställning + Öppna + Väntar på dig + %1$s vill ansluta + %1$s vill dela ”%2$s” + Acceptera för att välja var den sparas i DroidDeck. + Granska i webbgränssnittet + Mappar + Inga mappar än. + %1$s · %2$s + Aktuell + Synkroniserar %1$d %% + Skannar + Pausad + Inte synkroniserad + %1$d filer kunde inte synkroniseras + Blockerad + Den här mappen ligger utanför DroidDecks lagring, så DroidDeck har pausat den. Använd appen Syncthing-Fork för att synkronisera mappar utanför DroidDeck. + Ta bort från Syncthing + Inga filer raderas när den tas bort. + Enheter + Inga enheter än. + Ansluten + Offline + Senast sedd %1$s + Den här enhetens ID + Lägg till en enhet i Syncthing på din andra enhet och skanna den här koden eller klistra in ID:t. Acceptera den sedan här i webbgränssnittet. + Kopiera + Dela + Klar + Stänga av Syncthing? + Syncthing stoppas nu och förblir stoppat tills det slås på igen. Synkroniserade filer ligger kvar där de är. + Radera även Syncthings konfiguration + Tar bort enhetens ID, mappar och enheter. När du slår på det igen börjar det från början. + Stäng av + Portar + Synkronisering + Lokal identifiering + Välj nya portar + Kontrolleras igen vid varje start. + Välj om + Återställ + API-nyckel + Generera ny + Återställ Syncthing + Nytt enhets-ID, mappar och enheter tas bort. Filer i DroidDecks lagring behålls. + Återställ + Återställa Syncthing? + Körning + Kör + Medan DroidDeck är öppet + I bakgrunden + Endast Wi-Fi + Pausa under spel + Loggar + Syncthing-utdata för den här sessionen + Dela + Syncthing + Konfigurera Linux-körmiljön först. + Acceptera + Spara ”%1$s” i + Lägg till mapp + Synkronisera en mapp i DroidDecks lagring + Välj + Lägg till + Mappen kunde inte läggas till. + Dela med + Inga enheter än. Du kan dela den här mappen från webbgränssnittet senare. + Använd den här mappen + Ny mapp + Containerns hemmapp + Sync-mappen + DroidDecks delade lagring + DroidDeck på %1$s + DroidDecks appdata + Nya mappar sparas i DroidDecks Sync-mapp. Använd Lägg till mapp på fliken Syncthing för att synkronisera en annan DroidDeck-mapp. + Syncthing + Läs in igen + Syncthing körs inte. Steam stoppades av en otillåten instruktion (SIGILL). Det kan vara ett kompatibilitetsproblem med CPU-instruktioner eller en klientkrasch. Dela loggarna med uppgifter om din enhet. Du kan prova Steam-inställningar → Steam-klient → x86-64 (experimentellt). Det kräver en extra nedladdning och kan gå långsammare. Linux-grafikdrivrutinen hittade ingen användbar Vulkan-enhet. Kontrollera att den valda Linux-GPU-drivrutinen stöder den här GPU:n. diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 83c02b1c1..37c0bc635 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -455,7 +455,7 @@ นำเข้า AdrenoTools zip… ไม่เคย ตรวจสอบ Nightlies ครั้งล่าสุด %1$s - Components + คอมโพเนนต์ เกี่ยวกับ Components ตรวจสอบไดรเวอร์ใหม่ ตรวจสอบแพ็กเกจใหม่ใน Nightlies @@ -622,7 +622,7 @@ จำกัดเฟรมเรตของเกมนี้ หน้าจอจะเปลี่ยนไปใช้อัตรารีเฟรชที่หารด้วยค่าที่จำกัดได้ลงตัวหากมี ค่าอื่นอาจดูไม่สม่ำเสมอ เกม Steam ใช้ Frame Limit ของ Steam ในเมนู Quick Access เอฟเฟกต์ การควบคุม - Components + คอมโพเนนต์ คอนโทรลเลอร์ การตั้งค่า หน้า %1$s @@ -901,7 +901,7 @@ การกำหนดคอร์ CPU โฟลเดอร์ ROM ตำแหน่งที่เก็บเกมของโปรแกรมจำลอง - Components + คอมโพเนนต์ FEX, DXVK, VKD3D การควบคุม การกำหนดปุ่ม @@ -1060,7 +1060,7 @@ จับคู่แล้ว กรอกพอร์ตจากการตั้งค่า พบอุปกรณ์แล้ว กำลังปรับใช้การตั้งค่า… ไม่สามารถปรับใช้โดยอัตโนมัติได้ ตรวจสอบที่อยู่ด้านล่างแล้วลองใหม่ %1$s - Wireless debugging + การแก้ไขข้อบกพร่องแบบไร้สาย อัปเดตขีดจำกัดโพรเซสลูกแล้ว เปลี่ยนขีดจำกัดโพรเซสลูก หากไม่พบตัวเลือกนี้ ให้จับคู่ผ่าน Wireless debugging @@ -1255,7 +1255,7 @@ ไลบรารี เดสก์ท็อป สโตร์ - Components + คอมโพเนนต์ แอป การตั้งค่า การอัปเดต @@ -1754,6 +1754,93 @@ แป้นพิมพ์ + แทร็กแพด เทอร์มินัล ปุ่มด้านหลังและแทร็กแพดของ Deck + แดชบอร์ด + ยังไม่มีอะไรให้แสดงที่นี่ + ดาวน์โหลด + ไม่มีการดาวน์โหลดที่กำลังทำงาน + Steam + กำลังดาวน์โหลดแคชเชเดอร์ + หยุดชั่วคราว + กำลังรอเริ่ม + การแจ้งเตือน + หน้าจอล่างของ DroidDeck + แสดงข้อความและการแจ้งเตือนอื่นๆ ที่นี่ขณะที่คุณเล่น การแจ้งเตือนจะแสดงบนหน้าจอนี้เท่านั้นและจะไม่ถูกบันทึก Android จะถามเพียงครั้งเดียว: หน้าการเข้าถึงการแจ้งเตือนจะเปิดขึ้นบนหน้าจอนี้ เปิด DroidDeck ที่นั่น แล้วหน้านี้จะกลับมาเอง + อนุญาตการเข้าถึงการแจ้งเตือน + ไม่มีการแจ้งเตือน + การแจ้งเตือน %1$d รายการ + ล้าง + ภาพหน้าจอ + ต้องเปิด HUD เพื่อแสดง FPS + ความเร็ว + แป้นเหยียบ + แรง G + การจับเวลา + รอบที่ดีที่สุด + ยาง °C + คันเร่งและเบรก 10 s ล่าสุด + ความเร็ว, %1$s + เกียร์ + หน่วยความเร็ว + อัตโนมัติ (%1$s) + ไคลเอนต์ Steam ARM + ไคลเอนต์ Steam x86 + คันเร่ง + เบรก + แผงหน้าปัด + เปลี่ยนเกียร์ + เบรกมือ + ตัวจำกัดความเร็วพิต + TC + ABS + เทอร์โบ %1$s bar + เครื่องยนต์ %1$s °C + น้ำมันเครื่อง %1$s °C + ตัวกระจายพลังงาน + เชื้อเพลิง, t + หลัก %1$s t + สำรอง %1$s t + แตะแถบด้านบนหรือหน้าปัดเพื่อเปลี่ยนรูปแบบ + ส่งข้อมูลแดชบอร์ด + อัตโนมัติ + เปิดใช้สำหรับทุกเกมที่แสดงที่นี่ รวมถึงเกมที่พบในภายหลัง เกมที่คุณปิดไว้จะยังคงปิดอยู่ + การตั้งค่านี้จะเปลี่ยนการตั้งค่าการส่งออกข้อมูลของเกมเอง และมีผลในครั้งถัดไปที่เกมเริ่มทำงาน การปิดจะคืนค่าการตั้งค่าเดิม + ปิด %1$s เพื่อเปลี่ยนการตั้งค่านี้ + เปลี่ยนการตั้งค่าของ %1$s ไม่ได้ + %1$s: เปิด %2$s ในการตั้งค่าของเกม โดยใช้ IP 127.0.0.1 และพอร์ต %3$d + บันทึกไว้ที่ Pictures/DroidDeck ภาพหน้าจอของ Steam จะแสดงที่นี่ด้วย + ถ่ายภาพหน้าจอ + บันทึกภาพหน้าจอแล้ว + ถ่ายภาพหน้าจอไม่ได้ + ยังไม่มีภาพหน้าจอ + แชทและเสียง + อ่านและตอบแชท Steam ที่นี่ขณะที่เกมยังทำงานบนหน้าจอหลัก โดยใช้บัญชีที่ลงชื่อเข้าใช้ Steam อยู่ + Steam ยังไม่มีการลงชื่อเข้าใช้เว็บที่แชร์ได้ โปรดลงชื่อเข้าใช้ที่นี่หนึ่งครั้ง + Android ปิดหน้าแชทขณะที่ DroidDeck ทำงานอยู่ในเบื้องหลัง หน้าแชทจะเปิดอีกครั้งเมื่อคุณกลับมาที่ DroidDeck + เปิด Steam Chat + ปิดแชท + ไมค์เปิด + ไมค์ปิด + ไมค์ปิดอยู่สำหรับเกมนี้ หากต้องการพูด ให้เปิดไมโครโฟนในการตั้งค่า Steam ของ DroidDeck (เสียง) แล้วเริ่มเกมอีกครั้ง + ระดับเสียง %1$d%% + สถิติในเกม + FPS + CPU + GPU + แบตเตอรี่ + รอบ + อันดับ + รอบนี้ + รอบที่แล้ว + เชื้อเพลิง + สินค้า + พิป SYS · ENG · WEP + สถานะทางกฎหมาย + ยอดเงิน + War Thunder + Elite Dangerous + เปิดการส่งออกข้อมูลของเกมแข่งรถ (มักเรียกว่าเทเลเมทรีหรือ UDP) เพื่อดูหน้าปัดแบบสดที่นี่ ข้อมูลจะอยู่บนอุปกรณ์นี้เท่านั้น + พอร์ต %1$d ได้รับข้อมูล (%2$d แพ็กเก็ต ขนาด %3$d ไบต์) แต่อยู่ในรูปแบบที่หน้านี้ยังอ่านไม่ได้ + มีโปรแกรมอื่นใช้พอร์ต %1$s อยู่แล้ว เกมนั้นจึงแสดงที่นี่ไม่ได้ กำลังลบรันไทม์ Linux @@ -2274,7 +2361,7 @@ การเลือกของ Steam ต่อเกม เสียง DirectAudio - Components + คอมโพเนนต์ FEX, DXVK, VKD3D เอนจิน %1$s · เนทีฟ @@ -3220,6 +3307,113 @@ มุมมองที่บันทึกไว้สามารถคำนวณใหม่ได้ เริ่มการสอบเทียบใหม่เพื่อบันทึกในระบบพิกัดการติดตามปัจจุบัน ผลรวมตรวจสอบแบบจำลองมือไม่ตรงกัน OpenCV ไม่สามารถโหลดได้ + Syncthing + Syncthing + ซิงค์ด้วย Syncthing + นี่คือ Syncthing ที่อิงจาก Syncthing-Fork ซึ่งช่วยให้คุณซิงค์ไฟล์ในระบบไฟล์ภายในของ DroidDeck กับ Syncthing เครื่องอื่นได้ ใช้ได้เฉพาะกับไฟล์ที่คุณจัดเก็บไว้ในระบบไฟล์ภายในของ DroidDeck เท่านั้น + บิลด์นี้ไม่มี Syncthing + กำลังเริ่ม Syncthing + กำลังเริ่ม + เป็นปัจจุบัน + กำลังซิงค์ + หยุดชั่วคราว + กำลังหยุดชั่วคราว + กำลังทำต่อ + ต้องตรวจสอบ + Syncthing หยุดทำงาน + ลองอีกครั้ง + หยุดชั่วคราวจนกว่าจะเชื่อมต่อ Wi-Fi + หยุดชั่วคราวระหว่างเล่นเกม + %1$d โฟลเดอร์ · เชื่อมต่อแล้ว %2$d จาก %3$d อุปกรณ์ + เหลืออีก %1$s + เปิดเว็บ UI + หยุดชั่วคราว + ทำต่อ + กำลังทำงาน + ปิดเพื่อหยุด Syncthing ทั้งหมด + อุปกรณ์นี้ + ID อุปกรณ์ + แสดง QR + ขั้นสูง + พอร์ต เวลาที่ทำงาน การรีเซ็ต + เปิด + รอคุณอยู่ + %1$s ต้องการเชื่อมต่อ + %1$s ต้องการแชร์ “%2$s” + ยอมรับเพื่อเลือกตำแหน่งที่จะบันทึกใน DroidDeck + ตรวจสอบในเว็บ UI + โฟลเดอร์ + ยังไม่มีโฟลเดอร์ + %1$s · %2$s + เป็นปัจจุบัน + กำลังซิงค์ %1$d%% + กำลังสแกน + หยุดชั่วคราว + ไม่ตรงกัน + ซิงค์ไม่ได้ %1$d ไฟล์ + ถูกบล็อก + โฟลเดอร์นี้อยู่นอกพื้นที่จัดเก็บของ DroidDeck ดังนั้น DroidDeck จึงหยุดโฟลเดอร์นี้ชั่วคราว หากต้องการซิงค์โฟลเดอร์ที่อยู่นอก DroidDeck ให้ใช้แอป Syncthing-Fork + นำออกจาก Syncthing + การนำออกจะไม่ลบไฟล์ใดๆ + อุปกรณ์ + ยังไม่มีอุปกรณ์ + เชื่อมต่อแล้ว + ออฟไลน์ + พบล่าสุด %1$s + ID ของอุปกรณ์นี้ + บนอุปกรณ์อีกเครื่อง ให้เพิ่มอุปกรณ์ใน Syncthing แล้วสแกนโค้ดนี้หรือวาง ID จากนั้นยอมรับอุปกรณ์นั้นที่นี่ในเว็บ UI + คัดลอก + แชร์ + เสร็จสิ้น + ปิด Syncthing? + Syncthing จะหยุดทันทีและหยุดอยู่จนกว่าจะเปิดอีกครั้ง ไฟล์ที่ซิงค์แล้วจะยังอยู่ที่เดิม + ลบการกำหนดค่าของ Syncthing ด้วย + นำ ID ของอุปกรณ์นี้ โฟลเดอร์ และอุปกรณ์ออก เมื่อเปิดอีกครั้งจะเริ่มต้นใหม่ทั้งหมด + ปิด + พอร์ต + ซิงค์ + การค้นหาในเครือข่ายท้องถิ่น + เลือกพอร์ตใหม่ + ตรวจสอบอีกครั้งทุกครั้งที่เริ่ม + เลือกใหม่ + รีเซ็ต + คีย์ API + สร้างใหม่ + รีเซ็ต Syncthing + ได้ ID อุปกรณ์ใหม่ และนำโฟลเดอร์กับอุปกรณ์ออก ไฟล์ในพื้นที่จัดเก็บของ DroidDeck จะยังคงอยู่ + รีเซ็ต + รีเซ็ต Syncthing? + การทำงาน + ทำงาน + ขณะที่ DroidDeck เปิดอยู่ + ในเบื้องหลัง + Wi-Fi เท่านั้น + หยุดชั่วคราวระหว่างเล่นเกม + ล็อก + เอาต์พุตของ Syncthing สำหรับเซสชันนี้ + แชร์ + Syncthing + ตั้งค่ารันไทม์ Linux ก่อน + ยอมรับ + บันทึก “%1$s” ไปที่ + เพิ่มโฟลเดอร์ + ซิงค์โฟลเดอร์ในพื้นที่จัดเก็บของ DroidDeck + เลือก + เพิ่ม + เพิ่มโฟลเดอร์ไม่ได้ + แชร์กับ + ยังไม่มีอุปกรณ์ คุณสามารถแชร์โฟลเดอร์นี้จากเว็บ UI ได้ในภายหลัง + ใช้โฟลเดอร์นี้ + โฟลเดอร์ใหม่ + โฮมของคอนเทนเนอร์ + โฟลเดอร์ Sync + พื้นที่จัดเก็บที่ใช้ร่วมกันของ DroidDeck + DroidDeck บน %1$s + ข้อมูลแอป DroidDeck + โฟลเดอร์ใหม่จะบันทึกไว้ในโฟลเดอร์ Sync ของ DroidDeck หากต้องการซิงค์โฟลเดอร์อื่นของ DroidDeck ให้ใช้ เพิ่มโฟลเดอร์ ในแท็บ Syncthing + Syncthing + โหลดซ้ำ + Syncthing ไม่ได้ทำงานอยู่ Steam หยุดทำงานเพราะคำสั่งที่ไม่ถูกต้อง (SIGILL) อาจเป็นปัญหาความเข้ากันได้ของคำสั่ง CPU หรือไคลเอนต์ขัดข้อง แชร์บันทึกพร้อมรายละเอียดอุปกรณ์ของคุณ คุณสามารถลองการตั้งค่า Steam → ไคลเอนต์ Steam → x86-64 (ทดลอง) ต้องดาวน์โหลดเพิ่มเติมและอาจทำงานช้าลง ไดรเวอร์กราฟิก Linux ไม่พบอุปกรณ์ Vulkan ที่ใช้งานได้ ตรวจสอบว่าไดรเวอร์ GPU ของ Linux ที่เลือกรองรับ GPU นี้ @@ -3240,7 +3434,7 @@ ไม่เกิน 4 ไม่เกิน 1 ไม่มี - Wireless debugging + การแก้ไขข้อบกพร่องแบบไร้สาย จับคู่ DroidDeck กับ Wireless debugging ของ Android สำหรับตัวเลือกด้านบน ขีดจำกัดโพรเซสลูกยังคงปิดอยู่ จับคู่ กู้คืนมุมมองที่บันทึกไว้แล้ว %1$d รายการ คำนวณการสอบเทียบเพื่อลองอีกครั้ง หรือเริ่มชุดใหม่เพื่อบันทึกข้อมูลเพิ่มเติม diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 555cccde8..a2df0d777 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1768,6 +1768,93 @@ Klavye + dokunmatik yüzey Terminal Deck tutamakları + dokunmatik yüzeyler + Gösterge paneli + Burada henüz gösterilecek bir şey yok. + İndirmeler + Devam eden indirme yok. + Steam + Gölgelendirici önbelleği indiriliyor + Duraklatıldı + Başlamayı bekliyor + Bildirimler + DroidDeck alt ekranı + Oynarken mesajları ve diğer bildirimleri burada görün. Yalnızca bu ekranda kalırlar ve kaydedilmezler. Android bunu bir kez sorar: bildirim erişimi sayfası bu ekranda açılır; orada DroidDeck\'i açın, bu sayfa kendiliğinden geri gelir. + Bildirim erişimine izin ver + Bildirim yok. + %1$d bildirim + Temizle + Ekran görüntüleri + FPS için HUD açık olmalı + Hız + Pedallar + G kuvveti + Zamanlama + En iyi tur + Lastikler °C + Gaz ve fren, son 10 s + Hız, %1$s + Vites + Hız birimi + Otomatik (%1$s) + Steam İstemcisi ARM + Steam İstemcisi x86 + Gaz + Fren + Gösterge ışıkları + Vites uyarısı + El freni + Pit hız sınırlayıcı + TC + ABS + Turbo %1$s bar + Motor %1$s °C + Yağ %1$s °C + Güç dağıtıcı + Yakıt, t + Ana %1$s t + Yedek %1$s t + Stilini değiştirmek için üst şeride veya göstergeye dokunun. + Gösterge verilerini gönder + Otomatik + Sonradan bulunanlar dahil, burada listelenen her oyun için açar. Kapattığınız oyunlar kapalı kalır. + Bu, oyunun kendi veri çıkışı ayarını değiştirir. Oyun bir sonraki başlatılışında geçerli olur; kapatmak eski ayarı geri getirir. + Bunu değiştirmek için %1$s oyununu kapatın. + %1$s ayarları değiştirilemedi. + %1$s: oyunun ayarlarında IP 127.0.0.1 ve %3$d portuyla %2$s seçeneğini açın. + Pictures/DroidDeck klasörüne kaydedilir. Steam ekran görüntüleri de burada görünür. + Ekran görüntüsü al + Ekran görüntüsü kaydedildi. + Ekran görüntüsü alınamadı. + Henüz ekran görüntüsü yok. + Sohbet ve ses + Oyun ana ekranda çalışmaya devam ederken Steam sohbetini burada okuyun ve yanıtlayın. Steam\'de oturum açılmış hesabı kullanır. + Steam\'in paylaşabileceği bir web oturumu henüz yok, bu yüzden burada bir kez oturum açın. + DroidDeck arka plandayken Android sohbet sayfasını kapattı. DroidDeck\'e döndüğünüzde yeniden açılır. + Steam Chat\'i aç + Sohbeti kapat + Mikrofon açık + Mikrofon kapalı + Bu oyun için mikrofon kapalı. Konuşmak için DroidDeck\'in Steam ayarlarında (Ses) Mikrofon\'u açın ve oyunu yeniden başlatın. + Ses düzeyi %1$d%% + Oyun içi istatistikler + FPS + CPU + GPU + Pil + Tur + Sıra + Bu tur + Son tur + Yakıt + Kargo + Güç SYS · ENG · WEP + Yasal durum + Bakiye + War Thunder + Elite Dangerous + Burada canlı bir gösterge görmek için yarış oyununun veri çıkışını (genellikle telemetri veya UDP olarak geçer) açın. Veriler bu cihazda kalır. + %1$d portu veri alıyor (%3$d baytlık %2$d paket), ancak bu sayfanın henüz okuyamadığı bir biçimde. + %1$s portunu zaten başka bir program kullanıyor, bu yüzden o oyun burada gösterilemez. Linux çalışma ortamı kaldırılıyor @@ -3245,6 +3332,113 @@ Kaydedilen görünümler yeniden hesaplanabilir. Geçerli izleme koordinat sistemini yakalamak için yeni bir kalibrasyon başlatın. El modeli sağlama toplamı uyuşmazlığı OpenCV yüklenemedi + Syncthing + Syncthing + Syncthing ile eşitle + Syncthing-Fork tabanlı bu Syncthing uygulaması, DroidDeck\'in dahili dosya sistemindeki dosyaları diğer Syncthing örnekleriyle eşitlemenizi sağlar. Yalnızca DroidDeck\'in dahili dosya sisteminde sakladığınız dosyalar içindir. + Bu sürüm Syncthing içermiyor. + Syncthing başlatılıyor + Başlatılıyor + Güncel + Eşitleniyor + Duraklatıldı + Duraklatılıyor + Sürdürülüyor + İlgilenmeniz gerekiyor + Syncthing durdu + Tekrar dene + Wi-Fi\'ye bağlanana kadar duraklatıldı + Oyun sırasında duraklatıldı + %1$d klasör · %3$d cihazdan %2$d tanesi bağlı + %1$s kaldı + Web arayüzünü aç + Duraklat + Sürdür + Çalışıyor + Syncthing\'i tamamen durdurmak için kapatın. + Bu cihaz + Cihaz Kimliği + QR kodu göster + Gelişmiş + Bağlantı noktaları, ne zaman çalışacağı, sıfırlama + Aç + Onayınızı bekliyor + %1$s bağlanmak istiyor + %1$s “%2$s” klasörünü paylaşmak istiyor + DroidDeck\'te nereye kaydedileceğini seçmek için kabul edin. + Web arayüzünde incele + Klasörler + Henüz klasör yok. + %1$s · %2$s + Güncel + Eşitleniyor %%%1$d + Taranıyor + Duraklatıldı + Eşitlenmemiş + %1$d dosya eşitlenemedi + Engellendi + Bu klasör DroidDeck\'in depolama alanının dışında olduğu için DroidDeck onu duraklattı. DroidDeck dışındaki klasörleri eşitlemek için Syncthing-Fork uygulamasını kullanın. + Syncthing\'den kaldır + Kaldırmak hiçbir dosyayı silmez. + Cihazlar + Henüz cihaz yok. + Bağlı + Çevrimdışı + Son görülme: %1$s + Bu cihazın kimliği + Diğer cihazınızda Syncthing\'e bir cihaz ekleyin ve bu kodu tarayın ya da kimliği yapıştırın. Ardından burada web arayüzünden kabul edin. + Kopyala + Paylaş + Bitti + Syncthing kapatılsın mı? + Syncthing şimdi durur ve yeniden açılana kadar kapalı kalır. Eşitlenen dosyalar yerinde kalır. + Syncthing yapılandırmasını da sil + Bu cihazın kimliğini, klasörleri ve cihazları kaldırır. Yeniden açıldığında sıfırdan başlar. + Kapat + Bağlantı noktaları + Eşitleme + Yerel keşif + Yeni bağlantı noktaları seç + Her başlangıçta yeniden denetlenir. + Yeniden seç + Sıfırla + API anahtarı + Yeniden oluştur + Syncthing\'i sıfırla + Yeni cihaz kimliği oluşturulur, klasörler ve cihazlar kaldırılır. DroidDeck\'in depolama alanındaki dosyalar korunur. + Sıfırla + Syncthing sıfırlansın mı? + Çalışma + Çalıştırma + DroidDeck açıkken + Arka planda + Yalnızca Wi-Fi + Oyunlar sırasında duraklat + Günlükler + Bu oturumun Syncthing çıktısı + Paylaş + Syncthing + Önce Linux çalışma zamanını yükleyin. + Kabul et + “%1$s” klasörünün kaydedileceği yer + Klasör ekle + DroidDeck\'in depolama alanındaki bir klasörü eşitleyin + Seç + Ekle + Klasör eklenemedi. + Paylaşılacak cihazlar + Henüz cihaz yok. Bu klasörü daha sonra web arayüzünden paylaşabilirsiniz. + Bu klasörü kullan + Yeni klasör + Konteyner ana dizini + Sync klasörü + DroidDeck paylaşılan depolama alanı + %1$s üzerindeki DroidDeck + DroidDeck uygulama verileri + Yeni klasörler DroidDeck\'in Sync klasörüne kaydedilir. Başka bir DroidDeck klasörünü eşitlemek için Syncthing sekmesindeki Klasör ekle seçeneğini kullanın. + Syncthing + Yeniden yükle + Syncthing çalışmıyor. Steam geçersiz bir komutla durdu (SIGILL). Bu bir CPU komut uyumluluğu sorunu veya istemci çökmesi olabilir. Günlükleri cihaz bilgilerinizle paylaşın. Steam ayarları → Steam istemcisi → x86-64 (deneysel) seçeneğini deneyebilirsiniz. Ek bir indirme gerektirir ve daha yavaş çalışabilir. Linux grafik sürücüsü kullanılabilir bir Vulkan cihazı bulamadı. Seçilen Linux GPU sürücüsünün bu GPU\'yu desteklediğini kontrol edin. diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 4e3a617d4..c43da44fe 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1820,6 +1820,93 @@ Клавіатура + трекпад Термінал Задні кнопки + трекпади Deck + Панель + Тут поки нічого показати. + Завантаження + Немає активних завантажень. + Steam + Завантаження кешу шейдерів + Призупинено + Очікує початку + Сповіщення + Нижній екран DroidDeck + Показуйте тут повідомлення та інші сповіщення під час гри. Вони лишаються лише на цьому екрані й не зберігаються. Android запитає один раз: сторінка доступу до сповіщень відкриється на цьому екрані; увімкніть там DroidDeck, і ця сторінка повернеться сама. + Дозволити доступ до сповіщень + Немає сповіщень. + Сповіщень: %1$d + Очистити + Знімки екрана + Для FPS потрібно ввімкнути HUD + Швидкість + Педалі + Перевантаження + Час + Найкраще коло + Шини °C + Газ і гальмо, останні 10 s + Швидкість, %1$s + Передача + Одиниці швидкості + Автоматично (%1$s) + Клієнт Steam ARM + Клієнт Steam x86 + Газ + Гальмо + Панель приладів + Перемикання + Ручне гальмо + Обмежувач піт-лейну + TC + ABS + Турбо %1$s bar + Двигун %1$s °C + Олива %1$s °C + Розподільник енергії + Пальне, t + Основне %1$s t + Резерв %1$s t + Торкніться верхньої смуги або панелі, щоб змінити її стиль. + Надсилати дані для панелі + Автоматично + Вмикає це для кожної гри в цьому списку, зокрема для знайдених пізніше. Ігри, які ви вимкнете, лишаються вимкненими. + Це змінює власне налаштування виведення даних у грі. Зміна діє з наступного запуску гри; вимкнення повертає попереднє налаштування. + Закрийте %1$s, щоб змінити це. + Не вдалося змінити налаштування %1$s. + %1$s: увімкніть %2$s у налаштуваннях гри з IP 127.0.0.1 і портом %3$d. + Зберігаються в Pictures/DroidDeck. Знімки екрана Steam також показуються тут. + Зробити знімок екрана + Знімок екрана збережено. + Не вдалося зробити знімок екрана. + Ще немає знімків екрана. + Чат і голос + Читайте чат Steam і відповідайте в ньому тут, поки гра працює на головному екрані. Використовується обліковий запис, у який виконано вхід у Steam. + У Steam ще немає вебвходу, яким можна поділитися, тож увійдіть тут один раз. + Android закрив сторінку чату, поки DroidDeck був у фоні. Вона знову відкриється, коли ви повернетеся до DroidDeck. + Відкрити Steam Chat + Закрити чат + Мікрофон увімкнено + Мікрофон вимкнено + Для цієї гри мікрофон вимкнено. Щоб говорити, увімкніть «Мікрофон» у налаштуваннях Steam у DroidDeck (Звук) і запустіть гру знову. + Гучність %1$d%% + Ігрова статистика + FPS + CPU + GPU + Батарея + Коло + Позиція + Поточне коло + Останнє коло + Пальне + Вантаж + Енергія SYS · ENG · WEP + Правовий статус + Баланс + War Thunder + Elite Dangerous + Увімкніть виведення даних у гоночній грі (часто це називається телеметрією або UDP), щоб бачити тут живу панель. Дані лишаються на цьому пристрої. + Порт %1$d отримує дані (пакетів: %2$d, по %3$d байт), але у форматі, який ця сторінка ще не вміє читати. + Порт %1$s уже використовує інша програма, тому ця гра не може відображатися тут. Видалення середовища Linux @@ -3319,6 +3406,113 @@ Збережені перегляди можна перерахувати. Розпочніть нове калібрування для запису в поточній системі координат відстеження. Невідповідність контрольної суми моделі руки Не вдалося завантажити OpenCV + Syncthing + Syncthing + Синхронізація через Syncthing + Це реалізація Syncthing на основі Syncthing-Fork, яка дає змогу синхронізувати файли у внутрішній файловій системі DroidDeck з іншими екземплярами Syncthing. Вона працює лише з файлами, які ви зберігаєте у внутрішній файловій системі DroidDeck. + Ця збірка не містить Syncthing. + Запуск Syncthing + Запуск + Синхронізовано + Синхронізація + Призупинено + Призупинення + Відновлення + Потрібна увага + Syncthing зупинено + Повторити + Призупинено до підключення до Wi-Fi + Призупинено на час гри + Папок: %1$d · підключено пристроїв: %2$d з %3$d + Залишилося: %1$s + Відкрити вебінтерфейс + Призупинити + Відновити + Працює + Вимкніть, щоб повністю зупинити Syncthing. + Цей пристрій + ID пристрою + Показати QR-код + Додатково + Порти, коли працювати, скидання + Відкрити + Чекає на вас + %1$s хоче підключитися + %1$s хоче надати доступ до «%2$s» + Прийміть, щоб вибрати, куди зберігати її в DroidDeck. + Переглянути у вебінтерфейсі + Папки + Папок ще немає. + %1$s · %2$s + Синхронізовано + Синхронізація: %1$d%% + Сканування + Призупинено + Не синхронізовано + Не вдалося синхронізувати файлів: %1$d + Заблоковано + Ця папка розташована поза сховищем DroidDeck, тому DroidDeck її призупинив. Щоб синхронізувати папки поза DroidDeck, скористайтеся додатком Syncthing-Fork. + Видалити із Syncthing + Видалення не стирає жодних файлів. + Пристрої + Пристроїв ще немає. + Підключено + Не в мережі + Востаннє в мережі: %1$s + ID цього пристрою + На іншому пристрої додайте пристрій у Syncthing і відскануйте цей код або вставте ID. Потім прийміть його тут у вебінтерфейсі. + Копіювати + Поділитися + Готово + Вимкнути Syncthing? + Syncthing зупиниться зараз і не працюватиме, доки ви його знову не ввімкнете. Синхронізовані файли залишаться на місці. + Також видалити конфігурацію Syncthing + Видаляє ID цього пристрою, папки й пристрої. Після повторного ввімкнення все почнеться з нуля. + Вимкнути + Порти + Синхронізація + Локальне виявлення + Вибрати нові порти + Перевіряється під час кожного запуску. + Вибрати знову + Скидання + Ключ API + Згенерувати знову + Скинути Syncthing + Новий ID пристрою, папки й пристрої буде видалено. Файли у сховищі DroidDeck збережуться. + Скинути + Скинути Syncthing? + Робота + Режим роботи + Поки DroidDeck відкрито + У фоновому режимі + Лише Wi-Fi + Призупиняти під час ігор + Журнали + Вивід Syncthing за цей сеанс + Поділитися + Syncthing + Спершу встановіть середовище Linux. + Прийняти + Куди зберегти «%1$s» + Додати папку + Синхронізувати папку у сховищі DroidDeck + Вибрати + Додати + Не вдалося додати папку. + Надати доступ + Пристроїв ще немає. Ви зможете надати доступ до цієї папки пізніше у вебінтерфейсі. + Використати цю папку + Нова папка + Домашня папка контейнера + Папка Sync + Спільне сховище DroidDeck + DroidDeck на %1$s + Дані додатка DroidDeck + Нові папки зберігаються в папці Sync у DroidDeck. Щоб синхронізувати іншу папку DroidDeck, скористайтеся кнопкою «Додати папку» на вкладці Syncthing. + Syncthing + Перезавантажити + Syncthing не запущено. Steam зупинився через недопустиму інструкцію (SIGILL). Це може бути проблема сумісності інструкцій CPU або збій клієнта. Надішліть журнали разом із даними про ваш пристрій. Можна спробувати «Налаштування Steam → Клієнт Steam → x86-64 (експериментально)». Потрібне додаткове завантаження, і робота може бути повільнішою. Графічний драйвер Linux не знайшов придатного пристрою Vulkan. Перевірте, чи вибраний драйвер GPU для Linux підтримує цей GPU. diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 931860a60..bc472c323 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1754,6 +1754,93 @@ Bàn phím + trackpad Terminal Grip + trackpad Deck + Bảng điều khiển + Chưa có gì để hiển thị ở đây. + Tải xuống + Không có lượt tải nào đang chạy. + Steam + Đang tải bộ nhớ đệm shader + Đã tạm dừng + Đang chờ bắt đầu + Thông báo + Màn hình dưới DroidDeck + Hiển thị tin nhắn và các thông báo khác ở đây khi bạn chơi. Chúng chỉ ở trên màn hình này và không được lưu. Android chỉ hỏi một lần: trang quyền truy cập thông báo sẽ mở trên màn hình này; hãy bật DroidDeck ở đó và trang này sẽ tự quay lại. + Cho phép truy cập thông báo + Không có thông báo. + %1$d thông báo + Xóa + Ảnh chụp màn hình + Cần bật HUD để xem FPS + Tốc độ + Bàn đạp + Lực G + Thời gian + Vòng nhanh nhất + Lốp °C + Ga và phanh, 10 s gần nhất + Tốc độ, %1$s + Số + Đơn vị tốc độ + Tự động (%1$s) + Client Steam ARM + Client Steam x86 + Ga + Phanh + Đèn báo + Sang số + Phanh tay + Giới hạn tốc độ pit + TC + ABS + Turbo %1$s bar + Động cơ %1$s °C + Dầu %1$s °C + Bộ phân phối năng lượng + Nhiên liệu, t + Chính %1$s t + Dự trữ %1$s t + Nhấn vào dải phía trên hoặc bảng đồng hồ để đổi kiểu hiển thị. + Gửi dữ liệu bảng đồng hồ + Tự động + Bật cho mọi game trong danh sách này, kể cả game được tìm thấy sau này. Game bạn đã tắt sẽ vẫn tắt. + Thao tác này thay đổi thiết lập xuất dữ liệu của chính game. Thay đổi có hiệu lực từ lần chạy game tiếp theo; tắt đi sẽ khôi phục thiết lập cũ. + Hãy đóng %1$s để thay đổi mục này. + Không thể thay đổi thiết lập của %1$s. + %1$s: hãy bật %2$s trong thiết lập của game với IP 127.0.0.1 và cổng %3$d. + Được lưu vào Pictures/DroidDeck. Ảnh chụp màn hình Steam cũng hiện ở đây. + Chụp màn hình + Đã lưu ảnh chụp màn hình. + Không thể chụp màn hình. + Chưa có ảnh chụp màn hình. + Chat & thoại + Đọc và trả lời chat Steam ở đây trong khi game vẫn chạy trên màn hình chính. Dùng tài khoản đang đăng nhập Steam. + Steam chưa có phiên đăng nhập web để chia sẻ, vì vậy hãy đăng nhập ở đây một lần. + Android đã đóng trang chat khi DroidDeck chạy nền. Trang sẽ mở lại khi bạn quay lại DroidDeck. + Mở Steam Chat + Đóng chat + Micro bật + Micro tắt + Micro đang tắt cho game này. Để nói chuyện, hãy bật Micro trong thiết lập Steam của DroidDeck (Âm thanh) rồi khởi động lại game. + Âm lượng %1$d%% + Thông số trong game + FPS + CPU + GPU + Pin + Vòng + Vị trí + Vòng này + Vòng trước + Nhiên liệu + Hàng hóa + Năng lượng SYS · ENG · WEP + Tình trạng pháp lý + Số dư + War Thunder + Elite Dangerous + Bật xuất dữ liệu của game đua xe (thường gọi là telemetry hoặc UDP) để xem bảng đồng hồ trực tiếp ở đây. Dữ liệu chỉ ở trên thiết bị này. + Cổng %1$d đang nhận dữ liệu (%2$d gói, mỗi gói %3$d byte), nhưng ở định dạng mà trang này chưa đọc được. + Một chương trình khác đang dùng cổng %1$s, nên game đó không thể hiển thị ở đây. Đang gỡ runtime Linux @@ -3220,6 +3307,113 @@ Các lượt xem đã lưu có thể được tính toán lại. Bắt đầu hiệu chuẩn mới để chụp trong hệ tọa độ theo dõi hiện tại. Tổng kiểm tra mô hình bàn tay không khớp OpenCV không thể tải + Syncthing + Syncthing + Đồng bộ với Syncthing + Dựa trên Syncthing-Fork, đây là bản triển khai Syncthing cho phép bạn đồng bộ các tệp trong hệ thống tệp nội bộ của DroidDeck với các phiên bản Syncthing khác. Tính năng này chỉ dành cho các tệp bạn lưu trong hệ thống tệp nội bộ của DroidDeck. + Bản dựng này không có Syncthing. + Đang khởi động Syncthing + Đang khởi động + Đã cập nhật + Đang đồng bộ + Đã tạm dừng + Đang tạm dừng + Đang tiếp tục + Cần chú ý + Syncthing đã dừng + Thử lại + Tạm dừng cho đến khi có Wi-Fi + Tạm dừng trong khi chơi game + %1$d thư mục · %2$d/%3$d thiết bị đã kết nối + Còn %1$s + Mở giao diện web + Tạm dừng + Tiếp tục + Đang chạy + Tắt để dừng hoàn toàn Syncthing. + Thiết bị này + ID thiết bị + Hiện mã QR + Nâng cao + Cổng, thời điểm chạy, đặt lại + Mở + Đang chờ bạn + %1$s muốn kết nối + %1$s muốn chia sẻ “%2$s” + Chấp nhận để chọn nơi lưu thư mục này trong DroidDeck. + Xem lại trong giao diện web + Thư mục + Chưa có thư mục nào. + %1$s · %2$s + Đã cập nhật + Đang đồng bộ %1$d%% + Đang quét + Đã tạm dừng + Chưa đồng bộ + %1$d tệp không thể đồng bộ + Bị chặn + Thư mục này nằm ngoài bộ nhớ của DroidDeck nên DroidDeck đã tạm dừng nó. Để đồng bộ các thư mục bên ngoài DroidDeck, hãy dùng app Syncthing-Fork. + Xóa khỏi Syncthing + Xóa khỏi Syncthing không xóa tệp nào. + Thiết bị + Chưa có thiết bị nào. + Đã kết nối + Ngoại tuyến + Lần cuối thấy %1$s + ID của thiết bị này + Trên thiết bị còn lại, thêm thiết bị trong Syncthing rồi quét mã này hoặc dán ID. Sau đó chấp nhận ở đây trong giao diện web. + Sao chép + Chia sẻ + Xong + Tắt Syncthing? + Syncthing sẽ dừng ngay và không chạy cho đến khi được bật lại. Các tệp đã đồng bộ vẫn giữ nguyên vị trí. + Xóa cả cấu hình của Syncthing + Xóa ID của thiết bị này, các thư mục và thiết bị. Khi bật lại sẽ bắt đầu từ đầu. + Tắt + Cổng + Đồng bộ + Khám phá cục bộ + Chọn cổng mới + Được kiểm tra lại mỗi lần khởi động. + Chọn lại + Đặt lại + Khóa API + Tạo lại + Đặt lại Syncthing + ID thiết bị mới, các thư mục và thiết bị bị xóa. Tệp trong bộ nhớ của DroidDeck được giữ lại. + Đặt lại + Đặt lại Syncthing? + Chạy + Chạy + Khi DroidDeck đang mở + Trong nền + Chỉ Wi-Fi + Tạm dừng khi chơi game + Log + Đầu ra của Syncthing trong phiên này + Chia sẻ + Syncthing + Cài runtime Linux trước. + Chấp nhận + Lưu “%1$s” vào + Thêm thư mục + Đồng bộ một thư mục trong bộ nhớ của DroidDeck + Chọn + Thêm + Không thể thêm thư mục. + Chia sẻ với + Chưa có thiết bị nào. Bạn có thể chia sẻ thư mục này từ giao diện web sau. + Dùng thư mục này + Thư mục mới + Thư mục home của container + Thư mục Sync + Bộ nhớ dùng chung DroidDeck + DroidDeck trên %1$s + Dữ liệu app DroidDeck + Thư mục mới được lưu trong thư mục Sync của DroidDeck. Để đồng bộ một thư mục DroidDeck khác, hãy dùng Thêm thư mục trong thẻ Syncthing. + Syncthing + Tải lại + Syncthing không chạy. Steam đã dừng do lệnh không hợp lệ (SIGILL). Đây có thể là vấn đề tương thích lệnh CPU hoặc client bị lỗi. Hãy chia sẻ nhật ký cùng thông tin thiết bị của bạn. Bạn có thể thử Thiết lập Steam → Client Steam → x86-64 (thử nghiệm). Cần tải thêm và có thể chạy chậm hơn. Driver đồ họa Linux không tìm thấy thiết bị Vulkan dùng được. Hãy kiểm tra driver GPU Linux đã chọn có hỗ trợ GPU này không. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 8cde3f2ca..9477b8a4d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -88,6 +88,7 @@ 设为 1 可让此游戏禁用 droiddeck-fsync;存在对应包时使用 droiddeck-esync,否则使用 droiddeck-ntsync。 设为 1 可让此游戏使用 droiddeck-fsync;设为 0 则在有匹配其 Proton 的包时优先使用 droiddeck-esync。 设为 0 可为此游戏禁用 Proton 的手柄导航助手。 + 默认开启。当游戏支持离线游玩时,以不加载反作弊的方式启动 Easy Anti-Cheat 游戏。关闭后将照常通过 Easy Anti-Cheat 启动。需要 Easy Anti-Cheat 的在线模式仍不可用。 针对 D3D9–11 的 DXVK 覆盖层。fps 显示帧率;不设置则隐藏。 以英文分号(;)分隔的 DXVK 设置。新版本使用 dxvk.maxFrameRate 限制帧率;日常游戏建议优先使用会话帧率限制。 此游戏使用的 CPU 核心,例如 0-4 或 0,2,4,用于替代会话的游戏核心。核心越少,温度越低。 @@ -328,6 +329,12 @@ 已是最新 · %1$s 加载器 安装最新版 + 更新通道 + 要安装哪种 Decky Loader 版本。Decky 自带的更新程序也使用相同的通道。 + 稳定版 + 预发布版 + 测试版 + 无法保存 Decky 更新通道 随 Steam 启动。开启期间,本设备上的其他应用可能可以控制 Steam。 已关闭。Steam 的远程调试端口保持关闭。 卸载 Decky Loader? @@ -337,6 +344,8 @@ 若游戏在 Deck 模式下崩溃或黑屏,请关闭性能覆盖层。将在下次会话生效。 游戏结束后返回 DroidDeck 从 DroidDeck 或其他启动器启动的游戏关闭后,Steam 会正常退出(先完成云存档同步),并返回启动位置。关闭:留在 Steam 中。 + 着色器预缓存 + Steam 会为你的游戏下载预编译的着色器并在后台处理,以减少游戏卡顿。关闭可节省流量、存储空间和电量;游戏会改为在运行时编译着色器。与 Steam 的“下载”>“着色器预缓存”为同一设置。下次会话时生效。 修复 Steam 客户端 下次启动 Steam 时重新下载 Steam 客户端。如果 Steam 一直停在加载画面,请使用此项。游戏、登录信息和设置都会保留。 下次启动 Steam 时将重新下载 Steam 客户端。 @@ -593,6 +602,8 @@ proot 快速路径 需要 proot 的 seccomp 过滤器。 文件查找更快、卡顿更少。若某个程序找不到它的文件,请关闭。 + 在 gamescope 内绘制光标 + 由 Gamescope 自行绘制指针,而不是交给 Android。使用手柄控制光标时请开启。在手机上关闭会更流畅。下次会话时生效。 会话标识 主机名 Steam 看到的本设备名称。留空恢复为 %1$s。 @@ -1267,6 +1278,9 @@ 隐藏其他 GPU 的驱动 显示其他 GPU 的驱动 高级 + UBWC 兼容性 + 如果 Steam 或 Android 出现条纹或闪烁,请尝试开启或关闭。重启 DroidDeck 后对两种驱动均生效。 + 重启 DroidDeck 以应用。 与显示驱动一起设置:%1$s 同时携带两者。 与运行时驱动一起设置:%1$s 同时携带两者。 由游戏和桌面使用。 @@ -1375,6 +1389,8 @@ 自动进入画中画 展开屏幕上的控制 展开时,游戏画面位于 Steam Deck 触控板和背键上方。折叠时,游戏全屏显示。 + 连接手柄时也保留 + 连接手柄时也显示这些触控按键。 已暂停 会话就绪后即可使用画中画。 请在 Android 设置中允许 DroidDeck 的画中画。 @@ -1441,6 +1457,9 @@ 仍要安装 移除 Linux 运行时 这会删除运行时、其中的 Steam 客户端,以及安装在其中的所有游戏。 + Linux 运行时 + “修复”会重新安装运行时的系统文件,并保留 Steam、游戏和设置。桌面等附加组件会在下次使用时重新安装。“移除”会删除所有内容。 + 修复运行时 已应用:%1$s 无法在主屏幕上打开 %1$s 无法在 %2$s 上打开 %1$s @@ -1561,6 +1580,10 @@ 无法连接运行时目录。请检查网络连接,然后再次点“游玩”。 Linux 运行时未能安装。请在“配置”中检查运行时,然后再次点“游玩”。 桌面未能安装(%1$s)。请检查网络连接,然后再次点“桌面”。 + 无法完成之前中断的安装(%1$s)。请检查网络连接和存储空间,然后重试。 + + Linux 运行时已损坏:%1$d 个库链接指向缺失的文件(%2$s 等),通常是安装中断所致。修复会保留你的游戏和设置。 + %1$s 意外停止。请将日志随错误报告一起分享,或重试。 程序意外停止。请将日志随错误报告一起分享,或重试。 桌面意外停止。请将日志随错误报告一起分享,或重试。 @@ -1574,6 +1597,12 @@ 安装插件 ZIP 包,将在下次 Steam 会话中生效。 正在下载… 从 ZIP 安装 + 安装第三方插件 + Decky Loader 团队尚未审核此插件。它可能包含恶意软件,例如窃取你的 Steam 账户或损害设备的软件。安装此插件即表示你同意自行承担设备的一切风险。 + 继续 + + 请等待 %1$d 秒 + 游戏 影音 图形 @@ -1802,6 +1831,93 @@ 键盘 + 触控板 终端 Deck 背键 + 触控板 + 仪表盘 + 暂无内容。 + 下载 + 没有正在进行的下载。 + Steam + 正在下载着色器缓存 + 已暂停 + 等待开始 + 通知 + DroidDeck 底部屏幕 + 在游戏时于此处显示消息和其他通知。通知只显示在此屏幕上,不会被保存。Android 只会询问一次:其通知使用权页面会在此屏幕上打开,在那里开启 DroidDeck 后,会自动返回此页面。 + 允许通知使用权 + 没有通知。 + %1$d 条通知 + 清除 + 截图 + 显示 FPS 需要开启 HUD + 速度 + 踏板 + G 值 + 计时 + 最佳圈速 + 轮胎 °C + 油门和刹车,最近 10 s + 速度,%1$s + 挡位 + 速度单位 + 自动(%1$s) + Steam 客户端 ARM + Steam 客户端 x86 + 油门 + 刹车 + 仪表指示灯 + 换挡 + 手刹 + 维修区限速 + TC + ABS + 涡轮 %1$s bar + 发动机 %1$s °C + 机油 %1$s °C + 能量分配器 + 燃料,t + 主油箱 %1$s t + 备用 %1$s t + 点按顶部横条或仪表可更改其样式。 + 发送仪表盘数据 + 自动 + 为此处列出的所有游戏开启,包括之后发现的游戏。你手动关闭的游戏会保持关闭。 + 这会更改游戏自身的数据输出设置。将在游戏下次启动时生效;关闭后会恢复原来的设置。 + 请关闭 %1$s 后再更改此项。 + 无法更改 %1$s 的设置。 + %1$s:请在游戏设置中开启 %2$s,IP 设为 127.0.0.1,端口设为 %3$d。 + 保存到 Pictures/DroidDeck。Steam 截图也会显示在这里。 + 截图 + 截图已保存。 + 无法截图。 + 暂无截图。 + 聊天和语音 + 在主屏幕继续运行游戏的同时,在此阅读和回复 Steam 聊天。使用 Steam 当前登录的账户。 + Steam 目前还没有可共享的网页登录,请在此登录一次。 + DroidDeck 在后台时,Android 关闭了聊天页面。返回 DroidDeck 后会重新打开。 + 打开 Steam 聊天 + 关闭聊天 + 麦克风已开 + 麦克风已关 + 此游戏的麦克风已关闭。如需语音通话,请在 DroidDeck 的 Steam 设置(音频)中开启麦克风,然后重新启动游戏。 + 音量 %1$d%% + 游戏内数据 + FPS + CPU + GPU + 电池 + 圈数 + 名次 + 本圈 + 上一圈 + 燃料 + 货物 + 能量分配 SYS · ENG · WEP + 法律状态 + 余额 + War Thunder + Elite Dangerous + 开启赛车游戏的数据输出(通常称为遥测或 UDP),即可在此查看实时仪表。数据仅保留在此设备上。 + 端口 %1$d 正在接收数据(%2$d 个 %3$d 字节的数据包),但其格式本页面暂时无法读取。 + 端口 %1$s 已被其他程序占用,因此该游戏无法在此显示。 正在移除 Linux 运行时 @@ -2371,6 +2487,9 @@ 下载管理 同时下载数 安装到 + 游戏语言 + GOG 和 Epic 会安装此语言。“系统”跟随应用语言。 + 系统 安装 %1$s: 内部存储 可用 %1$s @@ -2599,6 +2718,22 @@ “自动”会让这款 2D 游戏固定在视野中。要使用头部追踪,请在“显示”中选择“虚拟屏幕”。 头部追踪适用于虚拟屏幕或正在运行的 SteamVR 游戏。 设备 + 控制器 + SteamVR 控制器 + VR 中的手柄 + 你的手柄所模拟的两只 VR 手。更改会立即生效,SteamVR 运行时也是如此。Joy-Con 和手部追踪仍会接管各自对应的一侧。 + 普通手柄 + Valve Index 控制器 + Meta Quest 3 控制器 + HTC Vive 控制器 + 摇杆初始用途 + 在 VR 中同时按下两个摇杆即可切换。 + 在游戏中移动 + 移动双手 + 按键 + 扳机即扳机,肩键为握持,A/B 和 X/Y 为各只手的正面按键,按下摇杆即按下拇指摇杆,Guide 键打开 SteamVR 菜单。方向键上下可让双手靠近或远离。 + 摇杆作为触控板使用;按 A/X 或按下摇杆即按压触控板。扳机即扳机,肩键为握持,Start 和 Select 为菜单键,Guide 键打开 SteamVR 菜单。 + 不使用 VR 手。支持手柄的游戏以及 SteamVR 菜单会照常使用你的手柄。 亮度 音量 遮光 @@ -2656,6 +2791,12 @@ 相机位置 开始游戏前,请确认相机能看到你。 检查 + 手部追踪下载 + 手部追踪需要一次性下载 %1$s。 + 下载 + 重试 + 正在下载手部追踪… %1$d%% + 无法下载手部追踪。请检查网络连接后重试。 相机位置 将设备立在距离 1.5 到 2 米、约胸口高度的地方,让后置相机对着你。然后张开双手走进画面。 将设备立在距离 1.5 到 2 米、约胸口高度的地方,让屏幕对着你。然后张开双手走进画面。 @@ -3164,6 +3305,113 @@ 可以重新计算保存的视图。开始新的校准以在当前跟踪坐标系中捕获。 手模型校验和不匹配 OpenCV 无法加载 + Syncthing + Syncthing + 使用 Syncthing 同步 + 这是基于 Syncthing-Fork 的 Syncthing 实现,可将 DroidDeck 内部文件系统中的文件与其他 Syncthing 实例同步。仅适用于你存放在 DroidDeck 内部文件系统中的文件。 + 此版本不包含 Syncthing。 + 正在启动 Syncthing + 正在启动 + 最新 + 同步中 + 已暂停 + 正在暂停 + 正在恢复 + 需要处理 + Syncthing 已停止 + 重试 + 已暂停,等待连接 Wi-Fi + 游戏期间已暂停 + %1$d 个文件夹 · 已连接 %2$d/%3$d 台设备 + 剩余 %1$s + 打开 Web 管理页面 + 暂停 + 恢复 + 运行中 + 关闭后将完全停止 Syncthing。 + 本设备 + 设备 ID + 显示二维码 + 高级 + 端口、运行时机、重置 + 打开 + 等待你处理 + %1$s 请求连接 + %1$s 想要共享“%2$s” + 接受后可选择在 DroidDeck 中的保存位置。 + 在 Web 管理页面中查看 + 文件夹 + 暂无文件夹。 + %1$s · %2$s + 最新 + 同步中 %1$d%% + 正在扫描 + 已暂停 + 未同步 + %1$d 个文件无法同步 + 已阻止 + 此文件夹位于 DroidDeck 的存储空间之外,因此 DroidDeck 已将其暂停。要同步 DroidDeck 之外的文件夹,请使用 Syncthing-Fork 应用。 + 从 Syncthing 中移除 + 移除不会删除任何文件。 + 设备 + 暂无设备。 + 已连接 + 离线 + 最后在线:%1$s + 本设备的 ID + 在你的另一台设备上,于 Syncthing 中添加设备,然后扫描此二维码或粘贴 ID。之后在这里的 Web 管理页面中接受该设备。 + 复制 + 分享 + 完成 + 关闭 Syncthing? + Syncthing 将立即停止,并保持停止状态,直到你重新开启。已同步的文件保留在原处。 + 同时删除 Syncthing 的配置 + 将移除本设备的 ID、文件夹和设备。重新开启后将从头开始。 + 关闭 + 端口 + 同步 + 本地发现 + 选择新端口 + 每次启动时都会重新检查。 + 重新选择 + 重置 + API 密钥 + 重新生成 + 重置 Syncthing + 将生成新的设备 ID,并移除文件夹和设备。DroidDeck 存储空间中的文件会保留。 + 重置 + 重置 Syncthing? + 运行 + 运行时机 + DroidDeck 打开时 + 在后台 + 仅限 Wi-Fi + 游戏期间暂停 + 日志 + 本次会话的 Syncthing 输出 + 分享 + Syncthing + 请先设置 Linux 运行时。 + 接受 + 将“%1$s”保存到 + 添加文件夹 + 同步 DroidDeck 存储空间中的文件夹 + 选择 + 添加 + 无法添加该文件夹。 + 共享给 + 暂无设备。你稍后可以在 Web 管理页面中共享此文件夹。 + 使用此文件夹 + 新建文件夹 + 容器主目录 + Sync 文件夹 + DroidDeck 共享存储 + %1$s 上的 DroidDeck + DroidDeck 应用数据 + 新文件夹会保存在 DroidDeck 的 Sync 文件夹中。要同步其他 DroidDeck 文件夹,请在“Syncthing”标签页中使用“添加文件夹”。 + Syncthing + 重新加载 + Syncthing 未在运行。 6DoF 虚拟屏幕 在光线充足的房间内缓慢环顾四周。\n不要遮挡追踪摄像头,并面向家具等有明显细节的物体。 DroidDeck ChArUco 30 mm diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 4aa76ab64..5380c1264 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -88,6 +88,7 @@ 設為 1 讓此遊戲不使用 droiddeck-fsync;有可用套件時會改用 droiddeck-esync,否則使用 droiddeck-ntsync。 設為 1 讓此遊戲使用 droiddeck-fsync;設為 0 則在有配合其 Proton 的套件時優先使用 droiddeck-esync。 設為 0 以為此遊戲停用 Proton 的控制器導覽輔助程式。 + 預設為開啟。當遊戲支援離線遊玩時,以不載入反作弊的方式啟動 Easy Anti-Cheat 遊戲。關閉後會照常透過 Easy Anti-Cheat 啟動。需要 Easy Anti-Cheat 的線上模式仍然無法使用。 D3D9–11 的 DXVK 疊加層。fps 會顯示幀率;不設定則隱藏。 以分號分隔的 DXVK 設定。較新版本使用 dxvk.maxFrameRate 限制幀率;一般遊玩建議使用會話的幀率限制。 此遊戲使用的 CPU 核心,例如 0-4 或 0,2,4,用來取代會話的遊戲核心。核心越少,溫度越低。 @@ -328,6 +329,12 @@ 已是最新 · %1$s 載入器 安裝最新版本 + 更新頻道 + 要安裝哪一種 Decky Loader 版本。Decky 本身的更新程式也會使用相同的頻道。 + 穩定版 + 預發佈版 + 測試版 + 無法儲存 Decky 更新頻道 隨 Steam 啟動。啟用時,此裝置上的其他應用程式可能可以控制 Steam。 已關閉。Steam 的遠端偵錯連接埠保持關閉。 解除安裝 Decky Loader? @@ -337,6 +344,8 @@ 如果在 Deck 模式下遊戲當機或黑畫面,請關閉此項。下次會話生效。 遊戲結束後返回 DroidDeck 從 DroidDeck 或其他啟動器啟動的遊戲關閉後,Steam 會正常退出(先完成雲端存檔同步),並返回啟動位置。關閉:留在 Steam 中。 + 著色器預先快取 + Steam 會為你的遊戲下載預先編譯的著色器並在背景處理,讓遊戲較不卡頓。關閉可節省流動數據用量、儲存空間和電量;遊戲則會改為在執行時編譯著色器。與 Steam 的「下載」>「著色器預先快取」為同一個設定。下次工作階段生效。 修復 Steam 用戶端 下次啟動 Steam 時重新下載 Steam 用戶端。如果 Steam 一直停留在載入畫面,請使用此功能。遊戲、登入資料和設定都會保留。 下次啟動 Steam 時會重新下載 Steam 用戶端。 @@ -593,6 +602,8 @@ proot 快速路徑 需要 proot 的 seccomp 篩選器。 檔案查找更快,卡頓更少。如果程式找不到其檔案,請關閉此項。 + 在 gamescope 內繪製游標 + 由 Gamescope 自行繪製指標,而不交給 Android。如要用控制器操控游標,請開啟此項。在手機上關閉會較快。下次工作階段生效。 會話身分 主機名稱 Steam 看到的此裝置名稱。留空則還原為 %1$s。 @@ -713,7 +724,7 @@ 去色帶強度 亮度 對比度 - Gamma + 伽瑪 飽和度 FXAA(邊緣平滑) 卡通描邊 @@ -1184,6 +1195,9 @@ 隱藏其他 GPU 的驅動程式 顯示其他 GPU 的驅動程式 進階 + UBWC 相容性 + 如果 Steam 或 Android 出現條紋或閃爍,請試試開啟或關閉。重新啟動 DroidDeck 後會套用到兩種驅動程式。 + 重新啟動 DroidDeck 以套用。 與顯示驅動程式一併設定:%1$s 同時包含兩者。 與運行環境驅動程式一併設定:%1$s 同時包含兩者。 供遊戲和桌面使用。 @@ -1292,6 +1306,8 @@ 自動進入畫中畫 展開螢幕時的控制 展開時,遊戲會顯示在 Steam Deck 觸控板和背面握把上方;摺疊時,遊戲會全螢幕顯示。 + 連接控制器時亦保留 + 連接控制器時亦顯示這些觸控按鍵。 已暫停 會話準備就緒後即可使用畫中畫。 請在 Android 設定中允許 DroidDeck 使用畫中畫。 @@ -1358,6 +1374,9 @@ 仍要安裝 移除 Linux 運行環境 此操作會刪除運行環境、其中的 Steam 用戶端,以及安裝在其中的所有遊戲。 + Linux 運行環境 + 「修復」會重新安裝運行環境的系統檔案,並保留 Steam、遊戲和設定。桌面等額外元件會在下次使用時重新安裝。「移除」會刪除所有內容。 + 修復運行環境 已套用:%1$s 無法在主螢幕開啟 %1$s 無法在 %2$s 開啟 %1$s @@ -1484,6 +1503,10 @@ 無法連接運行環境目錄。請檢查網絡連線,然後再按「開啟 Steam」。 Linux 運行環境未能安裝。請在「設定」中檢查運行環境,然後再按「開啟 Steam」。 桌面未能安裝(%1$s)。請檢查網絡連線,然後再按「桌面」。 + 無法完成先前中斷的安裝(%1$s)。請檢查網絡連線和儲存空間,然後再試一次。 + + Linux 運行環境已損壞:%1$d 個程式庫連結指向不存在的檔案(%2$s 等),通常是安裝中斷所致。修復會保留你的遊戲和設定。 + %1$s 意外停止。請在錯誤報告中附上記錄,或再試一次。 程式意外停止。請在錯誤報告中附上記錄,或再試一次。 桌面意外停止。請在錯誤報告中附上記錄,或再試一次。 @@ -1497,6 +1520,12 @@ 安裝插件 ZIP,在下次 Steam 會話使用。 正在下載… 從 ZIP 安裝 + 安裝第三方插件 + Decky Loader 團隊尚未審查此插件。它可能含有惡意軟件,例如竊取你的 Steam 帳戶或損害裝置的軟件。安裝此插件即表示你同意自行承擔裝置的一切風險。 + 繼續 + + 請稍候 %1$d 秒 + 遊戲 音訊及影片 圖形 @@ -1725,6 +1754,93 @@ 鍵盤 + 觸控板 終端機 Deck 握把 + 觸控板 + 儀表板 + 目前沒有可顯示的內容。 + 下載 + 沒有進行中的下載。 + Steam + 正在下載著色器快取 + 已暫停 + 等待開始 + 通知 + DroidDeck 下方螢幕 + 在遊玩時於此處顯示訊息和其他通知。通知只會顯示在這個螢幕上,不會儲存。Android 只會詢問一次:通知存取權頁面會在這個螢幕上開啟,在那裡開啟 DroidDeck 後,就會自動回到此頁面。 + 允許通知存取權 + 沒有通知。 + %1$d 則通知 + 清除 + 螢幕截圖 + 顯示 FPS 需要開啟 HUD + 速度 + 踏板 + G 力 + 計時 + 最佳單圈 + 輪胎 °C + 油門和剎車,最近 10 s + 速度,%1$s + 檔位 + 速度單位 + 自動(%1$s) + Steam 用戶端 ARM + Steam 用戶端 x86 + 油門 + 剎車 + 儀表指示燈 + 換檔 + 手剎 + 維修區限速 + TC + ABS + 渦輪 %1$s bar + 引擎 %1$s °C + 機油 %1$s °C + 能量分配器 + 燃料,t + 主油箱 %1$s t + 備用 %1$s t + 點按頂部橫條或儀表即可變更其樣式。 + 傳送儀表板資料 + 自動 + 為這裡列出的所有遊戲開啟,包括之後找到的遊戲。你關閉的遊戲會維持關閉。 + 這會變更遊戲本身的資料輸出設定。將在遊戲下次啟動時生效;關閉後會還原為原本的設定。 + 請先關閉 %1$s 再變更此項。 + 無法變更 %1$s 的設定。 + %1$s:請在遊戲設定中開啟 %2$s,IP 設為 127.0.0.1,連接埠設為 %3$d。 + 儲存至 Pictures/DroidDeck。Steam 螢幕截圖也會顯示在這裡。 + 截取螢幕畫面 + 已儲存螢幕截圖。 + 無法截取螢幕畫面。 + 尚無螢幕截圖。 + 聊天和語音 + 遊戲在主螢幕上持續執行的同時,在此閱讀和回覆 Steam 聊天。會使用 Steam 目前登入的帳戶。 + Steam 目前還沒有可共用的網頁登入狀態,請在此登入一次。 + DroidDeck 在背景執行時,Android 關閉了聊天頁面。回到 DroidDeck 後會重新開啟。 + 開啟 Steam 聊天 + 關閉聊天 + 麥克風已開啟 + 麥克風已關閉 + 此遊戲的麥克風已關閉。若要說話,請在 DroidDeck 的 Steam 設定(音訊)中開啟麥克風,然後重新啟動遊戲。 + 音量 %1$d%% + 遊戲內數據 + FPS + CPU + GPU + 電池 + 圈數 + 名次 + 本圈 + 上一圈 + 燃料 + 貨物 + 能量分配 SYS · ENG · WEP + 法律狀態 + 餘額 + War Thunder + Elite Dangerous + 開啟賽車遊戲的資料輸出(通常稱為遙測或 UDP),即可在此查看即時儀表。資料只會留在這部裝置上。 + 連接埠 %1$d 正在接收資料(%2$d 個 %3$d 位元組的封包),但此頁面目前還無法讀取這種格式。 + 其他程式已在使用連接埠 %1$s,因此該遊戲無法在此顯示。 正在移除 Linux 運行環境 @@ -2294,6 +2410,9 @@ 下載管理員 同時下載數目 安裝至 + 遊戲語言 + GOG 和 Epic 會安裝此語言。「系統」會跟隨應用程式語言。 + 系統 安裝 %1$s: 內部儲存空間 可用 %1$s @@ -2599,6 +2718,22 @@ 「自動」會令這款 2D 遊戲固定在視野中。如要使用頭部追蹤,請在「顯示」中選擇「虛擬螢幕」。 頭部追蹤適用於虛擬螢幕或正在運行的 SteamVR 遊戲。 設備 + 控制器 + SteamVR 控制器 + VR 中的控制器 + 你的控制器所模擬的兩隻 VR 手。變更會即時生效,SteamVR 運行期間亦然。Joy-Con 和手部追蹤仍會接管各自對應的一側。 + 一般控制器 + Valve Index 控制器 + Meta Quest 3 控制器 + HTC Vive 控制器 + 搖桿初始用途 + 在 VR 中同時按下兩個搖桿即可切換。 + 在遊戲中移動 + 移動雙手 + 按鈕 + 扳機就是扳機,肩鍵為握持,A/B 和 X/Y 為各隻手的正面按鈕,按下搖桿即按下拇指搖桿,Guide 鍵開啟 SteamVR 選單。方向鍵上下可讓雙手拉近或推遠。 + 搖桿作為觸控板使用;按 A/X 或按下搖桿即可按壓觸控板。扳機就是扳機,肩鍵為握持,Start 和 Select 為選單鍵,Guide 鍵開啟 SteamVR 選單。 + 不使用 VR 手。支援控制器的遊戲以及 SteamVR 選單會照常使用你的控制器。 亮度 音量 遮光 @@ -2656,6 +2791,12 @@ 相機位置 開始玩之前,請確認相機能看到你。 檢查 + 手部追蹤下載 + 手部追蹤需要一次性下載 %1$s。 + 下載 + 重試 + 正在下載手部追蹤… %1$d%% + 無法下載手部追蹤。請檢查網絡連線後再試一次。 相機位置 將裝置立在距離 1.5 到 2 米、約胸口高度的地方,讓後置相機對著你。然後張開雙手走進畫面。 將裝置立在距離 1.5 到 2 米、約胸口高度的地方,讓螢幕對著你。然後張開雙手走進畫面。 @@ -3164,6 +3305,113 @@ 可以重新計算已儲存的視圖。開始新的校準以在當前追蹤座標系中捕獲。 手模型校驗與不匹配 OpenCV 無法載入 + Syncthing + Syncthing + 使用 Syncthing 同步 + 這是以 Syncthing-Fork 為基礎的 Syncthing 實作,可將 DroidDeck 內部檔案系統中的檔案與其他 Syncthing 實例同步。只適用於你存放在 DroidDeck 內部檔案系統中的檔案。 + 此版本未包含 Syncthing。 + 正在啟動 Syncthing + 正在啟動 + 已是最新 + 同步中 + 已暫停 + 正在暫停 + 正在繼續 + 需要處理 + Syncthing 已停止 + 再試一次 + 已暫停,等待連接 Wi-Fi + 遊戲期間已暫停 + %1$d 個資料夾 · 已連接 %2$d/%3$d 部裝置 + 剩餘 %1$s + 開啟網頁介面 + 暫停 + 繼續 + 運行中 + 關閉後將完全停止 Syncthing。 + 此裝置 + 裝置 ID + 顯示 QR 碼 + 進階 + 連接埠、運行時機、重設 + 開啟 + 等待你處理 + %1$s 想要連接 + %1$s 想要共用「%2$s」 + 接受後即可選擇在 DroidDeck 中的儲存位置。 + 在網頁介面中檢視 + 資料夾 + 暫時沒有資料夾。 + %1$s · %2$s + 已是最新 + 同步中 %1$d%% + 正在掃描 + 已暫停 + 未同步 + %1$d 個檔案無法同步 + 已封鎖 + 此資料夾位於 DroidDeck 的儲存空間之外,因此 DroidDeck 已將其暫停。若要同步 DroidDeck 以外的資料夾,請使用 Syncthing-Fork 應用程式。 + 從 Syncthing 移除 + 移除後不會刪除任何檔案。 + 裝置 + 暫時沒有裝置。 + 已連接 + 離線 + 上次在線:%1$s + 此裝置的 ID + 在你的另一部裝置上,於 Syncthing 中新增裝置,然後掃描此 QR 碼或貼上 ID。接著在這裡的網頁介面中接受該裝置。 + 複製 + 分享 + 完成 + 關閉 Syncthing? + Syncthing 會立即停止,並保持停止狀態,直至再次開啟為止。已同步的檔案會保留在原處。 + 同時刪除 Syncthing 的設定 + 將移除此裝置的 ID、資料夾和裝置。再次開啟時會從頭開始。 + 關閉 + 連接埠 + 同步 + 本地探索 + 選擇新的連接埠 + 每次啟動時都會重新檢查。 + 重新選擇 + 重設 + API 密鑰 + 重新產生 + 重設 Syncthing + 將產生新的裝置 ID,並移除資料夾和裝置。DroidDeck 儲存空間中的檔案會保留。 + 重設 + 重設 Syncthing? + 運行 + 運行時機 + DroidDeck 開啟時 + 在背景運行 + 只限 Wi-Fi + 遊戲期間暫停 + 記錄 + 此會話的 Syncthing 輸出 + 分享 + Syncthing + 請先設定 Linux 運行環境。 + 接受 + 將「%1$s」儲存到 + 新增資料夾 + 同步 DroidDeck 儲存空間中的資料夾 + 選擇 + 新增 + 無法新增該資料夾。 + 共用對象 + 暫時沒有裝置。你稍後可以從網頁介面共用此資料夾。 + 使用此資料夾 + 新資料夾 + 容器主目錄 + Sync 資料夾 + DroidDeck 共用儲存空間 + %1$s 上的 DroidDeck + DroidDeck 應用程式資料 + 新資料夾會儲存在 DroidDeck 的 Sync 資料夾中。若要同步其他 DroidDeck 資料夾,請在「Syncthing」分頁中使用「新增資料夾」。 + Syncthing + 重新載入 + Syncthing 未在運行。 6DoF 虛擬螢幕 在光線充足的房間內緩慢環顧四周。\n不要遮擋追蹤攝影機,並面向家具等有明顯細節的物體。 DroidDeck ChArUco 30 mm diff --git a/app/src/main/res/values-zh-rTW/gestures.xml b/app/src/main/res/values-zh-rTW/gestures.xml index 7841fe7b4..551fab20d 100644 --- a/app/src/main/res/values-zh-rTW/gestures.xml +++ b/app/src/main/res/values-zh-rTW/gestures.xml @@ -71,7 +71,7 @@ 退格 Enter Tab - Space + 空白鍵 刪除 插入 Home diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 53e263b3f..fceb4e086 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -88,6 +88,7 @@ 設為 1 可讓此遊戲不使用 droiddeck-fsync;有對應套件時改用 droiddeck-esync,否則使用 droiddeck-ntsync。 設為 1 讓此遊戲使用 droiddeck-fsync;設為 0 則在有適用其 Proton 的套件時優先使用 droiddeck-esync。 設定為 0,為此遊戲停用 Proton 的手把導覽輔助程式。 + 預設為開啟。當遊戲支援離線遊玩時,以不載入反作弊的方式啟動 Easy Anti-Cheat 遊戲。關閉後會照常透過 Easy Anti-Cheat 啟動。需要 Easy Anti-Cheat 的線上模式仍然無法使用。 D3D9–11 的 DXVK 重疊層。fps 顯示影格率;取消設定則隱藏重疊層。 以分號分隔的 DXVK 設定。近期版本使用 dxvk.maxFrameRate 限制影格率;日常遊玩建議使用工作階段影格率限制器。 此遊戲使用的 CPU 核心,例如 0-4 或 0,2,4,用來取代工作階段的遊戲核心。核心越少,溫度越低。 @@ -328,6 +329,12 @@ 已是最新 · %1$s 載入器 安裝最新版本 + 更新頻道 + 要安裝哪一種 Decky Loader 版本。Decky 本身的更新程式也會使用相同的頻道。 + 穩定版 + 預先發行版 + 測試版 + 無法儲存 Decky 更新頻道 隨 Steam 啟動。啟用後,此裝置上的其他應用程式可能可以控制 Steam。 關閉。Steam 的遠端偵錯連接埠保持關閉。 解除安裝 Decky Loader? @@ -337,6 +344,8 @@ 如果 Deck 模式下遊戲崩潰或黑畫面,請關閉此項。下次工作階段生效。 遊戲結束後返回 DroidDeck 從 DroidDeck 或其他啟動器啟動的遊戲關閉後,Steam 會正常退出(先完成雲端存檔同步),並返回啟動位置。關閉:留在 Steam 中。 + 著色器預先快取 + Steam 會為你的遊戲下載預先編譯的著色器並在背景處理,讓遊戲較不卡頓。關閉可節省數據用量、儲存空間和電量;遊戲則會改為在執行時編譯著色器。與 Steam 的「下載」>「著色器預先快取」為同一個設定。下次工作階段生效。 修復 Steam 用戶端 下次啟動 Steam 時重新下載 Steam 用戶端。如果 Steam 一直停在載入畫面,請使用此功能。遊戲、登入狀態和設定都會保留。 下次啟動 Steam 時會重新下載 Steam 用戶端。 @@ -593,6 +602,8 @@ proot 快速路徑 需要 proot 的 seccomp 篩選器。 檔案尋找更快、卡頓更少。如果程式找不到檔案,請關閉此項。 + 在 gamescope 內繪製游標 + 由 Gamescope 自行繪製指標,而不交給 Android。若要用手把控制游標,請開啟此項。在手機上關閉會比較快。下次工作階段生效。 工作階段身分 主機名稱 Steam 看到的此裝置名稱。留空可還原為 %1$s。 @@ -1184,6 +1195,9 @@ 隱藏其他 GPU 的驅動程式 顯示其他 GPU 的驅動程式 進階 + UBWC 相容性 + 如果 Steam 或 Android 出現條紋或閃爍,請試試開啟或關閉。重新啟動 DroidDeck 後會套用到兩種驅動程式。 + 重新啟動 DroidDeck 以套用。 與顯示驅動程式一起設定:%1$s 同時包含兩者。 與執行環境驅動程式一起設定:%1$s 同時包含兩者。 供遊戲和桌面使用。 @@ -1292,6 +1306,8 @@ 自動進入子母畫面 展開螢幕時的控制 展開時,遊戲會顯示在 Steam Deck 觸控板和背面握把上方。摺疊時,遊戲以全螢幕顯示。 + 連接手把時也保留 + 連接手把時也顯示這些觸控按鍵。 已暫停 工作階段準備就緒後即可使用子母畫面。 請在 Android 設定中允許 DroidDeck 使用子母畫面。 @@ -1358,6 +1374,9 @@ 仍要安裝 移除 Linux 執行環境 這會刪除執行環境、其中的 Steam 用戶端,以及安裝在其中的所有遊戲。 + Linux 執行環境 + 「修復」會重新安裝執行環境的系統檔案,並保留 Steam、遊戲和設定。桌面等額外元件會在下次使用時重新安裝。「移除」會刪除所有內容。 + 修復執行環境 已套用:%1$s 無法在主螢幕上開啟 %1$s 無法在 %2$s 上開啟 %1$s @@ -1484,6 +1503,10 @@ 無法連線到執行環境目錄。請檢查網路連線,然後再按一次「執行」。 Linux 執行環境未能安裝。請在「設定」中檢查執行環境,然後再按一次「執行」。 桌面未能安裝(%1$s)。請檢查網路連線,然後再按一次「桌面」。 + 無法完成先前中斷的安裝(%1$s)。請檢查網路連線和儲存空間,然後再試一次。 + + Linux 執行環境已損壞:%1$d 個程式庫連結指向不存在的檔案(%2$s 等),通常是安裝中斷所致。修復會保留你的遊戲和設定。 + %1$s 意外停止。請在錯誤報告中附上日誌,或再試一次。 程式意外停止。請在錯誤報告中附上日誌,或再試一次。 桌面意外停止。請在錯誤報告中附上日誌,或再試一次。 @@ -1497,6 +1520,12 @@ 安裝外掛程式 ZIP,供下次 Steam 工作階段使用。 正在下載… 從 ZIP 安裝 + 安裝第三方外掛程式 + Decky Loader 團隊尚未審查此外掛程式。它可能含有惡意軟體,例如竊取你的 Steam 帳戶或損害裝置的軟體。安裝此外掛程式即表示你同意自行承擔裝置的一切風險。 + 繼續 + + 請稍候 %1$d 秒 + 遊戲 影音 圖形 @@ -1552,7 +1581,7 @@ Ctrl Super Alt - Space + 空白鍵 AltGr Tab %1$s ✓ @@ -1725,6 +1754,93 @@ 鍵盤 + 觸控板 終端機 Deck 握把 + 觸控板 + 儀表板 + 目前沒有可顯示的內容。 + 下載 + 沒有進行中的下載。 + Steam + 正在下載著色器快取 + 已暫停 + 等待開始 + 通知 + DroidDeck 底部螢幕 + 在遊玩時於此處顯示訊息和其他通知。通知只會顯示在這個螢幕上,不會儲存。Android 只會詢問一次:通知存取權頁面會在這個螢幕上開啟,在那裡開啟 DroidDeck 後,就會自動回到此頁面。 + 允許通知存取權 + 沒有通知。 + %1$d 則通知 + 清除 + 螢幕擷取畫面 + 顯示 FPS 需要開啟 HUD + 速度 + 踏板 + G 力 + 計時 + 最佳單圈 + 輪胎 °C + 油門和煞車,最近 10 s + 速度,%1$s + 檔位 + 速度單位 + 自動(%1$s) + Steam 用戶端 ARM + Steam 用戶端 x86 + 油門 + 煞車 + 儀表指示燈 + 換檔 + 手煞車 + 維修區限速 + TC + ABS + 渦輪 %1$s bar + 引擎 %1$s °C + 機油 %1$s °C + 能量分配器 + 燃料,t + 主油箱 %1$s t + 備用 %1$s t + 點按頂部橫條或儀表即可變更其樣式。 + 傳送儀表板資料 + 自動 + 為這裡列出的所有遊戲開啟,包括之後找到的遊戲。你關閉的遊戲會維持關閉。 + 這會變更遊戲本身的資料輸出設定。將在遊戲下次啟動時生效;關閉後會還原為原本的設定。 + 請先關閉 %1$s 再變更此項。 + 無法變更 %1$s 的設定。 + %1$s:請在遊戲設定中開啟 %2$s,IP 設為 127.0.0.1,連接埠設為 %3$d。 + 儲存至 Pictures/DroidDeck。Steam 螢幕擷取畫面也會顯示在這裡。 + 擷取螢幕畫面 + 已儲存螢幕擷取畫面。 + 無法擷取螢幕畫面。 + 尚無螢幕擷取畫面。 + 聊天和語音 + 遊戲在主螢幕上持續執行的同時,在此閱讀和回覆 Steam 聊天。會使用 Steam 目前登入的帳戶。 + Steam 目前還沒有可共用的網頁登入狀態,請在此登入一次。 + DroidDeck 在背景執行時,Android 關閉了聊天頁面。回到 DroidDeck 後會重新開啟。 + 開啟 Steam 聊天 + 關閉聊天 + 麥克風已開啟 + 麥克風已關閉 + 此遊戲的麥克風已關閉。若要說話,請在 DroidDeck 的 Steam 設定(音訊)中開啟麥克風,然後重新啟動遊戲。 + 音量 %1$d%% + 遊戲內數據 + FPS + CPU + GPU + 電池 + 圈數 + 名次 + 本圈 + 上一圈 + 燃料 + 貨物 + 能量分配 SYS · ENG · WEP + 法律狀態 + 餘額 + War Thunder + Elite Dangerous + 開啟賽車遊戲的資料輸出(通常稱為遙測或 UDP),即可在此查看即時儀表。資料只會留在這部裝置上。 + 連接埠 %1$d 正在接收資料(%2$d 個 %3$d 位元組的封包),但此頁面目前還無法讀取這種格式。 + 其他程式已在使用連接埠 %1$s,因此該遊戲無法在此顯示。 正在移除 Linux 執行環境 @@ -2294,6 +2410,9 @@ 下載管理員 同時下載數 安裝到 + 遊戲語言 + GOG 和 Epic 會安裝此語言。「系統」會跟隨應用程式語言。 + 系統 安裝 %1$s: 內部 可用 %1$s @@ -2599,6 +2718,22 @@ 「自動」會讓這款 2D 遊戲固定在視野中。如要使用頭部追蹤,請在「顯示」中選擇「虛擬螢幕」。 頭部追蹤適用於虛擬螢幕或執行中的 SteamVR 遊戲。 設備 + 控制器 + SteamVR 控制器 + VR 中的手把 + 你的手把所模擬的兩隻 VR 手。變更會立即生效,SteamVR 執行時也一樣。Joy-Con 和手部追蹤仍會接管各自對應的一側。 + 一般手把 + Valve Index 控制器 + Meta Quest 3 控制器 + HTC Vive 控制器 + 搖桿初始用途 + 在 VR 中同時按下兩個搖桿即可切換。 + 在遊戲中移動 + 移動雙手 + 按鈕 + 扳機就是扳機,肩鍵為握持,A/B 和 X/Y 為各隻手的正面按鈕,按下搖桿即按下拇指搖桿,Guide 鍵開啟 SteamVR 選單。方向鍵上下可讓雙手拉近或推遠。 + 搖桿作為觸控板使用;按 A/X 或按下搖桿即可按壓觸控板。扳機就是扳機,肩鍵為握持,Start 和 Select 為選單鍵,Guide 鍵開啟 SteamVR 選單。 + 不使用 VR 手。支援手把的遊戲以及 SteamVR 選單會照常使用你的手把。 亮度 音量 遮光 @@ -2656,6 +2791,12 @@ 相機位置 開始遊玩前,請確認相機能看到你。 檢查 + 手部追蹤下載 + 手部追蹤需要一次性下載 %1$s。 + 下載 + 重試 + 正在下載手部追蹤… %1$d%% + 無法下載手部追蹤。請檢查網路連線後再試一次。 相機位置 將裝置立在距離 1.5 到 2 公尺、約胸口高度的地方,讓後置相機對著你。然後張開雙手走進畫面。 將裝置立在距離 1.5 到 2 公尺、約胸口高度的地方,讓螢幕對著你。然後張開雙手走進畫面。 @@ -3164,6 +3305,113 @@ 可以重新計算已儲存的視圖。開始新的校準以在當前追蹤座標系中捕獲。 手模型校驗與不匹配 OpenCV 無法載入 + Syncthing + Syncthing + 使用 Syncthing 同步 + 這是以 Syncthing-Fork 為基礎的 Syncthing 實作,可將 DroidDeck 內部檔案系統中的檔案與其他 Syncthing 執行個體同步。僅適用於你存放在 DroidDeck 內部檔案系統中的檔案。 + 此版本未包含 Syncthing。 + 正在啟動 Syncthing + 正在啟動 + 已是最新 + 同步中 + 已暫停 + 正在暫停 + 正在繼續 + 需要處理 + Syncthing 已停止 + 再試一次 + 已暫停,等待連上 Wi-Fi + 遊戲期間已暫停 + %1$d 個資料夾 · 已連線 %2$d/%3$d 部裝置 + 剩餘 %1$s + 開啟網頁介面 + 暫停 + 繼續 + 執行中 + 關閉後將完全停止 Syncthing。 + 此裝置 + 裝置 ID + 顯示 QR 碼 + 進階 + 連接埠、執行時機、重設 + 開啟 + 等待你處理 + %1$s 想要連線 + %1$s 想要共用「%2$s」 + 接受後即可選擇在 DroidDeck 中的儲存位置。 + 在網頁介面中檢視 + 資料夾 + 尚無資料夾。 + %1$s · %2$s + 已是最新 + 同步中 %1$d%% + 正在掃描 + 已暫停 + 未同步 + %1$d 個檔案無法同步 + 已封鎖 + 此資料夾位於 DroidDeck 的儲存空間之外,因此 DroidDeck 已將其暫停。若要同步 DroidDeck 以外的資料夾,請使用 Syncthing-Fork 應用程式。 + 從 Syncthing 移除 + 移除後不會刪除任何檔案。 + 裝置 + 尚無裝置。 + 已連線 + 離線 + 上次上線:%1$s + 此裝置的 ID + 在你的另一部裝置上,於 Syncthing 中新增裝置,然後掃描此 QR 碼或貼上 ID。接著在這裡的網頁介面中接受該裝置。 + 複製 + 分享 + 完成 + 要關閉 Syncthing 嗎? + Syncthing 會立即停止,並維持停止狀態,直到再次開啟為止。已同步的檔案會保留在原處。 + 同時刪除 Syncthing 的設定 + 將移除此裝置的 ID、資料夾和裝置。再次開啟時會從頭開始。 + 關閉 + 連接埠 + 同步 + 本機探索 + 選擇新的連接埠 + 每次啟動時都會重新檢查。 + 重新選擇 + 重設 + API 金鑰 + 重新產生 + 重設 Syncthing + 將產生新的裝置 ID,並移除資料夾和裝置。DroidDeck 儲存空間中的檔案會保留。 + 重設 + 要重設 Syncthing 嗎? + 執行 + 執行時機 + DroidDeck 開啟時 + 在背景執行 + 僅限 Wi-Fi + 遊戲期間暫停 + 日誌 + 此工作階段的 Syncthing 輸出 + 分享 + Syncthing + 請先設定 Linux 執行環境。 + 接受 + 將「%1$s」儲存到 + 新增資料夾 + 同步 DroidDeck 儲存空間中的資料夾 + 選擇 + 新增 + 無法新增該資料夾。 + 共用對象 + 尚無裝置。你之後可以從網頁介面共用此資料夾。 + 使用此資料夾 + 新資料夾 + 容器家目錄 + Sync 資料夾 + DroidDeck 共用儲存空間 + %1$s 上的 DroidDeck + DroidDeck 應用程式資料 + 新資料夾會儲存在 DroidDeck 的 Sync 資料夾中。若要同步其他 DroidDeck 資料夾,請在「Syncthing」分頁中使用「新增資料夾」。 + Syncthing + 重新載入 + Syncthing 未在執行。 6DoF 虛擬螢幕 在光線充足的房間內緩慢環顧四周。\n不要遮擋追蹤攝影機,並面向家具等有明顯細節的物體。 DroidDeck ChArUco 30 mm diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ed95133b9..d80833d7d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3537,7 +3537,7 @@ DroidDeck shared storage DroidDeck on %1$s DroidDeck app data - New folders are saved in DroidDeck\'s Sync folder. To sync a different DroidDeck folder, use Add folder on the Sync tab. + New folders are saved in DroidDeck\'s Sync folder. To sync a different DroidDeck folder, use Add folder on the Syncthing tab. Syncthing Reload Syncthing isn\'t running. diff --git a/app/src/main/rust/stores/src/fetch_core.rs b/app/src/main/rust/stores/src/fetch_core.rs index f01973440..a0a4ac91a 100644 --- a/app/src/main/rust/stores/src/fetch_core.rs +++ b/app/src/main/rust/stores/src/fetch_core.rs @@ -609,8 +609,6 @@ fn classify_reqwest_error(err: &reqwest::Error) -> FetchFailKind { } else if err.is_connect() { FetchFailKind::Connect } else if err.is_body() || err.is_decode() { - // A truncated or undecodable body comes from the CDN, not the request. Rest that host so - // the retry goes to another mirror instead of reading the same bad object again. FetchFailKind::ServerFault } else { FetchFailKind::Other @@ -1424,9 +1422,6 @@ impl FetchScheduler { }; h.samples = h.samples.saturating_add(1); h.consecutive_errors = 0; - // A success can belong to a request that was already in flight when another response from - // this host failed. Keep a server-fault cooldown until it expires instead of clearing it - // early and sending the retry straight back to the same mirror. if let Some(fault) = h.fault_until.filter(|until| *until > now) { h.cooldown_until = Some(h.cooldown_until.map_or(fault, |current| current.max(fault))); } else { diff --git a/app/src/main/rust/stores/src/fetch_tuning.rs b/app/src/main/rust/stores/src/fetch_tuning.rs index 5c380d537..8a11c3dc0 100644 --- a/app/src/main/rust/stores/src/fetch_tuning.rs +++ b/app/src/main/rust/stores/src/fetch_tuning.rs @@ -68,8 +68,6 @@ pub const SERVER_EXPLORE_EVERY: u64 = 8; pub const NOMINAL_CHUNK_RESERVE_BYTES: u64 = 1024 * 1024; pub const RATE_LIMIT_COOLDOWN_MS: u64 = 5_000; -// Long enough that every retry of the item (5 attempts, at most ~4.5 s of backoff) goes to another -// mirror after this one served a truncated or undecodable body. pub const SERVER_FAULT_COOLDOWN_MS: u64 = 10_000; pub fn retry_backoff_millis(attempt: u32) -> u64 { diff --git a/app/src/test/cpp/viture_display_modes_test.cpp b/app/src/test/cpp/viture_display_modes_test.cpp index ea6ea5462..956e05fe7 100644 --- a/app/src/test/cpp/viture_display_modes_test.cpp +++ b/app/src/test/cpp/viture_display_modes_test.cpp @@ -1,4 +1,3 @@ -// Compile with the installed SDK's headers to verify the two mode vocabularies. #include "viture_protocol_public.h" #include "../../main/cpp/viture/display_modes.h" using namespace droiddeck::viture; diff --git a/app/src/test/java/com/droiddeck/launcher/core/DownloaderResumeTest.kt b/app/src/test/java/com/droiddeck/launcher/core/DownloaderResumeTest.kt index f0c1002eb..f2ed52f7e 100644 --- a/app/src/test/java/com/droiddeck/launcher/core/DownloaderResumeTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/core/DownloaderResumeTest.kt @@ -17,11 +17,6 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -/** - * A download that was cut off keeps its bytes, and a second call with `resume` asks for the rest - * with a Range header instead of starting again. The server is a plain socket so the first answer - * can promise the whole file and then hang up halfway. - */ class DownloaderResumeTest { private val data = ByteArray(300_000) { (it % 251).toByte() } private val cutAt = 100_000 @@ -43,11 +38,9 @@ class DownloaderResumeTest { try { while (!server.isClosed) { val socket = server.accept() - // the client may hang up on a body it refuses; that must not stop the server runCatching { handle(socket) } } } catch (_: Exception) { - // closed by tearDown } }.apply { isDaemon = true; start() } } @@ -72,7 +65,6 @@ class DownloaderResumeTest { val content = if (corrupt && "/corrupt" in path) ByteArray(data.size) else data val from = range?.removePrefix("bytes=")?.substringBefore('-')?.toIntOrNull() if (misbehave) { - // answers a Range request with a 206 that starts at 0, and serves plain requests whole val start = if (from == null) "200 OK" else "206 Partial Content" val extra = if (from == null) "" else "Content-Range: bytes 0-${data.size - 1}/${data.size}\r\n" out.write("HTTP/1.1 $start\r\n${extra}${validator}Content-Length: ${data.size}\r\nConnection: close\r\n\r\n".toByteArray()) @@ -89,7 +81,6 @@ class DownloaderResumeTest { out.write("HTTP/1.1 200 OK\r\n${validator}Content-Length: ${data.size}\r\nConnection: close\r\n\r\n".toByteArray()) out.write(content, 0, cutAt) out.flush() - // hang up with most of the promised body still unsent } else { val rest = data.size - from out.write(("HTTP/1.1 206 Partial Content\r\nContent-Range: bytes $from-${data.size - 1}/${data.size}\r\n" + @@ -187,7 +178,6 @@ class DownloaderResumeTest { val target = File(dir, "runtime.tar.zst") misbehave = true val polls = java.util.concurrent.atomic.AtomicInteger() - // candidate selection and attempt each check once before the request; later checks follow chunks val cancelled = java.util.function.BooleanSupplier { polls.incrementAndGet() > 2 } assertFalse(Downloader.downloadCandidates(first, listOf(first, second), target, true, null, cancelled, null)) assertEquals("a cancel must not fall back to the next source", listOf(null), requests.toList()) diff --git a/app/src/test/java/com/droiddeck/launcher/glasses/GlassesDisplayTest.kt b/app/src/test/java/com/droiddeck/launcher/glasses/GlassesDisplayTest.kt index 923f98848..38ccf3649 100644 --- a/app/src/test/java/com/droiddeck/launcher/glasses/GlassesDisplayTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/glasses/GlassesDisplayTest.kt @@ -93,7 +93,6 @@ class GlassesDisplayTest { @Test fun gtMaxSmartGlassesEdidUsesTheSameOutputSelectionAsViture() { val panel = display("SmartGlasses", productName = "SmartGlasses", productId = "980") assertEquals(panel.displayId, Glasses.selectGlassesDisplay(listOf(panel), usbPresent = true)?.displayId) - // The display remains usable while USB control reconnects during a mode switch. assertEquals(panel.displayId, Glasses.selectGlassesDisplay(listOf(panel), usbPresent = false)?.displayId) } @@ -126,7 +125,6 @@ class GlassesDisplayTest { } } - // Robolectric exposes display names and flags, but has no product-metadata setter. @Implements(Display::class) class ProductMetadataDisplay : ShadowDisplay() { var productMetadata: DeviceProductInfo? = null diff --git a/app/src/test/java/com/droiddeck/launcher/glasses/GlassesMenuProjectionTest.kt b/app/src/test/java/com/droiddeck/launcher/glasses/GlassesMenuProjectionTest.kt index d1cd22769..0192d9840 100644 --- a/app/src/test/java/com/droiddeck/launcher/glasses/GlassesMenuProjectionTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/glasses/GlassesMenuProjectionTest.kt @@ -63,7 +63,6 @@ class GlassesMenuProjectionTest { private object GlassesMenuProjection { data class Uv(val x: Float, val y: Float) - // Row-major head-to-anchor rotation; +Y is up, -Z is forward, output UV starts at the top left. fun intersect(frame: GlassesMenuPose.Frame, rayX: Float, rayY: Float, eye: Float, halfWidth: Float, distance: Float, aspect: Float): Uv? { if (frame.rotation.size != 9 || frame.position.size != 3 || frame.rotation.any { !it.isFinite() } || diff --git a/app/src/test/java/com/droiddeck/launcher/glasses/GlassesOutputHandoffTest.kt b/app/src/test/java/com/droiddeck/launcher/glasses/GlassesOutputHandoffTest.kt index e1258f489..1669d236f 100644 --- a/app/src/test/java/com/droiddeck/launcher/glasses/GlassesOutputHandoffTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/glasses/GlassesOutputHandoffTest.kt @@ -30,7 +30,6 @@ class GlassesOutputHandoffTest { private lateinit var screen: GlassesScreen @Before fun setup() { - // Exercise the output lifecycle without starting the native Linux runtime. activity = Robolectric.buildActivity(SessionActivity::class.java).get() phone = SurfaceView(activity) ReflectionHelpers.setField(activity, "surfaceView", phone) @@ -46,7 +45,7 @@ class GlassesOutputHandoffTest { @Test fun selectingGlassesDoesNotHidePhoneOrClaimOutputBeforeAttachment() { assertFalse(active()) assertEquals(View.VISIBLE, phone.visibility) - attach(phone.holder) // A late phone callback must not complete the handoff. + attach(phone.holder) assertFalse(active()) assertEquals(View.VISIBLE, phone.visibility) } diff --git a/app/src/test/java/com/droiddeck/launcher/glasses/GlassesRenderingTest.kt b/app/src/test/java/com/droiddeck/launcher/glasses/GlassesRenderingTest.kt index 4b5972ede..5c4b38d23 100644 --- a/app/src/test/java/com/droiddeck/launcher/glasses/GlassesRenderingTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/glasses/GlassesRenderingTest.kt @@ -183,11 +183,11 @@ class GlassesRenderingTest { picture3d = GlassesSettings.PICTURE_SCREEN) for (state in listOf(ultra.copy(displayId = -1), GlassesState())) { val preview = GlassesRendering.cardboard(base, settings, state, false, true) - assertTrue(preview.enabled) // Published as headset proximity, preventing standby. + assertTrue(preview.enabled) assertTrue(preview.mono) assertEquals(Cardboard.SOURCE_SBS, preview.source) assertEquals(Cardboard.Viewer.NONE.id, preview.viewer) - assertTrue(preview.headTracking) // Phone preference, not dormant glasses settings. + assertTrue(preview.headTracking) assertFalse(base.enabled) assertSame(base, GlassesRendering.cardboard(base, settings, state, false, false)) } diff --git a/app/src/test/java/com/droiddeck/launcher/glasses/GlassesSteamVrTest.kt b/app/src/test/java/com/droiddeck/launcher/glasses/GlassesSteamVrTest.kt index 02ab5aca8..8d4175374 100644 --- a/app/src/test/java/com/droiddeck/launcher/glasses/GlassesSteamVrTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/glasses/GlassesSteamVrTest.kt @@ -251,8 +251,6 @@ class GlassesSteamVrTest { .set(activity, LoadingState(activity).apply { visible = false }) SessionState.steamVr = true SessionState.mode = SessionService.MODE_STEAM - // Hold the external worker so status transitions can be exercised - // without launching a Linux runtime from the host test process. val worker = SessionActivity::class.java.getDeclaredField("steamVrWorker") .apply { isAccessible = true }.get(null) as ThreadPoolExecutor val blocked = CountDownLatch(1) diff --git a/app/src/test/java/com/droiddeck/launcher/glasses/RayNeoCalibrationTest.kt b/app/src/test/java/com/droiddeck/launcher/glasses/RayNeoCalibrationTest.kt index 996b8fa8c..70ef3a221 100644 --- a/app/src/test/java/com/droiddeck/launcher/glasses/RayNeoCalibrationTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/glasses/RayNeoCalibrationTest.kt @@ -115,8 +115,6 @@ class RayNeoCalibrationTest { val upright = RayNeoOrientationFilter(profile.rotationXDeg, profile.gtMaxAxes, saved) .update(sample(floatArrayOf(-0.02f, 8.819f, 4.106f), reading.gyroDps)) assertArrayEquals(floatArrayOf(1f, 0f, 0f, 0f), upright, 0.001f) - // This was the reported failure: the same physical pose cannot calibrate - // through the Max's permutation and 35-degree mounting correction. assertNull(finish(RayNeoCalibrationCollector(RayNeoSensorAxes(35f, true), 0), sample = reading)) } diff --git a/app/src/test/java/com/droiddeck/launcher/glasses/RayNeoLinkTest.kt b/app/src/test/java/com/droiddeck/launcher/glasses/RayNeoLinkTest.kt index 832d220c7..a880bf96f 100644 --- a/app/src/test/java/com/droiddeck/launcher/glasses/RayNeoLinkTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/glasses/RayNeoLinkTest.kt @@ -102,7 +102,6 @@ class RayNeoLinkTest { val cmd = bytes[1].toInt() and 0xff commands += cmd if (gtTraffic && cmd in listOf(0, 1)) { - // Notifications and unrelated ACKs can precede the requested response. for (other in listOf(0x08, 6, 7, 0x50)) { responses += ByteArray(64).apply { this[0] = 0x99.toByte(); this[1] = 0xc8.toByte(); this[2] = 64 diff --git a/app/src/test/java/com/droiddeck/launcher/glasses/RayNeoOrientationFilterTest.kt b/app/src/test/java/com/droiddeck/launcher/glasses/RayNeoOrientationFilterTest.kt index c05b7eb19..0dc767629 100644 --- a/app/src/test/java/com/droiddeck/launcher/glasses/RayNeoOrientationFilterTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/glasses/RayNeoOrientationFilterTest.kt @@ -79,8 +79,6 @@ class RayNeoOrientationFilterTest { val first = screen(filter.update(sample(0, level))) val ref = HeadPose.recenter(first) var q = FloatArray(4) - // Physical 30-degree turn over one second, through the complete shared - // VITURE conversion and recenter pipeline. GT signs match device captures. repeat(100) { i -> val a = Math.toRadians((i + 1) * 0.3) val accel = when (axis) { diff --git a/app/src/test/java/com/droiddeck/launcher/glasses/RayNeoProtocolTest.kt b/app/src/test/java/com/droiddeck/launcher/glasses/RayNeoProtocolTest.kt index 0cf666308..03fcd5bb5 100644 --- a/app/src/test/java/com/droiddeck/launcher/glasses/RayNeoProtocolTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/glasses/RayNeoProtocolTest.kt @@ -116,7 +116,7 @@ class RayNeoProtocolTest { @Test fun tickWrapDoesNotIntroduceOrientationJump() { val filter = RayNeoOrientationFilter(0f) filter.update(sample(0xfffffff0L, 90f)) - val q = filter.update(sample(84, 90f)) // 100 ticks = 10ms across uint32 rollover. + val q = filter.update(sample(84, 90f)) assertEquals(kotlin.math.sin(Math.PI * 0.005).toFloat(), q[2], 1e-4f) } } diff --git a/app/src/test/java/com/droiddeck/launcher/glasses/VitureSdkTest.kt b/app/src/test/java/com/droiddeck/launcher/glasses/VitureSdkTest.kt index 0bc140777..3be90f93c 100644 --- a/app/src/test/java/com/droiddeck/launcher/glasses/VitureSdkTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/glasses/VitureSdkTest.kt @@ -71,8 +71,8 @@ class VitureSdkTest { val s = kotlin.math.sin(Math.PI / 12).toFloat() val c = kotlin.math.cos(Math.PI / 12).toFloat() for ((axis, expected) in listOf( - 2 to floatArrayOf(-0.5f, 0f, -0.8660254f), // Observed left turn: positive Y. - 1 to floatArrayOf(0f, 0.5f, -0.8660254f), // Observed upward look: positive X. + 2 to floatArrayOf(-0.5f, 0f, -0.8660254f), + 1 to floatArrayOf(0f, 0.5f, -0.8660254f), 3 to floatArrayOf(-0.5f, 0.8660254f, 0f), )) { val q = floatArrayOf(c, 0f, 0f, 0f).apply { this[axis] = s } @@ -84,7 +84,6 @@ class VitureSdkTest { @Test fun ultraOpenGlAxesMatchExistingGlassesHeadTracker() { val a = sqrt(0.5f) - // OpenGL Y yaw maps to NWU Z; OpenGL X pitch maps to negative NWU Y. assertArrayEquals(floatArrayOf(0f, 0f, a, a), VitureSdkPose.toGlasses(floatArrayOf(a, 0f, a, 0f), true), 0.00001f) assertArrayEquals(floatArrayOf(0f, -a, 0f, a), diff --git a/app/src/test/java/com/droiddeck/launcher/glasses/VitureV2ProtocolTest.kt b/app/src/test/java/com/droiddeck/launcher/glasses/VitureV2ProtocolTest.kt index 95f0cfb55..993d33d82 100644 --- a/app/src/test/java/com/droiddeck/launcher/glasses/VitureV2ProtocolTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/glasses/VitureV2ProtocolTest.kt @@ -1,5 +1,4 @@ -// Fixtures adapted from elasticjava/viture-v2, MIT, Copyright (c) 2026 Holger Bartnick. -// Source: ece8c30485f7d357fefa362ae74f3eabac9b3899; see docs/third-party/viture-v2/. +// Fixtures adapted from elasticjava/viture-v2, MIT, Copyright (c) 2026 Holger Bartnick. Source: ece8c30485f7d357fefa362ae74f3eabac9b3899; see docs/third-party/viture-v2/. package com.droiddeck.launcher.glasses import org.junit.Assert.assertArrayEquals diff --git a/app/src/test/java/com/droiddeck/launcher/glasses/XrealOrientationFilterTest.kt b/app/src/test/java/com/droiddeck/launcher/glasses/XrealOrientationFilterTest.kt index 578d334a6..4aa94d69b 100644 --- a/app/src/test/java/com/droiddeck/launcher/glasses/XrealOrientationFilterTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/glasses/XrealOrientationFilterTest.kt @@ -63,10 +63,8 @@ class XrealOrientationFilterTest { @Test fun capturedNeutralGravityStartsUprightBeforeRecenter() { val pose = XrealOrientationFilter().update(sample(1000000, accel = Vec3(-0.0909f, -9.6529f, -1.0731f)))!! - // An upright start must not contain the previous approximately 180-degree - // pitch correction. Check before recenter can hide a mounting error. assertTrue(kotlin.math.abs(pose[3]) > 0.99f) val screen = matrix(pose) - assertTrue(screen[7] > 0.99f) // Screen up points toward shared world +Z. + assertTrue(screen[7] > 0.99f) } } diff --git a/app/src/test/java/com/droiddeck/launcher/glasses/XrealProtocolTest.kt b/app/src/test/java/com/droiddeck/launcher/glasses/XrealProtocolTest.kt index 0deee75dd..c84cd7cdb 100644 --- a/app/src/test/java/com/droiddeck/launcher/glasses/XrealProtocolTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/glasses/XrealProtocolTest.kt @@ -93,7 +93,6 @@ class XrealProtocolTest { assertEquals(0x2822, XrealProtocol.read(requests).command) repeat(3) { assertEquals(0x275e, XrealProtocol.read(requests).command) } assertEquals(0, requests.available()) - // Actual 1S busy response remains an error, never a successful ack. assertFalse(XrealProtocol.acknowledged(byteArrayOf(0x22, 3, 8, 0x89.toByte(), 0x27))) } diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/GpuDetectionTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/GpuDetectionTest.kt index 9a22eb1fd..7348210c9 100644 --- a/app/src/test/java/com/droiddeck/launcher/gpu/GpuDetectionTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/gpu/GpuDetectionTest.kt @@ -28,7 +28,6 @@ class GpuDetectionTest { } @Test fun nonAdrenoRenderersAreNeverAdrenoWhateverNumberTheyCarry() { - // Xclipse (Exynos), Mali and Immortalis names; familyOf ignores the number off Adreno. assertEquals(Family.NOT_ADRENO, GpuInfo.familyOf(false, 0)) assertEquals(Family.NOT_ADRENO, GpuInfo.familyOf(false, GpuInfo.threeDigits("Samsung Xclipse 920"))) assertEquals(Family.NOT_ADRENO, GpuInfo.familyOf(false, GpuInfo.threeDigits("Xclipse 940"))) diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/SteamVrDeviceLossLogTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/SteamVrDeviceLossLogTest.kt index e54564dba..7a6daf420 100644 --- a/app/src/test/java/com/droiddeck/launcher/gpu/SteamVrDeviceLossLogTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/gpu/SteamVrDeviceLossLogTest.kt @@ -17,7 +17,7 @@ class SteamVrDeviceLossLogTest { assertFalse(log.poll()) file.appendText("-4\n") assertTrue(log.poll()) - assertTrue(log.poll()) // Keep pending until the activity is foreground. + assertTrue(log.poll()) } @Test fun onlyExplicitDeviceLossTriggersRecovery() { diff --git a/app/src/test/java/com/droiddeck/launcher/hud/FrameStatsTest.kt b/app/src/test/java/com/droiddeck/launcher/hud/FrameStatsTest.kt index ac9348f52..15656e6a8 100644 --- a/app/src/test/java/com/droiddeck/launcher/hud/FrameStatsTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/hud/FrameStatsTest.kt @@ -8,7 +8,6 @@ import org.junit.Test import kotlin.random.Random class FrameStatsTest { - /** MangoHud's update_hud_info_with_frametime and fps()/frame_timing(), line by line (overlay.cpp). */ private class Mango { val frametimeData = ArrayList(List(200) { 0f }) var lastPresentTime = 0L diff --git a/app/src/test/java/com/droiddeck/launcher/hud/MangoReadingsTest.kt b/app/src/test/java/com/droiddeck/launcher/hud/MangoReadingsTest.kt index 246d97296..4de40ea2a 100644 --- a/app/src/test/java/com/droiddeck/launcher/hud/MangoReadingsTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/hud/MangoReadingsTest.kt @@ -18,12 +18,10 @@ class MangoReadingsTest { @Test fun cpuLoadFollowsMangoHudsFormulaAndTruncates() { val stat = File(tmp.root, "stat") val m = readings() - // user nice system idle iowait irq softirq steal guest guest_nice stat.writeText("cpu 1000 100 500 8000 400 50 50 0 200 0\ncpu0 500 50 250 4000 200 25 25 0 100 0\ncpu1 500 50 250 4000 200 25 25 0 100 0\nintr 0\n") m.cpu(stat) stat.writeText("cpu 1500 100 700 8600 500 60 70 10 300 0\ncpu0 900 50 400 4100 250 30 35 10 200 0\ncpu1 600 50 300 4500 250 30 35 0 100 0\nintr 0\n") val cpu = m.cpu(stat)!! - // busy = user-guest 400 + nice 0 + system+irq+softirq 230 + steal 10 + guest 100 = 740 of 1440 (idle+iowait 700) assertEquals((740 * 100f / 1440).toInt(), cpu.percent) assertEquals(51, cpu.percent) assertArrayEquals(intArrayOf(((300 + 165 + 10 + 100) * 100f / (300 + 165 + 10 + 100 + 150)).toInt(), ((100 + 65) * 100f / (100 + 65 + 550)).toInt()), cpu.perCore) diff --git a/app/src/test/java/com/droiddeck/launcher/input/TouchscreenGesturesTest.kt b/app/src/test/java/com/droiddeck/launcher/input/TouchscreenGesturesTest.kt index 5c49c5265..a78cd7f0c 100644 --- a/app/src/test/java/com/droiddeck/launcher/input/TouchscreenGesturesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/input/TouchscreenGesturesTest.kt @@ -738,7 +738,7 @@ class TouchscreenGesturesTest { @Test fun pinchReversalDoesNotConsumeOldDirectionRemainder() { startPinch() events.clear() - movePinch(4.5f) // Nine pixels toward zooming in, short of a step. + movePinch(4.5f) assertTrue(events.none { it.startsWith("wheel") }) for (i in 1..10) movePinch(4.5f - i * .5f) assertEquals(listOf("wheel 1"), events.filter { it.startsWith("wheel") }) @@ -794,16 +794,13 @@ class TouchscreenGesturesTest { override fun moveTo(x: Float, y: Float) { cachedX = x; clientX = x } override fun prepareRelativePan() { cachedX = 960f; clientX = 960f } override fun moveBy(dx: Float, dy: Float, relative: Boolean) { - // Before pointer lock, the compositor delivers an absolute - // position even when we inject a relative delta. The game - // only requests lock after seeing this first pan movement. cachedX += dx clientX = cachedX } override fun button(code: Int, down: Boolean) { if (code == 274) { middleDown = down - if (down) clientX = 960f // The client warps to the middle when pan starts. + if (down) clientX = 960f } } override fun key(code: Int, down: Boolean) {} diff --git a/app/src/test/java/com/droiddeck/launcher/session/BootMovieLibraryTest.kt b/app/src/test/java/com/droiddeck/launcher/session/BootMovieLibraryTest.kt index 2c1d90ca0..e40328041 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/BootMovieLibraryTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/BootMovieLibraryTest.kt @@ -29,7 +29,6 @@ class BootMovieLibraryTest { private fun movie(name: String) = File(movies, name).also { it.parentFile!!.mkdirs(); it.writeText("movie") } - /** A movie DroidDeck itself imported: kept in the library, copied into Steam's folder and noted in the list. */ private fun imported(title: String, source: String = BootMovieLibrary.SOURCE_IMPORT, repo: String? = null): String { val staged = File(BootMovieLibrary.libraryDir(app), ".t-${System.nanoTime()}-${Thread.currentThread().id}.staged").also { it.parentFile!!.mkdirs(); it.writeText("movie") } return BootMovieLibrary.commit(app, staged, title, source, repo)!! @@ -258,11 +257,9 @@ class BootMovieLibraryTest { steamPicks("b.webm") assertTrue(BootMovie.syncFromSteam(app)) assertEquals("file:b.webm", BootMovie.choice(app)) - // Picking another one here is not undone by the same Steam setting. BootMovie.select(app, "file:a.webm") assertFalse(BootMovie.syncFromSteam(app)) assertEquals("file:a.webm", BootMovie.choice(app)) - // A new pick inside Steam is adopted again. movie("c.webm") steamPicks("c.webm") assertTrue(BootMovie.syncFromSteam(app)) diff --git a/app/src/test/java/com/droiddeck/launcher/session/ComponentsNightlyCatalogTest.kt b/app/src/test/java/com/droiddeck/launcher/session/ComponentsNightlyCatalogTest.kt index 8fbb0bacd..5dbd3163f 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/ComponentsNightlyCatalogTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/ComponentsNightlyCatalogTest.kt @@ -55,11 +55,9 @@ class ComponentsNightlyCatalogTest { val nVkEc = item("DD-vkd3d-proton-arm64ec-3.0.1+455-nightly.wcp", "vkd3d", n) val pairs = ComponentsManager.nightlyPairs(listOf(dxvk, ec, gpl, fex, vk) + others + listOf(nDxvk, nEc, nGpl, nFex, nVk, nVkEc)) assertEquals(mapOf(dxvk to nDxvk, ec to nEc, gpl to nGpl, fex to nFex, vk to nVk), pairs) - // Two nightlies for one stable build: the newer one wins. val nDxvkNewer = item("DD-dxvk-3.1.1+154-nightly.wcp", "dxvk", n) assertEquals(mapOf(dxvk to nDxvkNewer), ComponentsManager.nightlyPairs(listOf(dxvk, nDxvkNewer, nDxvk))) assertEquals(mapOf(dxvk to nDxvkNewer), ComponentsManager.nightlyPairs(listOf(dxvk, nDxvk, nDxvkNewer))) - // A nightly of a version with no stable build pairs with nothing. assertEquals(emptyMap(), ComponentsManager.nightlyPairs(listOf(item("dxvk-3.1-linux.wcp", "dxvk", "Dxvk-Linux"), nDxvk))) } diff --git a/app/src/test/java/com/droiddeck/launcher/session/ForcedPackageTest.kt b/app/src/test/java/com/droiddeck/launcher/session/ForcedPackageTest.kt index ba026b474..241d81545 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/ForcedPackageTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/ForcedPackageTest.kt @@ -36,7 +36,6 @@ class ForcedPackageTest { ComponentsManager.unpackedCacheKey(filename) fail("Expected cache traversal name to be rejected: $filename") } catch (_: IllegalArgumentException) { - // Expected: the derived path would be a dot segment. } } } diff --git a/app/src/test/java/com/droiddeck/launcher/session/GameTelemetryTest.kt b/app/src/test/java/com/droiddeck/launcher/session/GameTelemetryTest.kt index fc5cbfca9..ebbd21b66 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/GameTelemetryTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/GameTelemetryTest.kt @@ -16,12 +16,12 @@ class GameTelemetryTest { @Test fun codemastersFloatsBecomeADash() { val data = packet(264) { - putFloat(7 * 4, 25f) // m/s + putFloat(7 * 4, 25f) putFloat(29 * 4, 0.8f) putFloat(31 * 4, 0.1f) putFloat(33 * 4, 3f) putFloat(36 * 4, 1f) - putFloat(37 * 4, 650f) // rpm / 10 + putFloat(37 * 4, 650f) putFloat(63 * 4, 800f) putFloat(45 * 4, 30f) putFloat(46 * 4, 60f) @@ -35,7 +35,6 @@ class GameTelemetryTest { assertEquals(0.5f, t.fuel!!, 0.001f) } - // Values from a DiRT Showdown packet recorded on a Retroid Pocket Duo (extradata 3 still sends 152 bytes there). @Test fun codemastersShortPacketFromDirtShowdown() { val data = packet(152) { putFloat(1 * 4, 80.085f) @@ -124,7 +123,6 @@ class GameTelemetryTest { assertNull(GameTelemetry.parse(1234, ByteArray(400), 400)) } - // DiRT Showdown's 38 floats already carry steering and both G-forces. @Test fun codemastersGForceAndSteering() { val data = packet(152) { putFloat(30 * 4, -0.5f) @@ -181,7 +179,6 @@ class GameTelemetryTest { assertEquals(GameTelemetry.DL_SHIFT or GameTelemetry.DL_ABS, t.lights) } - // BeamNG leaves some OutGauge fields at zero; those stay hidden. @Test fun outGaugeZeroFieldsStayHidden() { val t = GameTelemetry.parse(GameTelemetry.OUTGAUGE_PORT, packet(92) { put(10, 2) }, 92)!! assertNull(t.turboBar) diff --git a/app/src/test/java/com/droiddeck/launcher/session/GameThreadHintsTest.kt b/app/src/test/java/com/droiddeck/launcher/session/GameThreadHintsTest.kt index 16f5d7d2c..8233957e2 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/GameThreadHintsTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/GameThreadHintsTest.kt @@ -9,9 +9,7 @@ class GameThreadHintsTest { @Test fun `a thread joins above 60 percent and leaves under 15`() { assertEquals(emptySet(), GameThreadHints.pick(emptySet(), mapOf(1 to 0.55))) assertEquals(setOf(1), GameThreadHints.pick(emptySet(), mapOf(1 to 0.68))) - // Faster where it was moved, or capped by the display, it stays boosted at 25%... assertEquals(setOf(1), GameThreadHints.pick(setOf(1), mapOf(1 to 0.25))) - // ...and is let go once it idles. assertEquals(emptySet(), GameThreadHints.pick(setOf(1), mapOf(1 to 0.1))) } @@ -28,8 +26,8 @@ class GameThreadHintsTest { assertTrue(GameThreadHints.holdsPace(119.0, 120.0)) assertTrue(GameThreadHints.holdsPace(60.5, 120.0)) assertTrue(GameThreadHints.holdsPace(30.0, 120.0)) - assertFalse(GameThreadHints.holdsPace(92.0, 120.0)) // falling behind: boost + assertFalse(GameThreadHints.holdsPace(92.0, 120.0)) assertFalse(GameThreadHints.holdsPace(105.0, 120.0)) - assertFalse(GameThreadHints.holdsPace(3.0, 120.0)) // headless or loading: boost + assertFalse(GameThreadHints.holdsPace(3.0, 120.0)) } } 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 674f6211e..ff7fd9510 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/SessionLogShareZipTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/SessionLogShareZipTest.kt @@ -27,7 +27,6 @@ class SessionLogShareZipTest { val earlySecret = "early-sessionid-abcdef123456" val trailingSecret = "late-refresh-token-abcdef123456" try { - // One long line exercises the streaming limit; the final ordinary line must still be redacted. raw.bufferedWriter().use { out -> out.write("sessionid=$earlySecret") val chunk = "x".repeat(8192) @@ -40,7 +39,6 @@ class SessionLogShareZipTest { ZipFile(zip).use { archive -> val entry = archive.getEntry("${session.name}/steam-42.log")!! archive.getInputStream(entry).use { input -> - // Only inspect the bounded scrubbed prefix: the pre-fix raw archive is over 64 MiB. val expected = "${com.droiddeck.launcher.core.LogRedactor.OVERSIZED_LINE}\n" val prefix = ByteArray(expected.toByteArray(StandardCharsets.UTF_8).size) var offset = 0 @@ -74,7 +72,6 @@ class SessionLogShareZipTest { "Authorization: Bearer abc.def.ghi\n" + "kept: EResult 5, app 1086940, steamapps/common/Game\n" ) - // StoreLogFiles.prune, started by App.onCreate, deletes a stores log named for a day over a week old. val storesLog = "stores-" + SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date()) + ".log" File(logs, "stores").apply { mkdirs() }.resolve(storesLog).writeText("epic: f_token=zzzzzzzz12 window 32\n") File(logs, SessionPaths.TOOLS_DIR).apply { mkdirs() }.resolve("flatpak-install.log").writeText("== install\nCookie: steamLoginSecure=76561198000000000%7C%7Ceyabc\n") diff --git a/app/src/test/java/com/droiddeck/launcher/session/SteamWebLoginTest.kt b/app/src/test/java/com/droiddeck/launcher/session/SteamWebLoginTest.kt index 01988bf84..0b8fadada 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/SteamWebLoginTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/SteamWebLoginTest.kt @@ -7,7 +7,6 @@ import org.junit.Test class SteamWebLoginTest { private fun hex(s: String) = ByteArray(s.length / 2) { s.substring(it * 2, it * 2 + 2).toInt(16).toByte() } - // Made with: openssl enc -aes-128-cbc -K -iv <16 spaces>, prefixed "v10". @Test fun chromiumV10ValuesDecrypt() { val encrypted = hex("7631300b9cf0a6eee9cf1aaac601b85ca37cf9fe9e67078564f0e05835ae2aa3d9b0ab") assertEquals("0123456789abcdef01234567", SteamWebLogin.decrypt(encrypted, hostHashed = false)) diff --git a/app/src/test/java/com/droiddeck/launcher/session/TelemetryOutputTest.kt b/app/src/test/java/com/droiddeck/launcher/session/TelemetryOutputTest.kt index cd65b2a57..248b4a179 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/TelemetryOutputTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/TelemetryOutputTest.kt @@ -12,7 +12,6 @@ import java.io.File class TelemetryOutputTest { @get:Rule val tmp = TemporaryFolder() - // DiRT Showdown's own line, as found in its prefix on the Retroid Pocket Duo. private val showdown = "\n\n" + "\t\n" diff --git a/app/src/test/java/com/droiddeck/launcher/stores/StoreCatalogPatchTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/StoreCatalogPatchTest.kt index e545cc77d..e3b91ff6f 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/StoreCatalogPatchTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/StoreCatalogPatchTest.kt @@ -4,7 +4,6 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertSame import org.junit.Test -/** Filling a grid one card at a time: what is on screen stays put, what is new lands where its title belongs. */ class StoreCatalogPatchTest { private fun item(id: String, title: String, image: String? = null) = CatalogItem(Store.GOG, id, title, imageUrl = image, owned = true) 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 index 879e8de91..072999178 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadQueueRetryTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadQueueRetryTest.kt @@ -75,7 +75,6 @@ class DownloadQueueRetryTest { 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") diff --git a/app/src/test/java/com/droiddeck/launcher/stores/gog/GogListingTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/gog/GogListingTest.kt index 0c6502e93..5aace971f 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/gog/GogListingTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/gog/GogListingTest.kt @@ -7,7 +7,6 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config -/** The account listing: everything the tab needs to draw a library before a product is asked about. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class GogListingTest { diff --git a/app/src/test/java/com/droiddeck/launcher/ui/BottomDashboardTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/BottomDashboardTest.kt index 5a5141889..5d167e14e 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/BottomDashboardTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/BottomDashboardTest.kt @@ -31,7 +31,6 @@ class BottomDashboardTest { } @Test fun tabNamesShowOnlyWhileEveryTabFits() { - // Retroid Pocket Duo bottom panel: 1280x960 at 320 dpi = 640 dp wide. assertTrue(bottomTabsWithLabels(640f, 4)) assertFalse(bottomTabsWithLabels(640f, 5)) assertFalse(bottomTabsWithLabels(360f, 3)) diff --git a/app/src/test/java/com/droiddeck/launcher/ui/DeckControlsPanelTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/DeckControlsPanelTest.kt index 4aa3a0327..c6ebe838d 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/DeckControlsPanelTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/DeckControlsPanelTest.kt @@ -22,14 +22,11 @@ class DeckControlsPanelTest { val directory = Files.createTempDirectory("deck-panel").toFile() try { SessionState.deckPad = false - // No pad: the Deck layouts (trackpads, grips). A pad without Deck controls: the gamepad layout - // (sticks, d-pad, face buttons). for (pad in listOf(null, PadBridge(directory))) for (portraitDock in listOf(false, true)) { val name = "pad=${pad != null} portraitDock=$portraitDock" val panel = DeckControlsPanel(RuntimeEnvironment.getApplication(), onClose = {}, onSteamMenu = {}, onQam = {}, pad = pad, portraitDock = portraitDock) assertEquals(name, pad == null, panel.deckControls) - // The portrait dock sits under a portrait game; its gamepad layout needs a tall panel. val (w, h) = if (portraitDock) 720 to 1000 else 1200 to 480 panel.measure(View.MeasureSpec.makeMeasureSpec(w, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(h, View.MeasureSpec.EXACTLY)) @@ -41,7 +38,6 @@ class DeckControlsPanelTest { val down = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, x.toFloat(), y.toFloat(), 0) assertEquals("$name ($x, $y)", expected, panel.dispatchTouchEvent(down)) down.recycle() - // Cancel rather than lift, so no button fires its action. MotionEvent.obtain(0L, 1L, MotionEvent.ACTION_CANCEL, x.toFloat(), y.toFloat(), 0).also { panel.dispatchTouchEvent(it); it.recycle() } if (expected) taken++ else missed++ } diff --git a/app/src/test/java/com/droiddeck/launcher/ui/TrackingRecoveryViewTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/TrackingRecoveryViewTest.kt index 3275e622f..c921d7370 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/TrackingRecoveryViewTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/TrackingRecoveryViewTest.kt @@ -78,7 +78,6 @@ class TrackingRecoveryViewTest { difference+=abs(((actual ushr 16) and 255)-((reference ushr 16) and 255)) } } - // Native text rasterization can round a translated glyph edge differently. assertTrue("Eye at x=$x changed $changed pixels",changedreal_volume), PA_VOLUME_NORM); level = ((uint64_t)requested * maximum + PA_VOLUME_NORM / 2) / PA_VOLUME_NORM; pa_cvolume_set(&sink->real_volume, sink->sample_spec.channels, (uint64_t)level * PA_VOLUME_NORM / maximum); - /* Android applies media gain to both PulseAudio and DirectAudio streams. */ pa_cvolume_reset(&sink->soft_volume, sink->sample_spec.channels); android_volume_request(sink, "volume", level); } @@ -88,7 +87,6 @@ static void android_volume_refresh(pa_mainloop_api *api, pa_time_event *event, c pa_xfree(mute_request); pa_xfree(processing); pa_xfree(mute_processing); - /* A held volume key steps Android every ~50 ms; sampling at 100 ms hands Steam uneven multi-step jumps. */ unsigned delay_ms = ANDROID_VOLUME_IDLE_MS; if (bridge->active_ticks) { bridge->active_ticks--; diff --git a/tools/build_local.sh b/tools/build_local.sh index 222988773..7952d7a0e 100755 --- a/tools/build_local.sh +++ b/tools/build_local.sh @@ -86,8 +86,6 @@ trap 'exit 143' TERM cache_dir=${DROIDDECK_BUILD_CACHE:-"${HOME}/.cache/droiddeck-build"} mkdir -p "${cache_dir}" -# Steps whose output depends only on tracked sources are kept per input hash in the -# shared cache, so a rerun or a fresh worktree copies them instead of rebuilding. inputs_hash() { (cd "${repo_root}" && { printf '%s\n' "$1"; shift; find "$@" -type f ! -name '*.pyc' | LC_ALL=C sort | xargs shasum -a 256; }) \ | shasum -a 256 | cut -d' ' -f1 @@ -105,7 +103,6 @@ save_step() { part=$(mktemp -d "${stored}.part.XXXXXX") (cd "$3" && tar -cf - .) | tar -xf - -C "${part}" touch "${part}/.complete" - # Another checkout may have published the same key meanwhile; its copy is equivalent. if [[ -e "${stored}" ]]; then rm -rf "${part}"; else mv "${part}" "${stored}" || rm -rf "${part}"; fi } diff --git a/tools/cardboard/test_cardboard_params.c b/tools/cardboard/test_cardboard_params.c index 8b284cf6a..e1f761c1e 100644 --- a/tools/cardboard/test_cardboard_params.c +++ b/tools/cardboard/test_cardboard_params.c @@ -45,8 +45,6 @@ int main(void) { cardboard_push_constants(&q, pose, 0, 0, 0, 0, pc); CHECK(isfinite(pc[17]) && isfinite(pc[19])); - // Narrow optical profiles and the lower SteamVR slider values must reach - // the projection unchanged, rather than all becoming a 30-degree view. const float glasses_fovs[] = {22.0f, 23.3f, 24.0f, 25.8f, 29.0f}; for (unsigned i = 0; i < sizeof(glasses_fovs) / sizeof(glasses_fovs[0]); i++) { q.fov_deg = glasses_fovs[i]; diff --git a/tools/check_glasses_menu_shader.py b/tools/check_glasses_menu_shader.py index 1b775b3e3..45ed20677 100644 --- a/tools/check_glasses_menu_shader.py +++ b/tools/check_glasses_menu_shader.py @@ -156,7 +156,6 @@ def main(): read(0,0,width,height,0x1908,0x1401,actual) samples = [] for x, y in coords: - # Readback is GL bottom-up; coordinates below are image top-left. offset = ((height-1-y)*width+x)*4 rgb = list(actual[offset:offset+3]) eye = 0 if mono or x < width/2 else 1 diff --git a/tools/components/preloads.py b/tools/components/preloads.py index 5f16610c3..808d51046 100644 --- a/tools/components/preloads.py +++ b/tools/components/preloads.py @@ -1,4 +1,3 @@ -"""Reuse reviewed preload snapshots; changed snapshots must prove their main-build origin.""" import argparse import gzip import http.client @@ -147,7 +146,6 @@ def producer_sources(group, commit, root=ROOT): name = 'arm64-preloads' if group == 'arm64' else 'x86-preloads' patterns = definitions[name]['source_inputs'] else: - # Older approved builds kept ARM64 commands inline, before the Vulkan/VR build. if group == 'arm64': text = run('git', 'show', f'{commit}:.github/workflows/build.yml', root=root).decode() text = text[text.index(' aarch64-linux-gnu-g++ -shared'):] diff --git a/tools/components/prepare.py b/tools/components/prepare.py index 064b452be..acccf54e8 100644 --- a/tools/components/prepare.py +++ b/tools/components/prepare.py @@ -1,4 +1,3 @@ -"""One entry point for runtime recipes, cache identities and verified job handoff.""" import argparse import hashlib import json diff --git a/tools/deploy_fast.py b/tools/deploy_fast.py index 259dc89cb..958ac3051 100644 --- a/tools/deploy_fast.py +++ b/tools/deploy_fast.py @@ -63,7 +63,6 @@ def graft_targets(): def changed_paths(donor_commit): - """Return {path: status} for the checkout, uncommitted and untracked files included.""" changes = {} fields = git('diff', '--name-status', '-z', '--no-renames', donor_commit).decode().split('\0') for status, path in zip(fields[0::2], fields[1::2]): @@ -74,7 +73,6 @@ def changed_paths(donor_commit): def classify(changes, targets, donor_entries): - """Split changes into grafts {path: asset}, ignored paths and blockers {path: reason}.""" grafts, ignored, blocked = {}, [], {} for path, status in sorted(changes.items()): if is_ignored(path): @@ -96,7 +94,6 @@ def helper_bytes(path): def graft(donor, replacements, output): - """Copy the donor with entries replaced, dropping its signature so it can be re-signed.""" with ZipFile(donor) as source, ZipFile(output, 'w') as target: for info in source.infolist(): name = info.filename @@ -200,7 +197,6 @@ def pwsh(): def signing_env(): - """The .signing.env build_local.sh uses: the override, else the checkout's own, else the main checkout's.""" common = Path(git('rev-parse', '--path-format=absolute', '--git-common-dir').decode().strip()) for candidate in (os.environ.get('DROIDDECK_SIGNING_ENV'), ROOT / '.signing.env', common.parent / '.signing.env'): if candidate and Path(candidate).is_file(): @@ -218,7 +214,6 @@ def sign_with_env(env_file, unsigned, signed): build_tools = sorted(complete, key=version_key, reverse=True) if not build_tools: raise Refused('no build-tools with aapt, zipalign and apksigner under ' + str(sdk_root() / 'build-tools')) - # BUILD_TOOLS is assigned after the env file is sourced so the file cannot replace the chosen SDK. script = 'set -a; cd "$(dirname "$1")"; . "$1"; set +a; BUILD_TOOLS="$5" exec "$2" "$3" standard "$4"' result = subprocess.run(['bash', '-c', script, 'sign', str(env_file), str(ROOT / 'tools/release/sign-apk.sh'), str(unsigned), str(signed), str(build_tools[0].parent.resolve())], @@ -230,7 +225,6 @@ def sign_with_env(env_file, unsigned, signed): def sign(unsigned, signed): if not SIGNER.is_file() and (env_file := signing_env()): return sign_with_env(env_file, unsigned, signed) - # Microsoft Store Python virtualizes AppData for child processes too and hides the signer; use a python.org install. def quoted(value): return "'" + str(value).replace("'", "''") + "'" command = (f"if (-not (Test-Path -LiteralPath {quoted(SIGNER)})) {{ Write-Error 'Local signer not found'; exit 1 }}; " diff --git a/tools/desktop-kde/closure.py b/tools/desktop-kde/closure.py index 2c74e299f..73d3781a4 100755 --- a/tools/desktop-kde/closure.py +++ b/tools/desktop-kde/closure.py @@ -1,11 +1,4 @@ #!/usr/bin/env python3 -"""The packages the KDE desktop package ships: the dependency closure of the seeds over Arch Linux -ARM's extracted repository databases, minus what the runtime already has at a version that serves. - - closure.py - -Prints one "repo/filename sha256" per package to ship (the sha256 the repository database gives). -""" import collections import os import re @@ -107,7 +100,6 @@ def main(): providers[pname].append((name, pver or pkgs[name]["version"])) def resolve(dep): - """The package to take for a dependency string, and whether the runtime's copy serves.""" name, op, want = parse_dep(dep) if name in runtime and satisfies(runtime[name], op, want): return name, True diff --git a/tools/desktop-kde/patches/0002-select-explicit-nested-render-node.patch b/tools/desktop-kde/patches/0002-select-explicit-nested-render-node.patch index 369ce1a56..8c0b8cccb 100644 --- a/tools/desktop-kde/patches/0002-select-explicit-nested-render-node.patch +++ b/tools/desktop-kde/patches/0002-select-explicit-nested-render-node.patch @@ -1,13 +1,12 @@ --- a/src/backends/wayland/wayland_backend.cpp +++ b/src/backends/wayland/wayland_backend.cpp -@@ -455,6 +455,20 @@ +@@ -455,6 +455,19 @@ if (WaylandClient::LinuxDmabufV1 *dmabuf = m_display->linuxDmabuf()) { m_renderDevice = GpuManager::self()->compatibleRenderDevice(dmabuf->mainDevice()); + if (!m_renderDevice && qEnvironmentVariableIsSet("DROIDDECK_NESTED_SCALE")) { + const auto nodes = GpuManager::splitPathList(qEnvironmentVariable("KWIN_RENDER_NODES")); + const auto &devices = GpuManager::self()->renderDevices(); -+ // Android's KGSL host cannot name a DRM device in its dmabuf feedback. + if (nodes.size() == 1) { + for (const auto &device : devices) { + if (device->path() == nodes.constFirst()) { diff --git a/tools/desktop-kde/removal-manifests.py b/tools/desktop-kde/removal-manifests.py index 1089f9421..1e9079697 100644 --- a/tools/desktop-kde/removal-manifests.py +++ b/tools/desktop-kde/removal-manifests.py @@ -1,15 +1,4 @@ #!/usr/bin/env python3 -"""Generate removal inventories from checksum-pinned published archives. - -Usage: removal-manifests.py - - - removal-manifests.py --add -Removal includes desktop applications/themes and our overlay, not their shared dependencies. -The KDE cache is verified against the published desktop-kde.packages.txt before use. -The LXQt cache contains the signed application packages listed by desktop.packages.txt; -verify their Arch Linux ARM Build System signatures before generating the inventory. -""" import gzip import hashlib import json diff --git a/tools/device-smoke.sh b/tools/device-smoke.sh index 80a883805..479088937 100755 --- a/tools/device-smoke.sh +++ b/tools/device-smoke.sh @@ -1,26 +1,31 @@ #!/usr/bin/env bash -# Smoke test of a DroidDeck build on a real device, over plain adb (no python, no root). -# -# What it does, in order, and restores afterwards: -# 1. prints the device (model, Android, kernel, page size, SoC); -# 2. installs --apk when given and launches the app; -# 3. checks that the app starts and that nothing in the crash buffer belongs to it; -# 4. changes the display density and the window size and checks the app keeps its process -# (and does not recreate the activity that is on top); -# 5. on a foldable cycles the device state (half-opened, and with --closed also closed) through -# `cmd device_state`, then puts the original state back; -# 6. reads the newest session folder of a debug build: first frame, last frame rate, the -# memory page size, the proot that session ran, and the last process exits. -# -# Usage: tools/device-smoke.sh [--serial SERIAL] [--apk FILE] [--pkg PACKAGE] -# [--no-density] [--no-size] [--no-fold] [--closed] -# Exit status: 0 when no check failed, 1 when one did, 2 for a usage or device problem. -# -# Not a replacement for tools/droiddeck-scenario, which drives a game through the agent bridge; -# this covers what that does not: display changes and foldable states. set -u export MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' +usage() { + cat <<'EOF' +Smoke test of a DroidDeck build on a real device, over plain adb (no python, no root). + +What it does, in order, and restores afterwards: + 1. prints the device (model, Android, kernel, page size, SoC); + 2. installs --apk when given and launches the app; + 3. checks that the app starts and that nothing in the crash buffer belongs to it; + 4. changes the display density and the window size and checks the app keeps its process + (and does not recreate the activity that is on top); + 5. on a foldable cycles the device state (half-opened, and with --closed also closed) through + `cmd device_state`, then puts the original state back; + 6. reads the newest session folder of a debug build: first frame, last frame rate, the + memory page size, the proot that session ran, and the last process exits. + +Usage: tools/device-smoke.sh [--serial SERIAL] [--apk FILE] [--pkg PACKAGE] + [--no-density] [--no-size] [--no-fold] [--closed] +Exit status: 0 when no check failed, 1 when one did, 2 for a usage or device problem. + +Not a replacement for tools/droiddeck-scenario, which drives a game through the agent bridge; +this covers what that does not: display changes and foldable states. +EOF +} + pkg=com.droiddeck.launcher serial=${ANDROID_SERIAL:-} apk= @@ -39,7 +44,7 @@ while [ $# -gt 0 ]; do --no-size) do_size=0 ;; --no-fold) do_fold=0 ;; --closed) do_closed=1 ;; - -h|--help) sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + -h|--help) usage; exit 0 ;; *) echo "unknown option: $1" >&2; exit 2 ;; esac shift @@ -96,7 +101,6 @@ restore_size() { echo "$current" | grep -q 'Physical size:' || return 1 [ "$(echo "$current" | wm_override size)" = "$override_size" ] } -# An override that was already in place (a test lab pins one) is put back; otherwise the request is cancelled. restore_fold() { if [ -n "$pre_override" ]; then sh_ cmd device_state state "$pre_override" >/dev/null; else sh_ cmd device_state state reset >/dev/null; fi; } restore() { @@ -133,7 +137,7 @@ app_crashes() { pat=$(printf '%s' "$pkg" | sed 's/[.[\*^$]/\\&/g') printf '%s\n' "$out" | grep -c -E "(Process: |Cmdline: |>>> )$pat([ ,<:]|$)" || true } -relaunches_of() { # $1 = activity class fragment +relaunches_of() { sh_ logcat -b events -d 2>/dev/null | grep -E 'wm_(relaunch|destroy|create)_activity' | grep -c "$1" } @@ -214,7 +218,7 @@ if [ "$do_size" = 1 ]; then phys=$(echo "$size_snapshot" | sed -n 's/.*Physical size: \([0-9]*\)x\([0-9]*\).*/\1 \2/p' | head -1) set -- $phys if [ -n "${1:-}" ]; then - w=$(( ($2 * 2 / 3) / 2 * 2 )); h=$1 # a narrower, taller window, like a cover screen + w=$(( ($2 * 2 / 3) / 2 * 2 )); h=$1 pid0=$(app_pid) size_changed=1 wm_status=0 @@ -250,7 +254,7 @@ if [ "$do_fold" = 1 ]; then else id_of() { echo "$states" | sed -n "s/.*identifier=\([0-9]*\), name='$1'.*/\1/p" | head -1; } half=$(id_of HALF_OPENED); closed=$(id_of CLOSED) - cycle() { # $1 name $2 id + cycle() { pid0=$(app_pid) fold_changed=1 sh_ cmd device_state state "$2" >/dev/null; sleep 8 diff --git a/tools/directaudio/sink/da_relay_proto.h b/tools/directaudio/sink/da_relay_proto.h index 351cdfb60..2207ae929 100644 --- a/tools/directaudio/sink/da_relay_proto.h +++ b/tools/directaudio/sink/da_relay_proto.h @@ -1,49 +1,10 @@ -/* DirectAudio relay - the contract between the driver and the relay helper. - * +/* * Copyright 2026 The412Banner * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. - * - * WHY THIS EXISTS. The driver's whole premise is an in-process AAudio client: - * the guest's WASAPI buffers are mixed and handed to Android's audio server - * from inside the game, no daemon in the path. That is only possible when the - * game process is a bionic process. Under a glibc runtime (the Linux Steam - * client, where the game runs inside a proot'd Arch rootfs on Valve's own - * ARM64 Proton) libaaudio cannot be loaded at all: it is a bionic library and - * the rootfs's dynamic linker does not know how to bring it into a glibc - * process. The game side therefore has to stop at a boundary it CAN cross, and - * the only thing both worlds agree on is the kernel: a unix socket, a memfd and - * a futex word. - * - * SHAPE. One helper process (directaudio-relay, bionic, links libaaudio) runs - * on the Android side and listens on a unix socket. Each game process that - * needs audio connects once, sends its launch config (the same knobs the - * in-process build reads from BANNER_AUDIO_DIRECT_*), and gets back one or two - * shared rings (memfds passed with SCM_RIGHTS): - * - * render ring game (producer) -> helper (consumer) -> AAudio OUTPUT - * capture ring helper (producer, AAudio INPUT) -> game (consumer) - * - * The driver keeps its mixer and its per-voice WASAPI rings exactly as in the - * in-process build; what used to be the AAudio data callback is now a pump - * thread that keeps the render ring topped up, and the capture callback is a - * pump that drains the capture ring into the capture voices. Everything that - * needs an AAudioStream (open, adaptive buffer growth and decay, route-change - * reopen, the stalled-callback watchdog, the live-config mailbox) moves to the - * helper unchanged in spirit. - * - * COST. One extra hand-off: the ring. The helper's callback pulls a burst from - * the ring, so the ring must hold at least one burst at every callback; the - * driver keeps DA_RING_TARGET_BURSTS bursts queued and the helper raises that - * target when a callback finds the ring short. Everything else - the AAudio - * buffer, AudioFlinger's own latency - is what the in-process build pays too. - * - * WIRE FORMAT. Fixed-width, native-endian, same-machine only. The two sides are - * different libcs but the same CPU, so no marshalling beyond fixed-size structs. - * Bump DA_RELAY_VERSION on any layout change; the helper refuses a mismatch. */ #ifndef DA_RELAY_PROTO_H #define DA_RELAY_PROTO_H @@ -52,34 +13,20 @@ #include #define DA_RELAY_VERSION 1 -#define DA_RING_MAGIC 0x44415247u /* 'DARG' */ -#define DA_HELLO_MAGIC 0x44414831u /* 'DAH1' */ -#define DA_ACK_MAGIC 0x44414131u /* 'DAA1' */ +#define DA_RING_MAGIC 0x44415247u +#define DA_HELLO_MAGIC 0x44414831u +#define DA_ACK_MAGIC 0x44414131u -/* Socket path. The driver reads BANNER_AUDIO_DIRECT_RELAY; when unset it falls - * back to $XDG_RUNTIME_DIR/DA_RELAY_DEFAULT_NAME, which a host that already - * hands its runtime dir into the session gets for free. */ #define DA_RELAY_ENV "BANNER_AUDIO_DIRECT_RELAY" #define DA_RELAY_DEFAULT_NAME "directaudio-relay" -/* Both rings carry float32 interleaved stereo. The render ring is always - * 48 kHz (the mixer's output rate); the capture ring runs at whatever rate the - * input stream was granted, published in da_ring.rate, and the driver resamples - * per voice as the in-process build already does. */ #define DA_RING_RATE 48000 #define DA_RING_CHANNELS 2 -/* Render ring sizing. Capacity is the ceiling adaptive growth can reach; the - * initial target is what the driver keeps queued. Two bursts: the helper's - * callback consumes one burst and wakes the pump, which then has a full burst - * period to refill before the next callback. */ #define DA_RING_CAP_MS 250 #define DA_RING_TARGET_BURSTS 2 #define DA_RING_MAX_TARGET_MS 100 -/* Capture ring: 200 ms is far more than a game ever leaves queued; on overrun - * the helper drops the newest block, matching the in-process "guest is not - * draining" behaviour of dropping data rather than blocking the mic thread. */ #define DA_CAPTURE_RING_MS 200 enum da_ring_state @@ -93,35 +40,27 @@ struct da_ring { uint32_t magic; uint32_t version; - uint32_t cap_frames; /* ring capacity in frames */ - uint32_t rate; /* frames per second of the data in this ring */ - uint32_t channels; /* DA_RING_CHANNELS */ + uint32_t cap_frames; + uint32_t rate; + uint32_t channels; uint32_t _pad0; - /* Single producer, single consumer; monotonic frame totals, wrap-safe with - * unsigned arithmetic. Producer writes widx with RELEASE after the data; - * consumer writes ridx with RELEASE after it has copied the data out. */ volatile uint32_t widx; volatile uint32_t ridx; - /* Futex word the CONSUMER of a render ring / PRODUCER of a capture ring - * bumps after each block, so the other side can sleep on it instead of - * polling. Waiters always use a timeout, so a missed wake only costs one - * period. */ volatile uint32_t wake; - volatile uint32_t quit; /* either side: tear down */ - volatile uint32_t state; /* enum da_ring_state, helper-owned */ + volatile uint32_t quit; + volatile uint32_t state; - /* Render ring only, helper-owned, read by the driver: */ - volatile int32_t target_frames; /* keep at least this many queued */ - volatile int32_t hw_buf_frames; /* AAudio buffer size right now (get_latency) */ - volatile int32_t hw_burst; /* frames per burst of the output stream */ - volatile uint32_t underruns; /* callbacks that found fewer than they needed */ - volatile uint32_t cb_count; /* data callbacks so far (liveness) */ + volatile int32_t target_frames; + volatile int32_t hw_buf_frames; + volatile int32_t hw_burst; + volatile uint32_t underruns; + volatile uint32_t cb_count; uint32_t _pad1[3]; - float data[]; /* cap_frames * channels */ + float data[]; }; static inline size_t da_ring_bytes(uint32_t cap_frames) @@ -141,62 +80,52 @@ static inline uint32_t da_ring_space(const struct da_ring *r) return r->cap_frames - da_ring_avail(r); } -/* ---- wire messages ------------------------------------------------------- */ - -#define DA_HELLO_MIC 0x01u /* also open an INPUT stream + capture ring */ -#define DA_HELLO_EXCLUSIVE 0x02u /* AAUDIO_SHARING_MODE_EXCLUSIVE requested */ +#define DA_HELLO_MIC 0x01u +#define DA_HELLO_EXCLUSIVE 0x02u #define DA_HELLO_ADAPTIVE 0x04u #define DA_HELLO_DECAY 0x08u #define DA_HELLO_WATCHDOG 0x10u -/* First message on a fresh connection, driver -> helper. Mirrors the launch - * config the in-process build derives from BANNER_AUDIO_DIRECT_* so a preset - * means the same thing under both runtimes. */ struct da_hello { - uint32_t magic; /* DA_HELLO_MAGIC */ - uint32_t version; /* DA_RELAY_VERSION */ - uint32_t flags; /* DA_HELLO_* */ - int32_t perf; /* 0 NONE, 1 LOW_LATENCY, 2 POWER_SAVING (host convention) */ - int32_t target_ms; /* AAudio buffer, ms; 0 = use target_frames or the default */ - int32_t max_ms; /* adaptive ceiling, ms; 0 = max_frames or the default */ - int32_t target_frames; /* frame forms, used only when the ms forms are 0 */ + uint32_t magic; + uint32_t version; + uint32_t flags; + int32_t perf; + int32_t target_ms; + int32_t max_ms; + int32_t target_frames; int32_t max_frames; - uint32_t stall_ms; /* watchdog: callback silent this long => rebuild */ - uint32_t quiet_ms; /* decay pacing, see the driver */ + uint32_t stall_ms; + uint32_t quiet_ms; uint32_t punish_ms; uint32_t max_backoff; - int32_t pid; /* the game process, for the log */ - uint32_t log; /* BANNER_AUDIO_DIRECT_LOG */ - char runtime_path[256]; /* BANNER_AUDIO_DIRECT_RUNTIME mailbox, "" = none */ - char name[64]; /* process name, for the log */ + int32_t pid; + uint32_t log; + char runtime_path[256]; + char name[64]; }; -#define DA_ACK_MIC_FAILED 0x01u /* mic asked for, input stream would not open */ +#define DA_ACK_MIC_FAILED 0x01u -/* Reply, helper -> driver, followed by SCM_RIGHTS carrying the ring fds: - * fd[0] = render ring, fd[1] = capture ring (present iff in_cap_frames > 0). */ struct da_ack { - uint32_t magic; /* DA_ACK_MAGIC */ - int32_t status; /* 0 = ok, else an AAudio error code (negative) */ - int32_t rate; /* output stream rate (48000) */ - int32_t burst; /* output frames per burst */ - int32_t buf_frames; /* output buffer size as opened */ - int32_t out_cap_frames; /* render ring capacity */ - int32_t in_cap_frames; /* capture ring capacity, 0 = no capture ring */ - int32_t in_rate; /* rate of the capture ring's data */ - uint32_t flags; /* DA_ACK_* */ + uint32_t magic; + int32_t status; + int32_t rate; + int32_t burst; + int32_t buf_frames; + int32_t out_cap_frames; + int32_t in_cap_frames; + int32_t in_rate; + uint32_t flags; uint32_t _pad; }; -/* Subsequent messages, driver -> helper, on the same socket. The helper never - * writes after the ack; everything it has to say is in the ring header. EOF on - * the socket is how the helper learns the game is gone. */ enum da_msg_type { - DA_MSG_MIC_START = 1, /* first capture voice started: make the mic hot */ - DA_MSG_MIC_STOP = 2, /* last capture voice gone: stop (keep open) */ + DA_MSG_MIC_START = 1, + DA_MSG_MIC_STOP = 2, DA_MSG_PING = 3, }; @@ -206,4 +135,4 @@ struct da_msg int32_t arg; }; -#endif /* DA_RELAY_PROTO_H */ +#endif diff --git a/tools/directaudio/sink/module-directaudio-native-sink.c b/tools/directaudio/sink/module-directaudio-native-sink.c index 3b6e5f6c9..3311fe0ed 100644 --- a/tools/directaudio/sink/module-directaudio-native-sink.c +++ b/tools/directaudio/sink/module-directaudio-native-sink.c @@ -1,24 +1,5 @@ /* - * module-directaudio-native-sink: a PulseAudio 13 sink that is DirectAudio's AAudio engine, - * in-process. For the native Linux Steam client on Android (DroidDeck, Bannerlator's Linux - * session), whose sound can only ever reach a PulseAudio daemon: this module is where that - * daemon's mixed output meets Android, one step away from AudioFlinger, with the same engine the - * game driver has - adaptive device buffer that grows a burst on every xrun, optional decay with a - * remembered floor, a primed ring with fades instead of clicks, route-change / error reopen on a - * fresh stream, and a stall watchdog. - * - * It is the relay helper's per-client OUTPUT engine (directaudio-relay.c) with the socket and the - * shared-memory ring replaced by a ring in this process, fed by the sink's IO thread exactly as - * module-directaudio-sink feeds the relay. Compared with that module there is no second process - * and no 40 ms proot safety margin: the ring here only has to cover the IO thread's own - * scheduling jitter, so it starts at two bursts and grows only if a callback finds it short. - * - * The daemon must run on the Android side (the host app's process), not inside proot, for AAudio - * to be reachable at all - which is where both host apps already run it. - * - * Field fixes carried from the relay (MaxsTechReview, Droid-Deck/DroidDeck#338): prime before - * playing, fade on an underrun, one consumer per ring across a reopen, a device buffer of at - * least two bursts. + * Derived from the AAudio engine of DirectAudio's relay helper (directaudio-relay.c) by The412Banner. * * SPDX-License-Identifier: LGPL-2.1-or-later */ @@ -52,7 +33,7 @@ #include #include -#include "da_relay_proto.h" /* struct da_ring + the ring helpers; nothing of the socket protocol */ +#include "da_relay_proto.h" PA_MODULE_AUTHOR("The412Banner"); PA_MODULE_DESCRIPTION("Android audio output: DirectAudio's AAudio engine, in-process"); @@ -77,7 +58,6 @@ PA_MODULE_USAGE( #define WATCHDOG_TICK_USEC (1 * PA_USEC_PER_SEC) #define AUDIO_NICE (-16) -/* ---- tunables: the in-process driver's, same defaults ----------------------- */ #define DA_KNEE 0.75f #define DA_DEFAULT_MS 12 #define DA_DEFAULT_MAX_MS 100 @@ -112,17 +92,14 @@ struct userdata { pa_sample_spec ss; size_t frame_size; - /* configuration */ aaudio_performance_mode_t perf; int32_t target_ms, max_ms, ring_ms; bool adaptive, decay, limiter, watchdog_on; - /* the ring: this process's, same layout as the relay's so the engine is shared */ struct da_ring *ring; int32_t burst; int32_t ring_max_target; - /* the one OUTPUT stream and its adaptive state (the relay's struct client, minus the socket) */ AAudioStream *aq; pthread_mutex_t lock; int32_t last_xrun; @@ -134,7 +111,6 @@ struct userdata { int suspended; volatile int dead; - /* what the IO thread last logged, so engine changes made on the AAudio thread get a line */ int32_t seen_target, seen_hw_buf; uint32_t seen_underruns; pa_usec_t next_watchdog, settled_at, stats_at; @@ -152,8 +128,6 @@ static uint64_t now_ns(void) { static void request_reopen(struct userdata *u, const char *why); -/* ---- the engine: the relay's out_cb / open_output / reopen / watchdog, on a local ring ---- */ - static void fade(float *p, uint32_t frames, int in) { uint32_t i, ch; @@ -165,8 +139,6 @@ static void fade(float *p, uint32_t frames, int in) { } } -/* The driver's soft-knee limiter: below DA_KNEE the signal is untouched, so ordinary material is - * bit-identical; above it a smooth knee folds the overshoot back under full scale. */ static void limit(float *p, uint32_t samples) { uint32_t i; @@ -193,7 +165,6 @@ static aaudio_data_callback_result_t out_cb(AAudioStream *aq, void *user, void * int fade_in = 0, short_read = 0, starving = 0; uint64_t now; - /* Only the promoted stream reads the ring; an outgoing one during a reopen plays silence. */ if ((live && aq != live) || __atomic_exchange_n(&u->consuming, 1, __ATOMIC_ACQUIRE)) { memset(out, 0, (size_t) frames * DA_RING_CHANNELS * sizeof(float)); return AAUDIO_CALLBACK_RESULT_CONTINUE; @@ -248,8 +219,6 @@ static aaudio_data_callback_result_t out_cb(AAudioStream *aq, void *user, void * __atomic_add_fetch(&u->cb_count, 1, __ATOMIC_SEQ_CST); __atomic_store_n(&r->cb_count, u->cb_count, __ATOMIC_RELAXED); - /* The ring's own adaptive step: a short ring means the IO thread did not get scheduled in - * time - an xrun one stage earlier - so keep one more burst queued. Only grows. */ if (short_read) __atomic_add_fetch(&r->underruns, 1, __ATOMIC_RELAXED); if ((short_read || starving) && u->adaptive) { @@ -262,8 +231,6 @@ static aaudio_data_callback_result_t out_cb(AAudioStream *aq, void *user, void * __atomic_store_n(&r->target_frames, want, __ATOMIC_RELEASE); } - /* The device buffer: grow a burst per xrun; decay a burst after a quiet spell, never below - * the floor a punished decay taught us. */ if (u->adaptive) { int32_t xruns = AAudioStream_getXRunCount(aq); int32_t burst = AAudioStream_getFramesPerBurst(aq); @@ -312,8 +279,6 @@ static void out_error_cb(AAudioStream *aq, void *user, aaudio_result_t error) { if (aq != __atomic_load_n(&u->aq, __ATOMIC_SEQ_CST)) return; if (__atomic_load_n(&u->suspended, __ATOMIC_SEQ_CST)) return; - /* A disconnected stream (headphones plugged or unplugged, a Bluetooth route coming or going) - * can never be restarted: it has to be closed and a new one opened on the current route. */ if (error != AAUDIO_ERROR_DISCONNECTED && error != AAUDIO_ERROR_INVALID_STATE && error != AAUDIO_ERROR_INVALID_HANDLE && @@ -322,8 +287,6 @@ static void out_error_cb(AAudioStream *aq, void *user, aaudio_result_t error) { request_reopen(u, "stream error"); } -/* 48 kHz / float / stereo, buffer from the ms form rounded up to a burst and never below two, - * capacity from the ceiling. The ring target floor is two bursts or ring_ms, whichever is more. */ static aaudio_result_t open_output(struct userdata *u, AAudioStream **out) { AAudioStreamBuilder *builder = NULL; AAudioStream *aq = NULL; @@ -446,7 +409,6 @@ static void request_reopen(struct userdata *u, const char *why) { } } -/* Ticked once a second from the IO thread: a callback silent for a second is a dead stream. */ static void watchdog(struct userdata *u) { uint64_t now = now_ns(), last_cb, prev_tick; unsigned int cb; @@ -484,13 +446,10 @@ static void close_output(struct userdata *u) { AAudioStream_requestStop(aq); AAudioStream_close(aq); } - /* A reopen still in flight sees u->dead and stops; give it a moment to let go of u. */ while (__atomic_load_n(&u->reopen_running, __ATOMIC_SEQ_CST)) usleep(10000); } -/* ---- the PulseAudio side: module-directaudio-sink's producer, on the local ring ---- */ - static pa_usec_t frames_to_usec(const struct userdata *u, int64_t frames) { return (pa_usec_t) (frames * (int64_t) PA_USEC_PER_SEC / (int64_t) u->ss.rate); } @@ -528,7 +487,6 @@ static void produce(struct userdata *u, uint32_t frames) { __atomic_store_n(&r->widx, w + frames, __ATOMIC_RELEASE); } -/* Tops the ring up to the engine's target and narrates what the engine changed. */ static void service_ring(struct userdata *u) { struct da_ring *r = u->ring; uint32_t avail, space; @@ -587,8 +545,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); } -/* A started AAudio stream keeps audioserver's AudioMix wake lock held, so the device never - * suspends while the session sleeps: stop it, and the IO thread's timer, until the sink reopens. */ 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; aaudio_result_t res = AAUDIO_OK; @@ -629,9 +585,6 @@ static void thread_func(void *userdata) { for (;;) { int ret; - /* Half a burst between visits, never blocking on the stream: clients' audio reaches this - * thread as messages pa_rtpoll_run handles, and a thread stuck in a write or a wait lets - * them run dry (the crackle #338 fixed). */ if (!u->suspended) { service_ring(u); pa_rtpoll_set_timer_relative(u->rtpoll, PA_MAX(frames_to_usec(u, u->burst) / 2, PA_USEC_PER_MSEC)); @@ -719,7 +672,6 @@ int pa__init(pa_module *m) { pthread_mutex_init(&u->lock, NULL); pa_channel_map_init_stereo(&map); - /* The ring: the relay's layout in this process's memory. */ cap_frames = DA_RING_CAP_MS_LOCAL * DA_RING_RATE / 1000; u->ring = pa_xmalloc0(da_ring_bytes(cap_frames)); u->ring->magic = DA_RING_MAGIC; @@ -774,8 +726,6 @@ int pa__init(pa_module *m) { pa_sink_set_asyncmsgq(u->sink, u->thread_mq.inq); pa_sink_set_rtpoll(u->sink, u->rtpoll); pa_sink_set_max_request(u->sink, (size_t) u->ring->target_frames * u->frame_size); - /* What clients should keep queued on their side: the ring's starting target plus the device - * buffer. Grows are reported live through GET_LATENCY. */ pa_sink_set_fixed_latency(u->sink, frames_to_usec(u, (int64_t) u->ring->target_frames + u->base_buf_frames)); if (!(u->thread = pa_thread_new("directaudio-native-sink", thread_func, u))) { diff --git a/tools/dirt-ws2/build.sh b/tools/dirt-ws2/build.sh index 3abe6d16c..67a1b3be0 100755 --- a/tools/dirt-ws2/build.sh +++ b/tools/dirt-ws2/build.sh @@ -1,5 +1,4 @@ #!/usr/bin/env bash -# Builds the 32-bit ws2_32.dll stand-in for DiRT 3 and DiRT Showdown into the given folder. set -euo pipefail here=$(cd "$(dirname "$0")" && pwd) mkdir -p "$1" diff --git a/tools/dirt-ws2/gen.py b/tools/dirt-ws2/gen.py index 3d2532b0e..02743506f 100644 --- a/tools/dirt-ws2/gen.py +++ b/tools/dirt-ws2/gen.py @@ -1,8 +1,3 @@ -"""ws2_32 stand-in helpers. - -gen.py generate SRC OUT writes OUT/thunks.c (one jump per export) and OUT/ws2_32.def from SRC/ws2_32-exports.txt. -gen.py verify SRC DLL checks that DLL exports exactly the ordinals and names in SRC/ws2_32-exports.txt. -""" import struct import sys from pathlib import Path diff --git a/tools/dirt-ws2/ws2_32.c b/tools/dirt-ws2/ws2_32.c index de18fe912..fcad74b85 100644 --- a/tools/dirt-ws2/ws2_32.c +++ b/tools/dirt-ws2/ws2_32.c @@ -1,9 +1,4 @@ -/* ws2_32.dll stand-in for DiRT 3 and DiRT Showdown (32-bit). - * - * Both games close their motion telemetry UDP socket at the end of a race (and when leaving Joyride) and keep - * calling sendto() on it; the WSAENOTSOCK (10038) failure stops the game. Every export here jumps to the - * same-ordinal export of Wine's own ws2_32.dll, except that a sendto() failing with 10038 reports 0 bytes sent. - * Same behaviour as DiRTTelemetryErrorFix by RoccoC (MIT), based on the fix by Carsten Gottschling. */ +/* Same behaviour as DiRTTelemetryErrorFix by RoccoC (MIT), based on the fix by Carsten Gottschling. */ typedef unsigned short WCHAR; typedef unsigned long DWORD; @@ -21,7 +16,6 @@ __attribute__((dllimport)) UINT WINAPI GetSystemDirectoryW(WCHAR *buffer, UINT s __attribute__((dllimport)) DWORD WINAPI GetEnvironmentVariableW(const WCHAR *name, WCHAR *buffer, DWORD size); __attribute__((dllimport)) DWORD WINAPI GetFileAttributesW(const WCHAR *name); -/* droiddeck-game-env recognises a swapped-in copy by this text. */ __attribute__((used)) const char dirtfix_marker[] = "droiddeck-dirt-ws2_32"; extern const unsigned short dirtfix_ordinals[]; @@ -59,8 +53,6 @@ static void append_number(WCHAR *dst, UINT n) append(dst, digits + i); } -/* When this file replaces the prefix's syswow64\ws2_32.dll, Wine's own copy is the i386-windows\ws2_32.dll in one - * of the builtin DLL folders Wine lists in WINEDLLDIR0, WINEDLLDIR1, ... as NT paths (\??\Z:\...). */ static HMODULE load_wine_ws2_32(HMODULE self) { WCHAR name[24], path[600]; diff --git a/tools/docker_platform.sh b/tools/docker_platform.sh index e7f34e561..4d4744875 100644 --- a/tools/docker_platform.sh +++ b/tools/docker_platform.sh @@ -1,4 +1,3 @@ -# Sourced by the build scripts: picks the Docker platform and the matching cross-build image. docker_platform=${DROIDDECK_DOCKER_PLATFORM:-} if [[ -z "${docker_platform}" ]]; then case "$(uname -m)" in arm64|aarch64) docker_platform=linux/arm64 ;; *) docker_platform=linux/amd64 ;; esac diff --git a/tools/docker_preflight.py b/tools/docker_preflight.py index eddd6206a..65322fd61 100644 --- a/tools/docker_preflight.py +++ b/tools/docker_preflight.py @@ -1,4 +1,3 @@ -"""Check the engine before modifying build assets; never rebuild for a daemon error.""" import os from pathlib import Path import subprocess @@ -18,7 +17,7 @@ if __name__ == '__main__': sys.exit('Docker is unavailable: ' + engine.stderr.strip()) image = run('image', 'inspect', sys.argv[1]) if image.returncode: - if 'No such image' not in image.stderr and 'image not known' not in image.stderr: # Docker, Podman + if 'No such image' not in image.stderr and 'image not known' not in image.stderr: sys.exit('Cannot inspect the build image: ' + image.stderr.strip()) repo = Path(__file__).resolve().parents[1] subprocess.run(['docker', 'build', '--platform', os.environ.get('DROIDDECK_DOCKER_PLATFORM', 'linux/amd64'), '-t', sys.argv[1], diff --git a/tools/droiddeck-scenario b/tools/droiddeck-scenario index b4590b02f..8b238ef80 100755 --- a/tools/droiddeck-scenario +++ b/tools/droiddeck-scenario @@ -146,7 +146,6 @@ class Runner: log.append(entry) def evidence(self, folder: Path) -> dict: - """What a failed run leaves behind: the screen, gamescope's focus, the session's logs.""" found = {} for name, action in ( ("screenshot", lambda: ctl.screenshot(self.adb, self.serial, str(folder / "failure.png"), None)), diff --git a/tools/droiddeckctl b/tools/droiddeckctl index b3a6c2d5b..95ebc5dbe 100755 --- a/tools/droiddeckctl +++ b/tools/droiddeckctl @@ -24,7 +24,6 @@ from adb_device import DeviceResolutionError, resolve_serial from proot_profile import report as proot_profile_report -# tools/release/variants.txt: the same app also ships as other packages (e.g. com.antutu.benchmark.full). PACKAGE = os.environ.get("DROIDDECK_PACKAGE", "com.droiddeck.launcher") AUTHORITY = f"content://{PACKAGE}.agent" PACKAGE_NAME = re.compile(r"[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+") @@ -115,7 +114,6 @@ def provider_call(adb: str, serial: str, method: str, arg: str | None = None, re def checked(response: dict, default_code: str) -> dict: - """A provider or guest answer, raised as a ControlError when it carries ok: false.""" if response.get("ok") is False: error = response.get("error", {}) raise ControlError(error.get("message", default_code), error.get("code", default_code)) @@ -166,7 +164,6 @@ def get_state(adb: str, serial: str) -> dict: def wait_until(adb: str, serial: str, describe: str, done, timeout: float, poll: float = 1.0) -> dict: - """Poll state until done(state) is true. A failed session ends the wait at once.""" deadline = time.monotonic() + timeout while True: state = get_state(adb, serial) @@ -203,8 +200,6 @@ def wait_for_phase(adb: str, serial: str, wanted: str, timeout: float) -> dict: def focused(state: dict, app_id: int | None) -> bool: - """The game has focus the way a player sees it: gamescope focuses it and it is in the list the - Steam client orders the base layer from. app_id None means any game (not the client).""" focus = state.get("session", {}).get("focus") or {} current = focus.get("focusedApp") if app_id is None: @@ -232,7 +227,6 @@ def add_serial(response: dict, serial: str) -> dict: def displays(adb: str, serial: str) -> list[dict]: - """Logical displays with the SurfaceFlinger (physical) id screencap and screenrecord want.""" result = run_adb(adb, serial, "shell", "dumpsys", "display", timeout=30) text = result.stdout.decode(errors="replace") devices = dict(re.findall(r'DisplayDeviceInfo\{"([^"]+)": uniqueId="local:(\d+)"', text)) @@ -300,14 +294,6 @@ def record(adb: str, serial: str, path: str, seconds: float, display: int | None def pacing_summary(text: str, package: str = PACKAGE) -> dict: - """What reached the panel, from a `dumpsys SurfaceFlinger --timestats -dump`. - - The game is on the compositor's own layer (droiddeck_wayland_game) when it has one, otherwise on - the session's SurfaceView. SurfaceFlinger keeps one record per layer AND refresh rate, so a panel - that switches between 60 and 120 Hz mid-capture splits the same layer into several records: they - are summed. A game's own fps counter does not show any of this - it counts frames handed over, not - frames shown, and 60 handed over can be 48 on screen with one in four held for two refreshes. - """ records = {"game": [], "surface": []} for part in re.split(r"^displayRefreshRate = ", text, flags=re.M)[1:]: rate = re.match(r"(\d+)", part) @@ -329,7 +315,6 @@ def pacing_summary(text: str, package: str = PACKAGE) -> dict: for ms, count in bins.items(): histogram[ms] = histogram.get(ms, 0) + count if bins: - # SurfaceFlinger bins by whole milliseconds, so one cadence spans two buckets (16.7 ms: 16 and 17). mode = max(bins, key=bins.get) off += sum(count for ms, count in bins.items() if abs(ms - mode) > 1) intervals = sum(histogram.values()) @@ -346,7 +331,6 @@ def pacing_summary(text: str, package: str = PACKAGE) -> dict: def pacing(adb: str, serial: str, seconds: float) -> dict: - """Frames on screen and their intervals over `seconds`, from SurfaceFlinger's timestats.""" seconds = max(1.0, min(600.0, seconds)) timestats = ("shell", "dumpsys", "SurfaceFlinger", "--timestats") try: @@ -487,7 +471,6 @@ def xr_capture(adb: str, serial: str, destination: Path, timeout: float) -> dict except (OSError, EOFError, zlib.error) as error: raise ControlError('Incomplete compressed XR image', 'XR_CAPTURE_FAILED') from error images[eye] = xr_ppm_to_png(pixels, linear=eye == 'source' and metadata.get('sourceColorSpace') == 'linear') - # Detect overlapping captures before publishing images with another request's metadata. check = run_adb(adb, serial, 'shell', 'cat', remote + '.json') try: unchanged = json.loads(check.stdout).get('id') == token @@ -961,7 +944,7 @@ def main(argv: list[str] | None = None) -> int: args = build_parser().parse_args(argv) serial = None try: - if not PACKAGE_NAME.fullmatch(PACKAGE): # it reaches the device shell through `am start` and `content call` + if not PACKAGE_NAME.fullmatch(PACKAGE): raise ControlError(f"DROIDDECK_PACKAGE is not an Android package name: {PACKAGE!r}", "INVALID_REQUEST") if args.command == "proot-profile" and args.profile_op == "report": if args.limit < 1: diff --git a/tools/fex/build-in-arch.sh b/tools/fex/build-in-arch.sh index ce5f68ceb..66d2c2a4e 100755 --- a/tools/fex/build-in-arch.sh +++ b/tools/fex/build-in-arch.sh @@ -1,6 +1,4 @@ #!/usr/bin/bash -# Runs inside an Arch Linux ARM container (menci/archlinuxarm:base-devel) on an arm64 runner. Arch Linux ARM ships -# no FEX package and FEX publishes no Linux binaries. set -euxo pipefail VERSION=${FEX_VERSION:-FEX-2610} WORK=/work @@ -10,7 +8,6 @@ grep -q '^DisableSandbox' /etc/pacman.conf || sed -i 's/^\[options\]/[options]\n mv /etc/pacman.d/mirrorlist.new /etc/pacman.d/mirrorlist pacman -Syu --noconfirm --needed git zstd binutils cmake ninja clang lld llvm python squashfs-tools file patchelf xxhash -# The x86 sysroot is Arch x86_64 and multilib i686 packages unpacked by pacman, never run: no scriptlets or signature checks. THUNKS=${FEX_THUNKS:-0} X86ROOT=$WORK/x86root if [ "$THUNKS" = 1 ]; then @@ -40,15 +37,12 @@ fi rm -rf fex-src out && git clone -q --depth 1 --branch "$VERSION" --recurse-submodules --shallow-submodules https://github.com/FEX-Emu/FEX.git fex-src for p in tools/fex/patches/*.patch; do patch -d fex-src -p1 --no-backup-if-mismatch < "$p"; done -# FEX's x86 toolchain files assume an x86 build machine; here the guest thunks need the x86 sysroot too. -# Arch's clang adds stack protection; its canary load in a guest thunk's prologue clobbered r11 (the host function). if [ "$THUNKS" = 1 ]; then for t in fex-src/Data/CMake/toolchain_x86_64.cmake fex-src/Data/CMake/toolchain_x86_32.cmake; do printf '\nif (X86_DEV_ROOTFS AND NOT X86_DEV_ROOTFS STREQUAL "/")\n set(CMAKE_SYSROOT "${X86_DEV_ROOTFS}")\nendif()\n' >> "$t" printf '\nset(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-stack-protector")\nset(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-stack-protector")\n' >> "$t" done fi -# TUNE_CPU none: the default (native) would tune for the runner's Neoverse cores, not the device. cmake -S fex-src -B fex-build -G Ninja \ -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local \ -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DUSE_LINKER=lld \ @@ -60,8 +54,6 @@ cmake -S fex-src -B fex-build -G Ninja \ ninja -C fex-build DESTDIR="$WORK/out" ninja -C fex-build install install -Dm755 /usr/bin/unsquashfs out/usr/local/bin/unsquashfs -# The Linux runtime has no xxhash and no ld.so.cache: the library goes beside libFEXCore and the programs -# that need it find it through RUNPATH. install -Dm644 "$(readlink -f /usr/lib/libxxhash.so.0)" out/usr/local/lib/libxxhash.so.0 bundled=$(cd out/usr/local/lib && ls -- *.so* | sort -u) for f in out/usr/local/bin/*; do @@ -70,7 +62,6 @@ for f in out/usr/local/bin/*; do readelf -d "$f" | awk '/NEEDED/ { gsub(/[\[\]]/, "", $5); print $5 }' | grep -qxF "$bundled" \ && patchelf --set-rpath '$ORIGIN/../lib' "$f" done -# Every NEEDED soname must resolve in a stock guest: bundled here or provided by the runtime (runtime-libs.txt). for f in out/usr/local/bin/* out/usr/local/lib/*.so* out/usr/local/lib/fex-emu/HostThunks*/*.so; do [ -f "$f" ] && file "$f" | grep -q ELF || continue readelf -d "$f" | awk '/NEEDED/ { gsub(/[\[\]]/, "", $5); print $5 }' diff --git a/tools/fex/patches/0001-openat2-enosys-fallback.patch b/tools/fex/patches/0001-openat2-enosys-fallback.patch index d7914133e..af0af4fa1 100644 --- a/tools/fex/patches/0001-openat2-enosys-fallback.patch +++ b/tools/fex/patches/0001-openat2-enosys-fallback.patch @@ -10,7 +10,7 @@ index 7172460..2c80dca 100644 #include #include #include -@@ -632,6 +633,163 @@ static bool ShouldSkipOpenInEmu(int flags) { +@@ -632,6 +633,157 @@ static bool ShouldSkipOpenInEmu(int flags) { return false; } @@ -26,19 +26,15 @@ index 7172460..2c80dca 100644 + return Result; +} + -+// openat2() with RESOLVE_IN_ROOT, walked one component at a time: PRoot has no openat2 and fails it with ENOSYS. -+// DirFD is AT_FDCWD only for thunk overlays, which never resolve in the root. +static int OpenatWithoutOpenat2(int DirFD, const char* Pathname, int Flags, uint32_t Mode) { + if (DirFD == AT_FDCWD) { + return ::syscall(SYSCALL_DEF(openat), DirFD, Pathname, Flags, Mode); + } + -+ // The kernel's MAXSYMLINKS. + constexpr uint32_t MaxSymlinks = 40; + uint32_t Symlinks {}; + std::optional RootDev {}; + -+ // O_NOFOLLOW and O_CREAT | O_EXCL leave a final symlink alone, unless a trailing slash follows it. + const bool NoFollow = (Flags & O_NOFOLLOW) || (Flags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL); + + char Resolved[PATH_MAX]; @@ -116,7 +112,6 @@ index 7172460..2c80dca 100644 + } + + if (Buffer.st_dev != *RootDev && IsOnProcFS(DirFD, Resolved)) { -+ // Only procfs has magic links. RESOLVE_IN_ROOT fails them with EXDEV and the callers then open without it. + return ::syscall(SYSCALL_DEF(openat), DirFD, Pathname, Flags, Mode); + } + @@ -145,7 +140,6 @@ index 7172460..2c80dca 100644 + } + + if (Last) { -+ // openat reports on the final component, which O_CREAT may create. + ResolvedLen = CandidateLen; + if (TrailingSlash) { + Resolved[ResolvedLen++] = '/'; @@ -174,7 +168,7 @@ index 7172460..2c80dca 100644 bool FileManager::ReplaceEmuFd(int fd, int flags, uint32_t mode) { char Tmp[PATH_MAX + 1]; -@@ -678,6 +836,8 @@ uint64_t FileManager::Open(const char* pathname, int flags, uint32_t mode) { +@@ -678,6 +830,8 @@ uint64_t FileManager::Open(const char* pathname, int flags, uint32_t mode) { // This means a magic symlink (/proc/foo) was involved. In this case we // just punt and do the access without RESOLVE_IN_ROOT. fd = ::syscall(SYSCALL_DEF(openat), Path.FD, Path.Path, flags, mode); @@ -183,7 +177,7 @@ index 7172460..2c80dca 100644 } } -@@ -931,6 +1091,8 @@ uint64_t FileManager::Openat([[maybe_unused]] int dirfs, const char* pathname, i +@@ -931,6 +1085,8 @@ uint64_t FileManager::Openat([[maybe_unused]] int dirfs, const char* pathname, i // This means a magic symlink (/proc/foo) was involved. In this case we // just punt and do the access without RESOLVE_IN_ROOT. fd = ::syscall(SYSCALL_DEF(openat), Path.FD, Path.Path, flags, mode); @@ -192,7 +186,7 @@ index 7172460..2c80dca 100644 } } -@@ -967,6 +1129,8 @@ uint64_t FileManager::Openat2(int dirfs, const char* pathname, FEX::HLE::open_ho +@@ -967,6 +1123,8 @@ uint64_t FileManager::Openat2(int dirfs, const char* pathname, FEX::HLE::open_ho // This means a magic symlink (/proc/foo) was involved. In this case we // just punt and do the access without RESOLVE_IN_ROOT. fd = ::syscall(SYSCALL_DEF(openat2), Path.FD, Path.Path, how, usize); diff --git a/tools/fex/patches/0003-allocator32-map32bit-exhaustion.patch b/tools/fex/patches/0003-allocator32-map32bit-exhaustion.patch index 923d68789..2d9f0a172 100644 --- a/tools/fex/patches/0003-allocator32-map32bit-exhaustion.patch +++ b/tools/fex/patches/0003-allocator32-map32bit-exhaustion.patch @@ -26,11 +26,10 @@ index 60b0ef1..4a94009 100644 using FindHandler = uint64_t (MemAllocator32Bit::*)(uint64_t Start, size_t Pages) const; FindHandler FindPageRangePtr {}; }; -@@ -132,6 +135,37 @@ uint64_t MemAllocator32Bit::FindPageRange_TopDown(uint64_t Start, size_t Pages) +@@ -132,6 +135,36 @@ uint64_t MemAllocator32Bit::FindPageRange_TopDown(uint64_t Start, size_t Pages) return 0; } -+// Marks each run of host mappings that overlaps the range as used, in full. Returns false if none overlaps. +bool MemAllocator32Bit::SetHostMappedPages(uint64_t PageAddr, size_t PagesLength) { + const int MapsFD = ::open("/proc/self/maps", O_RDONLY | O_CLOEXEC); + if (MapsFD == -1) { @@ -64,7 +63,7 @@ index 60b0ef1..4a94009 100644 void* MemAllocator32Bit::Mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset) { std::scoped_lock lk {AllocMutex}; size_t PagesLength = FEXCore::AlignUp(length, FEXCore::Utils::FEX_PAGE_SIZE) >> FEXCore::Utils::FEX_PAGE_SHIFT; -@@ -175,11 +209,17 @@ void* MemAllocator32Bit::Mmap(void* addr, size_t length, int prot, int flags, in +@@ -175,11 +208,16 @@ void* MemAllocator32Bit::Mmap(void* addr, size_t length, int prot, int flags, in auto AllocateNoHint = [&]() -> void* { bool Wrapped = false; @@ -74,7 +73,6 @@ index 60b0ef1..4a94009 100644 // Linear range scan uint64_t LowerPage = (this->*FindPageRangePtr)(BottomPage, PagesLength); if (LowerPage == 0) { -+ // A second scan from the start found nothing either: the range is full, as the kernel would say. + if (Restarted) { + return reinterpret_cast(-ENOMEM); + } @@ -82,15 +80,12 @@ index 60b0ef1..4a94009 100644 // Try again but this time from the start BottomPage = Map32Bit ? LastKeyLocation32Bit : LastKeyLocation; LowerPage = (this->*FindPageRangePtr)(BottomPage, PagesLength); -@@ -206,7 +246,17 @@ restart: { +@@ -206,7 +244,14 @@ restart: { return (void*)(uintptr_t)-errno; } else { // Try again - if (SearchDown) { + if (Map32Bit) { -+ // MAP_32BIT from a 64-bit guest, whose other mappings bypass this allocator: once Counter-Strike 2 had reserved the -+ // low 2GB, stepping one page at a time scanned for ever. Record what the host holds there and rescan the range, or -+ // move past it if that mapping is gone. + const bool Marked = SetHostMappedPages(LowerPage, PagesLength); + if (SearchDown) { + BottomPage = Marked ? UpperPage - 1 : LowerPage - 1; diff --git a/tools/gamescope/build-in-arch.sh b/tools/gamescope/build-in-arch.sh index ba3b1605d..dbf323136 100755 --- a/tools/gamescope/build-in-arch.sh +++ b/tools/gamescope/build-in-arch.sh @@ -49,7 +49,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 -# The WSI layer carries patches too (0117); the runtime's stock copy is replaced. mkdir -p out/usr/lib tar --use-compress-program=unzstd -xf "$PKG" -C out usr/lib/libVkLayer_FROG_gamescope_wsi_aarch64.so strip --strip-unneeded out/usr/lib/libVkLayer_FROG_gamescope_wsi_aarch64.so || true diff --git a/tools/gamescope/patches/0116-release-cursor-shm-buffers.patch b/tools/gamescope/patches/0116-release-cursor-shm-buffers.patch index 03b4d7fd0..a68ac9133 100644 --- a/tools/gamescope/patches/0116-release-cursor-shm-buffers.patch +++ b/tools/gamescope/patches/0116-release-cursor-shm-buffers.patch @@ -1,12 +1,9 @@ --- a/src/wlserver.cpp +++ b/src/wlserver.cpp -@@ -203,6 +203,12 @@ +@@ -203,6 +203,9 @@ } void xwayland_surface_commit(struct wlr_surface *wlr_surface) { -+ // Cursor surfaces never acquire an X11/xdg window association. Keeping their -+ // commits in g_PendingCommits pins every old SHM buffer until the cursor -+ // surface is destroyed. X11 cursor images are handled by MouseCursor. + if ( wlr_surface->role && strcmp( wlr_surface->role->name, "wl_pointer-cursor" ) == 0 ) + return; + diff --git a/tools/gamescope/patches/0117-wsi-cache-present-bypass-check.patch b/tools/gamescope/patches/0117-wsi-cache-present-bypass-check.patch index 7ce0ec48d..9ee156d8a 100644 --- a/tools/gamescope/patches/0117-wsi-cache-present-bypass-check.patch +++ b/tools/gamescope/patches/0117-wsi-cache-present-bypass-check.patch @@ -9,12 +9,10 @@ #include // For limiter file. #include -@@ -476,6 +478,34 @@ +@@ -476,6 +478,32 @@ // Cached for comparison. std::optional cachedWindowRect; -+ // canBypassXWayland costs several X round trips; at present time it is refreshed at most -+ // every DROIDDECK_WSI_BYPASS_CHECK_MS (default 0 = every present; DroidDeck's Performance mode sets 100). + std::optional presentBypass; + bool presentBypassHdr = false; + uint64_t presentBypassNs = 0; @@ -44,7 +42,7 @@ bool isWayland() const { // Is native Wayland? return connection == nullptr; -@@ -1597,7 +1627,7 @@ +@@ -1597,7 +1625,7 @@ continue; } diff --git a/tools/gamescope/patches/0119-wsi-default-images-under-limiter.patch b/tools/gamescope/patches/0119-wsi-default-images-under-limiter.patch index 78f2837ca..c55ae8643 100644 --- a/tools/gamescope/patches/0119-wsi-default-images-under-limiter.patch +++ b/tools/gamescope/patches/0119-wsi-default-images-under-limiter.patch @@ -2,13 +2,10 @@ diff --git a/layer/VkLayer_FROG_gamescope_wsi.cpp b/layer/VkLayer_FROG_gamescope index ea4ba84..46d086c 100644 --- a/layer/VkLayer_FROG_gamescope_wsi.cpp +++ b/layer/VkLayer_FROG_gamescope_wsi.cpp -@@ -201,6 +201,13 @@ namespace GamescopeWSILayer { +@@ -201,6 +201,10 @@ namespace GamescopeWSILayer { return s_minImageCount; } -+ // The raised count keeps an uncapped game drawing while gamescope turns its buffers around. -+ // Under the frame limiter's FIFO the extra images only queue frames ahead of the screen, -+ // so a limited swapchain gets the default count. + static uint32_t getMinImageCount(bool forcingFifo) { + return forcingFifo ? std::min(getMinImageCount(), 3u) : getMinImageCount(); + } @@ -16,7 +13,7 @@ index ea4ba84..46d086c 100644 static bool getEnsureMinImageCount() { static bool s_ensureMinImageCount = []() -> bool { if (auto ensure = parseEnv("GAMESCOPE_WSI_ENSURE_MIN_IMAGE_COUNT")) { -@@ -984,7 +991,7 @@ namespace GamescopeWSILayer { +@@ -984,7 +988,7 @@ namespace GamescopeWSILayer { pSurfaceCapabilities->currentExtent = rect->extent; } @@ -25,7 +22,7 @@ index ea4ba84..46d086c 100644 return VK_SUCCESS; } -@@ -1027,7 +1034,7 @@ namespace GamescopeWSILayer { +@@ -1027,7 +1031,7 @@ namespace GamescopeWSILayer { pSurfaceCapabilities->surfaceCapabilities.currentExtent = rect->extent; } @@ -34,7 +31,7 @@ index ea4ba84..46d086c 100644 return VK_SUCCESS; } -@@ -1305,7 +1312,7 @@ namespace GamescopeWSILayer { +@@ -1305,7 +1309,7 @@ namespace GamescopeWSILayer { uint32_t minImageCount = swapchainInfo.minImageCount; if (getEnsureMinImageCount()) @@ -43,12 +40,10 @@ index ea4ba84..46d086c 100644 swapchainInfo.minImageCount = minImageCount; fprintf(stderr, "[Gamescope WSI] Creating swapchain for xid: 0x%0x - oldSwapchain: %p - provided minImageCount: %u - minImageCount: %u - format: %s - colorspace: %s - flip: %s\n", -@@ -1627,6 +1634,13 @@ namespace GamescopeWSILayer { +@@ -1627,6 +1631,11 @@ namespace GamescopeWSILayer { continue; } -+ // Other apps keep their swapchain when the limiter changes; ask them to recreate it so -+ // the image count follows the limiter as well. + if (!frameLimiterAware && gamescopeSwapchain->forceFifo != forceFifo && + getMinImageCount(true) != getMinImageCount(false) && + !(gamescopeSurface->flags & GamescopeLayerClient::Flag::NoSuboptimal)) diff --git a/tools/gamescope/patches/0120-nested-refresh-follows-host.patch b/tools/gamescope/patches/0120-nested-refresh-follows-host.patch index 83ed73b74..a2b846440 100644 --- a/tools/gamescope/patches/0120-nested-refresh-follows-host.patch +++ b/tools/gamescope/patches/0120-nested-refresh-follows-host.patch @@ -2,13 +2,10 @@ diff --git a/src/Backends/WaylandBackend.cpp b/src/Backends/WaylandBackend.cpp index 40164ba..1b85e63 100644 --- a/src/Backends/WaylandBackend.cpp +++ b/src/Backends/WaylandBackend.cpp -@@ -1698,6 +1698,52 @@ namespace gamescope +@@ -1698,6 +1698,46 @@ namespace gamescope } } -+ // -r sets the refresh gamescope starts with. With GAMESCOPE_NESTED_REFRESH_FOLLOWS_HOST=1 a refresh -+ // the host compositor reports later replaces it, so pacing and the frame limiter follow a panel -+ // switched to another mode during the session. + static bool NestedRefreshFollowsHost() + { + static const bool s_bFollow = [] @@ -19,9 +16,6 @@ index 40164ba..1b85e63 100644 + return s_bFollow; + } + -+ // A mode switch on the host can report a passing rate for a few frames (72 Hz between 144 and -+ // 120 on an AYANEO Pocket FIT); a new rate is taken once it has been reported for 300 ms. -+ // Until then gamescope keeps presenting, so a still screen also gets the feedback that takes it. + static bool HostRefreshChanged( int32_t nRefreshmHz, uint64_t ulPresentedNs ) + { + static int32_t s_nPendingmHz = 0; @@ -55,7 +49,7 @@ index 40164ba..1b85e63 100644 void CWaylandPlane::UpdateVRRRefreshRate() { if ( m_pParent ) -@@ -1724,6 +1770,7 @@ namespace gamescope +@@ -1724,6 +1764,7 @@ namespace gamescope // TODO(strategy): We should pick the largest refresh rate. xdg_log.infof( "Changed refresh to: %.3fhz", ConvertmHzToHz( (float) nLargestRefreshRateMhz ) ); g_nOutputRefresh = nLargestRefreshRateMhz; @@ -63,7 +57,7 @@ index 40164ba..1b85e63 100644 } } -@@ -1794,10 +1841,11 @@ namespace gamescope +@@ -1794,10 +1835,11 @@ namespace gamescope if ( uRefreshCycle ) { int32_t nRefresh = RefreshCycleTomHz( uRefreshCycle ); @@ -76,11 +70,10 @@ index 40164ba..1b85e63 100644 } m_pConnector->SetHostCompositorIsCurrentlyVRR( false ); -@@ -2636,6 +2684,9 @@ namespace gamescope +@@ -2636,6 +2678,8 @@ namespace gamescope } void CWaylandBackend::Wayland_Output_Mode( wl_output *pOutput, uint32_t uFlags, int32_t nWidth, int32_t nHeight, int32_t nRefresh ) { -+ // The followed rate comes from presentation feedback, so a still screen presents once for it. + if ( NestedRefreshFollowsHost() && m_pOutputs[ pOutput ].nRefresh != nRefresh ) + force_repaint(); m_pOutputs[ pOutput ].nRefresh = nRefresh; @@ -90,12 +83,11 @@ diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp index 6c9299d..5fb9086 100644 --- a/src/steamcompmgr.cpp +++ b/src/steamcompmgr.cpp -@@ -9254,6 +9254,15 @@ void update_vrr_atoms(xwayland_ctx_t *root_ctx, bool force, bool* needs_flush = +@@ -9254,6 +9254,14 @@ void update_vrr_atoms(xwayland_ctx_t *root_ctx, bool force, bool* needs_flush = g_nCurrentRefreshRate_CachedValue = g_nOutputRefresh; if (needs_flush) *needs_flush = true; + -+ // Steam reads the display's refresh rates from gamescope_control when it binds; send them again. + if ( !force ) + { + wlserver_lock(); diff --git a/tools/gamescope/patches/0121-override-content-skip-toplevel-walk.patch b/tools/gamescope/patches/0121-override-content-skip-toplevel-walk.patch index 016ae5af3..a7624b402 100644 --- a/tools/gamescope/patches/0121-override-content-skip-toplevel-walk.patch +++ b/tools/gamescope/patches/0121-override-content-skip-toplevel-walk.patch @@ -11,12 +11,10 @@ index 9a027a2..e6d8e31 100644 }; std::mutex g_wlserver_xdg_shell_windows_lock; -@@ -800,6 +802,20 @@ static void gamescope_swapchain_destroy_co( struct wl_resource *resource ); +@@ -800,6 +802,18 @@ static void gamescope_swapchain_destroy_co( struct wl_resource *resource ); void gamescope_xwayland_server_t::handle_override_window_content( struct wl_client *client, struct wl_resource *gamescope_swapchain_resource, struct wlr_surface *surface, uint32_t x11_window ) { -+ // The WSI layer sends this on every present. Finding the top level walks the X tree with a -+ // round trip per level, so a swapchain repeating the same window skips it for 100 ms. + const uint64_t ulNow = get_time_in_nanos(); + if ( gamescope_swapchain_resource ) + { @@ -32,7 +30,7 @@ index 9a027a2..e6d8e31 100644 x11_window = x11_find_toplevel_for_xid( this->ctx->dpy, x11_window ); wlserver_x11_surface_info *x11_surface = lookup_x11_surface_info_from_xid( this, x11_window ); -@@ -813,7 +829,11 @@ void gamescope_xwayland_server_t::handle_override_window_content( struct wl_clie +@@ -813,7 +827,11 @@ void gamescope_xwayland_server_t::handle_override_window_content( struct wl_clie if ( content_overrides.count( x11_window ) ) { if ( content_overrides[x11_window]->gamescope_swapchain == gamescope_swapchain_resource ) @@ -44,7 +42,7 @@ index 9a027a2..e6d8e31 100644 #ifdef GAMESCOPE_SWAPCHAIN_DEBUG wl_log.infof( "handle_override_window_content: (2) DESTROYING x11_window: 0x%x old_swapchain: %p new_swapchain: %p", x11_window, content_overrides[x11_window]->gamescope_swapchain, gamescope_swapchain_resource ); #endif -@@ -830,6 +850,8 @@ void gamescope_xwayland_server_t::handle_override_window_content( struct wl_clie +@@ -830,6 +848,8 @@ void gamescope_xwayland_server_t::handle_override_window_content( struct wl_clie co->surface = surface; co->x11_window = x11_window; co->gamescope_swapchain = gamescope_swapchain_resource; diff --git a/tools/gamescope/patches/0122-wayland-wake-after-host-events.patch b/tools/gamescope/patches/0122-wayland-wake-after-host-events.patch index 0be39bf16..b33cef1b6 100644 --- a/tools/gamescope/patches/0122-wayland-wake-after-host-events.patch +++ b/tools/gamescope/patches/0122-wayland-wake-after-host-events.patch @@ -2,12 +2,11 @@ diff --git a/src/Backends/WaylandBackend.cpp b/src/Backends/WaylandBackend.cpp index 1b85e63..370661d 100644 --- a/src/Backends/WaylandBackend.cpp +++ b/src/Backends/WaylandBackend.cpp -@@ -2977,6 +2977,9 @@ namespace gamescope +@@ -2977,6 +2977,8 @@ namespace gamescope LogDisplayError( "Failed to read events on input thread", m_pBackend->GetDisplay() ); abort(); } + -+ // Reading also queues releases and presentation feedback for steamcompmgr. + nudge_steamcompmgr(); } } diff --git a/tools/gamescope/patches/0124-commit-done-at-import.patch b/tools/gamescope/patches/0124-commit-done-at-import.patch index 9ac818e4e..9adfdf911 100644 --- a/tools/gamescope/patches/0124-commit-done-at-import.patch +++ b/tools/gamescope/patches/0124-commit-done-at-import.patch @@ -36,12 +36,11 @@ index 22aa907..0aef598 100644 void update_wayland_res(const std::shared_ptr &doneCommits, steamcompmgr_win_t *w, ResListEntry_t& reslistentry) { struct wlr_buffer *buf = reslistentry.buf; -@@ -8602,7 +8629,11 @@ void update_wayland_res(const std::shared_ptr &doneCommits, st +@@ -8602,7 +8629,10 @@ void update_wayland_res(const std::shared_ptr &doneCommits, st struct wlr_dmabuf_attributes dmabuf = {0}; if ( wlr_buffer_get_dmabuf( buf, &dmabuf ) ) { - fence = dup( dmabuf.fd[0] ); -+ // A finished buffer needs no fence of its own. + if ( cv_commit_ready_check && commit_fence_ready_now( dmabuf.fd[0] ) ) + bKnownReady = true; + else @@ -49,7 +48,7 @@ index 22aa907..0aef598 100644 } else { -@@ -8610,11 +8641,22 @@ void update_wayland_res(const std::shared_ptr &doneCommits, st +@@ -8610,11 +8640,22 @@ void update_wayland_res(const std::shared_ptr &doneCommits, st } } @@ -72,12 +71,11 @@ index 22aa907..0aef598 100644 else g_ImageWaiter.AddWaitable( newCommit.get() ); } -@@ -9400,6 +9442,13 @@ void steamcompmgr_check_xdg(bool vblank, uint64_t vblank_idx) +@@ -9400,6 +9441,12 @@ void steamcompmgr_check_xdg(bool vblank, uint64_t vblank_idx) } check_new_xdg_res(); + -+ // Commits marked done at import: handle them now rather than on the next wake. FIFO commits still wait for a vblank. + if ( s_bXdgCommitsDoneAtImport ) + { + s_bXdgCommitsDoneAtImport = false; @@ -86,12 +84,11 @@ index 22aa907..0aef598 100644 } void update_edid_prop() -@@ -10512,6 +10561,14 @@ steamcompmgr_main(int argc, char **argv) +@@ -10512,6 +10559,13 @@ steamcompmgr_main(int argc, char **argv) gamescope_xwayland_server_t *server = NULL; for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++) check_new_xwayland_res(server->ctx.get()); + -+ // Commits marked done at import: handle them now rather than on the next wake. FIFO commits still wait for a vblank. + if ( s_bXwaylandCommitsDoneAtImport ) + { + s_bXwaylandCommitsDoneAtImport = false; diff --git a/tools/gamescope/patches/0125-droiddeck-hud-frame-times.patch b/tools/gamescope/patches/0125-droiddeck-hud-frame-times.patch index 8fdbbf824..2fec1e7d2 100644 --- a/tools/gamescope/patches/0125-droiddeck-hud-frame-times.patch +++ b/tools/gamescope/patches/0125-droiddeck-hud-frame-times.patch @@ -69,11 +69,10 @@ index 0aef598..1473148 100644 gamescope::ConVar cv_commit_ready_check( "commit_ready_check", true, "Check a new commit's fence at import; a finished frame is marked done in the same pass instead of through the image waiter." ); // Set when update_wayland_res marks a commit done itself, so the main loop handles it in the same pass. -@@ -8519,6 +8522,16 @@ void update_wayland_res(const std::shared_ptr &doneCommits, st +@@ -8519,6 +8522,15 @@ void update_wayland_res(const std::shared_ptr &doneCommits, st uMangoMsgType = pMangoFocus->externalOverlayWindow->uMangoappMsgType; } -+ // The commits the typed stream of connector 0 counts, whether or not a mangoapp runs. + bool bDroidDeckHudFrame = false; + if ( pUpscaleFocus && !w->isSteamStreamingClient ) + bDroidDeckHudFrame = pUpscaleFocus->ulVirtualFocusKey == 0; @@ -86,7 +85,7 @@ index 0aef598..1473148 100644 bool bValidPreemptiveScale = reslistentry.pAcquirePoint && pUpscaleFocus && cv_upscale_preemptive; bool bPreemptiveUpscale = bValidPreemptiveScale && newCommit->ShouldPreemptivelyUpscale( pUpscaleFocus->eUpscaleFilter, pUpscaleFocus->eUpscaleScaler ); -@@ -8650,7 +8663,7 @@ void update_wayland_res(const std::shared_ptr &doneCommits, st +@@ -8650,7 +8662,7 @@ void update_wayland_res(const std::shared_ptr &doneCommits, st gpuvis_trace_printf( "pushing wait for commit %lu win %lx", newCommit->commitID, w->type == steamcompmgr_win_type_t::XWAYLAND ? w->xwayland().id : 0 ); { @@ -95,18 +94,10 @@ index 0aef598..1473148 100644 if ( bKnownReady ) { newCommit->Signal(); -@@ -9865,6 +9878,109 @@ static void publish_mangoapp_connector_snapshots() +@@ -9865,6 +9877,100 @@ static void publish_mangoapp_connector_snapshots() } } -+// DroidDeck's own HUD reads the frame times DroidDeck's mangoapp shows from a shared file named by -+// DROIDDECK_HUD_FRAMES, also while no mangoapp runs. That mangoapp (Valve's 20260902.1 build) reads the -+// field at byte 16 of each message as its frame time, which is app_frametime_ns here: one time per -+// signalled commit of the connector's windows (mangoapp_nudge_app_frame). The file gets the same commits -+// of connector 0. Little-endian: 0 magic, 4 version, 8 capacity, 12 pid, 16 frames written (u64), -+// 24 info sequence (odd while the info is written), 28 output width, 32 height, 36 refresh mHz, 40 Steam -+// focused, 44 engine (40 bytes), then from 128 one { frametime ns, monotonic time ns } pair per frame, -+// a ring of capacity. +static constexpr uint32_t k_uDroidDeckHudFramesMagic = 0x46484444; +static constexpr uint32_t k_uDroidDeckHudFramesVersion = 1; +static constexpr uint32_t k_uDroidDeckHudFramesCapacity = 1024; @@ -157,7 +148,6 @@ index 0aef598..1473148 100644 + return droiddeck_hud_frames_map() != nullptr; +} + -+// Signal() runs on the image waiter thread or, for a frame finished at import, on steamcompmgr's. +void droiddeck_hud_app_frame( uint64_t ulNow ) +{ + std::unique_lock lock( s_DroidDeckHudFramesMutex ); @@ -205,7 +195,7 @@ index 0aef598..1473148 100644 // mangohudctl posts one control message for every instance to act on. static void relay_mangoapp_control() { -@@ -10843,6 +10959,7 @@ steamcompmgr_main(int argc, char **argv) +@@ -10843,6 +10949,7 @@ steamcompmgr_main(int argc, char **argv) publish_mangoapp_snapshot(); publish_mangoapp_connector_snapshots(); diff --git a/tools/linuxfs/desktop/droiddeck-desktop-gpu b/tools/linuxfs/desktop/droiddeck-desktop-gpu index 419636e53..061d64620 100644 --- a/tools/linuxfs/desktop/droiddeck-desktop-gpu +++ b/tools/linuxfs/desktop/droiddeck-desktop-gpu @@ -19,7 +19,6 @@ def enabled(): def main_group(text): - """The [Desktop Entry] group's lines, and everything after it.""" lines = text.splitlines() start = next((i for i, l in enumerate(lines) if l.strip() == "[Desktop Entry]"), None) if start is None: @@ -56,8 +55,6 @@ def chromium_exec(exec_line): def gpu_exec(exec_line): - """The Exec that runs a program on the GPU; a Flatpak app goes through its launcher, which - keeps it on gamescope's X server instead of the desktop's Wayland socket.""" if exec_line.startswith("/usr/bin/flatpak run ") or exec_line.startswith("flatpak run "): exec_line = "/usr/local/bin/droiddeck-flatpak-run " + exec_line.split(" run ", 1)[1] return "droiddeck-gpu " + exec_line diff --git a/tools/linuxfs/fex/vulkanpin.c b/tools/linuxfs/fex/vulkanpin.c index 45aeb9edb..4ab28afe4 100644 --- a/tools/linuxfs/fex/vulkanpin.c +++ b/tools/linuxfs/fex/vulkanpin.c @@ -19,7 +19,6 @@ typedef struct { typedef VkResult (*Enumerate)(const char *, unsigned int *, VkExtensionProperties *); typedef Function (*ProcAddr)(void *, const char *); -/* The driver cannot enumerate devices for an instance with VK_KHR_display, and gamescope's WSI layer aborts on that. */ static const char *const hidden_extensions[] = { "VK_KHR_display", "VK_KHR_get_display_properties2", diff --git a/tools/linuxfs/innoextract/linuxfs/usr/local/bin/droiddeck-innoextract b/tools/linuxfs/innoextract/linuxfs/usr/local/bin/droiddeck-innoextract index 217878a36..f055affe6 100755 --- a/tools/linuxfs/innoextract/linuxfs/usr/local/bin/droiddeck-innoextract +++ b/tools/linuxfs/innoextract/linuxfs/usr/local/bin/droiddeck-innoextract @@ -1,7 +1,4 @@ #!/bin/sh -# Runs DroidDeck's bundled innoextract with its own boost libraries. The guest's -# env -i startup has no LD_LIBRARY_PATH, and the rootfs must not grow a second -# copy of boost for the installer fallback. LD_LIBRARY_PATH=/usr/local/lib/droiddeck-inno export LD_LIBRARY_PATH exec /usr/local/lib/droiddeck-inno/innoextract "$@" diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-agent b/tools/linuxfs/overlay/usr/local/bin/droiddeck-agent index 5e335c313..234be2f00 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-agent +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-agent @@ -1,22 +1,4 @@ #!/usr/bin/python3 -"""The guest side of the agent bridge (docs/agent-control.md). - -Started by droiddeck-session inside the session, so whatever it runs has the session's own -environment: DISPLAY, the games' Xwayland, XDG_RUNTIME_DIR, and the preloads every guest process -gets - which is what lets it reach the Steam client's DevTools port, open to guest processes only. - -The app talks to it through files under $BL_LAUNCH_DIR/agent (the session root, the same path on -both sides): - - req/.json a request the app wrote (tmp + rename) - resp/.json the answer, written the same way - hello.json this daemon is up: pid, version, what it can do - focus.json gamescope's focus and every server's top-level windows, rewritten on change - cdp-port the client's DevTools port, written by droiddeck-session - -Requests: ping, exec, focus, cdp, quit. Each answer is {"ok": true, ...} or -{"ok": false, "error": {"code", "message"}}. -""" import base64 import json @@ -72,7 +54,6 @@ class AgentError(Exception): class X: - """One connection to an Xwayland server. The rootfs has xprop but no python X library.""" def __init__(self, display): num = display.lstrip(":").split(".")[0] @@ -116,7 +97,6 @@ class X: return buf def request(self, data): - """Send one request and return its reply, or None for an X error (a window that went).""" self.s.sendall(data) while True: head = self._read(32) @@ -186,7 +166,6 @@ class X: def windows(x): - """Top-level windows that are mapped or carry an app id: what gamescope picks focus from.""" out = [] for win in x.children(x.root): attrs = x.attributes(win) @@ -230,9 +209,6 @@ def displays(): def focus_snapshot(): - """gamescope's focus as Steam sees it (root properties on the client's server) and every - server's windows. GAMESCOPE_FOCUSABLE_WINDOWS is the list the client orders the base layer - from: a game missing there stays behind the client however healthy its window looks.""" shown = displays() if not shown: raise AgentError("NO_DISPLAY", "This session has no X display") @@ -349,7 +325,6 @@ def tag_game_windows(snap, proc=Path("/proc")): def vr_scene_app(path=Path('/run/droiddeck/droiddeck-steamvr/scene-focus.json'), proc=Path('/proc')): - """Only a live SteamVR scene with its dashboard hidden may claim game focus.""" try: state = json.loads(path.read_text()) server, scene = int(state['serverPid']), int(state['scenePid']) @@ -385,8 +360,6 @@ class VrSceneFocus: class FocusWatcher(threading.Thread): - """Rewrites focus.json when gamescope's focus properties change, and every few seconds when - the windows do (a window can change without any root property moving).""" def __init__(self, path): super().__init__(daemon=True, name="focus") @@ -450,7 +423,6 @@ class FocusWatcher(threading.Thread): class WebSocket: - """A minimal RFC 6455 client: text frames out, text frames in. Enough for CDP.""" def __init__(self, url, timeout): target = urllib.parse.urlsplit(url) @@ -887,11 +859,6 @@ def steam_selection(agent_dir, timeout=5): def steam_shortcut_app_id(game_id): - """Return the 32-bit app id in Steam's shortcut CGameID form, or None for other ids. - - DroidDeck's shortcut IDs are `(appid << 32) | 0x02000000`. The URL still needs that full - CGameID for RunGame; the app-details API separately needs the unsigned 32-bit app id. - """ if not isinstance(game_id, str) or not game_id.isascii() or not game_id.isdigit(): return None try: @@ -907,7 +874,6 @@ def steam_shortcut_app_id(game_id): def wait_for_steam_shortcut_overview(agent_dir, game_id, timeout=45): - """Wait until Steam can map a shortcut CGameID to its 32-bit app overview.""" app_id = steam_shortcut_app_id(game_id) if app_id is None: return {"supported": False, "ready": False} @@ -962,8 +928,6 @@ def steam_shortcut_state(agent_dir, game_id, timeout=6): def game_pids(app_id): - """Our processes started for this Steam app (Steam sets SteamAppId / SteamGameId in their - environment, and Proton's wine processes inherit it).""" wanted = [("SteamAppId=%s" % app_id).encode(), ("SteamGameId=%s" % app_id).encode()] me = os.getpid() pids = [] @@ -1022,8 +986,6 @@ VR_ACTIONS = ("status", "install", "start", "stop") def steamvr(req): - """Cardboard mode's SteamVR: a fixed helper with four actions, unlike exec (no command line from - the request). install downloads 571 MB, so it runs detached and status reports it.""" action = req.get("action", "status") if action not in VR_ACTIONS: raise AgentError("INVALID_REQUEST", "vr action must be one of " + ", ".join(VR_ACTIONS)) @@ -1244,11 +1206,10 @@ def main(argv): if len(argv) != 2 or not argv[1].isascii() or not argv[1].isdecimal(): print("usage: droiddeck-agent --steam-supports-vr ", file=sys.stderr) return 2 - # Optional-VR games (Thumper, VRChat) carry vr_supported without vr_only. expression = ("(o => o ? (o.vr_supported === true || o.vr_only === true) : null)" "(window.appStore?.GetAppOverviewByGameID?.(%s))" % json.dumps(argv[1])) value = None - deadline = time.monotonic() + 12 # Steam's UI is busiest while a game starts. + deadline = time.monotonic() + 12 while value is None and time.monotonic() < deadline: try: value = cdp_eval(Path(base) / "agent", expression, await_promise=False, timeout=8).get("value") diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-bwrap b/tools/linuxfs/overlay/usr/local/bin/droiddeck-bwrap index 59bf0baf2..13dafc0ba 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-bwrap +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-bwrap @@ -68,7 +68,6 @@ def fd_path(fd): def expand(argv): - """argv with every --args FD replaced by the NUL-separated arguments read from FD.""" out = [] i = 0 while i < len(argv): @@ -188,7 +187,6 @@ def real(path): def remaps_nothing(s): - """True when every path in the sandbox is what it is out here already.""" for op in s.ops: t = op["t"] if t == "bind": diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-bookmarks b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-bookmarks index 28bdd8ac5..feb1f304b 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-bookmarks +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-bookmarks @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -"""Expose available Steam libraries in Dolphin, retaining the old PCManFM bookmark.""" import hashlib import json import os diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-games b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-games index fe1cf0d97..f49a90dd7 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-games +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-games @@ -54,7 +54,6 @@ def icon_for(appid): def client_entries(): - """App ids the client already wrote a menu entry for.""" found = set() if not os.path.isdir(APPS_DIR): return found diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-fex b/tools/linuxfs/overlay/usr/local/bin/droiddeck-fex index 7db59b048..bf18c79d3 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-fex +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-fex @@ -329,7 +329,6 @@ def mirror(source, provider, name, target): def build_rootfs(base, provider): - """The runtime with glibc and libstdc++ taken from a newer one, which FEX's GL and Vulkan thunks are built against.""" base_files, provider_files = os.path.join(base, "files"), os.path.join(provider, "files") rootfs = os.path.join(STATE, "rootfs") stamp = "%d\n%s\n%s%s\n%s" % (ROOTFS_LAYOUT, os.path.realpath(base), read_text(os.path.join(base_files, MARKER)) or "", @@ -431,8 +430,6 @@ def link_steam_sdk(): def link_guest_sdk(root): - # Proton's lsteamclient only looks in $HOME/.steam/sdk64; on ARM Steam that is the arm64 client, so - # the x86 runtime gets its own link, which FEX resolves before the host path. for sdk, candidates in (("sdk64", ("linux64", "ubuntu12_64")), ("sdk32", ("linux32", "ubuntu12_32"))): want = "x86_64" if sdk == "sdk64" else "i386" directory = next((os.path.join(STEAM_ROOT, candidate) for candidate in candidates @@ -612,17 +609,13 @@ def guest_environment(fex, env): result["FEX_APP_CONFIG_LOCATION"] = CONFIG_DIR + "/" result["FEX_APP_DATA_LOCATION"] = data + "/" result["FEX_APP_CACHE_LOCATION"] = CACHE_DIR + "/" - # Valve's FEX-Emu binds its server socket under $STEAM_COMPAT_DATA_PATH/fex-emu whatever FEX_APP_DATA_LOCATION or - # FEX_SERVERSOCKETPATH say, and sun_path is 108 bytes: compatdata under the app's data folder is too long. if result.get("STEAM_COMPAT_DATA_PATH"): short = data if data != DATA_DIR else "/tmp/fex" os.makedirs(short, exist_ok=True) result["STEAM_COMPAT_DATA_PATH"] = short result.setdefault("FEX_SILENTLOG", "0") result.setdefault("FEX_OUTPUTLOG", "stderr") - # The session's tunables are for the host libc; an x86 ld.so copying them under FEX fails in sbrk. result.pop("GLIBC_TUNABLES", None) - # Mono's incremental Boehm collector corrupts its heap under FEX, so Unity games crash at their first collections. result.setdefault("GC_DISABLE_INCREMENTAL", "1") if fex["portable"]: result["FEX_PORTABLE"] = "1" @@ -713,7 +706,6 @@ def launch(command, env, mode, decide=None): folder = decide if decide and os.path.isdir(decide) else os.path.dirname(program) if thunks == "on" and x86_64 and os.path.isfile(os.path.join(PRELOADS, "x86_64", VULKAN_PIN)) \ and vulkan_thunk_loads(fex, folder): - # Any LD_AUDIT module makes Mono's garbage collector crash under FEX, so the Vulkan thunk is pinned by search path. guest["LD_LIBRARY_PATH"] = ":".join(["/usr/" + VULKAN_PIN_DIR] + split_paths(guest.get("LD_LIBRARY_PATH"))) thunks = "on, its own Vulkan loader too" say("running %s with %s (runtime %s, thunks %s)" % (program, fex["bin"], guest.get("FEX_ROOTFS", "none"), thunks)) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-fex-update b/tools/linuxfs/overlay/usr/local/bin/droiddeck-fex-update index 3611978f5..d88a4a97e 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-fex-update +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-fex-update @@ -3,7 +3,6 @@ FEX_TAG=fex-2610-r5 FEX_SHA256=0fc1ca2ba13817ad70d1ea157119a75c9c02446b57d166e6d676cce0807c19c4 FEX_URL=https://github.com/Droid-Deck/DroidDeck-Components/releases/download/$FEX_TAG/fex.tzst -# Whether every program in the FEX tree at $1 finds its libraries (r4 did not without libxxhash). fex_loads() { local b out for b in FEX FEXServer unsquashfs; do @@ -13,8 +12,6 @@ fex_loads() { done } -# Installs one FEX build only once its archive matches the sha256 and it loads. The files it replaces are kept -# until the installed copy loads too, and put back otherwise; the tag is written last. fex_install() { local tag=$1 sha=$2 urls=$3 dst=/ tmp u f tmp=$(mktemp -d) @@ -54,8 +51,6 @@ fex_install() { return 1 } -# The build DroidDeck-Components' fex.json names (the app passes it as BL_FEX_*) comes first, then a working -# installed one, then FEX_TAG above. Sets FEX_NOW and FEX_FROM. fex_ready() { local have ok= have=$(cat /usr/local/share/fex-emu/.droiddeck-tag 2>/dev/null || true) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak b/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak index eea38e457..8c23140ef 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak @@ -38,10 +38,6 @@ BUS_CONFIG = """ 2 and os.path.basename(command[2]).lower() == "redprelauncher.exe": game = os.path.join(os.path.dirname(command[2]), "bin", "x64", "Cyberpunk2077.exe") if os.path.isfile(game): @@ -246,7 +233,6 @@ def ubisoft_overlay_off(env): def offline_without_anticheat(command, env): - """Start the configured game executable when the player opts into launching without EAC.""" if (env.get("DROIDDECK_ANTICHEAT_OFFLINE") != "1" or len(command) <= 2 or command[1] != "waitforexitandrun" or not re.fullmatch(r"start_protected_game(?:_?x64|_?64)?\.exe", @@ -264,7 +250,6 @@ def offline_without_anticheat(command, env): game = os.path.join(folder, relative) if os.path.isabs(relative) or relative.startswith("..") or not os.path.isfile(game): return command - # Resolve symlinks before accepting an executable inside the game directory. root = os.path.realpath(folder) if os.path.commonpath((root, os.path.realpath(game))) != root: return command @@ -275,7 +260,6 @@ def offline_without_anticheat(command, env): def engine_fixes(exe, args): - """Environment and extra arguments a game needs because of what it is built with.""" game = Game(exe, args) env, extra, notes = {}, [], [] for check in FIXES: @@ -291,7 +275,6 @@ def engine_fixes(exe, args): def win_components(compat, app_id): - """The game's Windows components put into its prefix; returns their WINEDLLOVERRIDES entries.""" if not compat: return "" try: @@ -318,7 +301,6 @@ def game_gpu(env): def vram_cap(env): - """Puts the session's VRAM cap ahead of the game's own DXVK settings, so a game profile can still raise it.""" value = env.get("BL_VRAM_MB", "") if not re.fullmatch(r"[1-9][0-9]{0,6}", value): return env @@ -330,7 +312,6 @@ def vram_cap(env): def parse_cores(text): - """CPU numbers from a list such as "0-3,6"; None when the text is not one.""" cores = set() for part in text.replace(" ", "").split(","): match = re.fullmatch(r"(\d{1,3})(?:-(\d{1,3}))?", part) @@ -353,7 +334,6 @@ def online_cores(): def game_cores(env): - """Pins the game to the cores its profile names, in place of the session's game cores.""" text = env.get("DROIDDECK_GAME_CORES", "").strip() if not text: return @@ -370,7 +350,6 @@ def game_cores(env): def core_speeds(cores, root="/sys/devices/system/cpu"): - """Each core's capacity, else its top clock; None when some core reports neither.""" for name in ("cpu_capacity", "cpufreq/cpuinfo_max_freq"): speeds = {} for core in cores: @@ -385,14 +364,12 @@ def core_speeds(cores, root="/sys/devices/system/cpu"): def fastest_first(cores): - """The cores, fastest first; cores of equal or unknown speed keep their numeric order.""" cores = sorted(cores) speeds = core_speeds(cores) return sorted(cores, key=lambda core: -speeds[core]) if speeds else cores def proton_cpu_limit(command, env): - """This game's entry in Proton's own default_cpu_limit list: a core count, another value such as "P" as text, or None.""" app_id = env.get("SteamGameId", "") if not command or not app_id.isdigit(): return None @@ -412,7 +389,6 @@ def proton_cpu_limit(command, env): def cpu_topology(env, command=()): - """Windows sees the game's cores fastest first, so a thread a game pins to CPU 0 runs on the fastest core.""" if "PROTON_CPU_TOPOLOGY" in env: return if "WINE_CPU_TOPOLOGY" in env: @@ -445,9 +421,6 @@ def cpu_topology(env, command=()): def gl_vsync(env): - """OpenGL stops waiting for each screen refresh; Steam's Frame Limit still holds the game through gamescope.""" - # Wine asks for interval 1 on every GL window, and WineD3D's front-buffer drawing (DirectDraw, - # Direct3D 1-7) never lowers it, so those games ran at the refresh rate with no limit set. if "vblank_mode" in env or game_id(env) is None: return False env["vblank_mode"] = "0" @@ -501,7 +474,6 @@ WINE_DEBUG_SELECTOR = re.compile(r"(?:(trace|warn|err|fixme))?([+-])([A-Za-z_][A def game_debug_channels(*values): - """Normalize Wine selectors, retaining custom channels before required diagnostics.""" selectors = {} for value in values: if not isinstance(value, str) or "\0" in value or len(value) > 8192: @@ -512,8 +484,6 @@ def game_debug_channels(*values): if match is None: continue key = (match[1] or "", match[3]) - # Move the last value to the end: global +/-all and per-class selectors - # have ordered semantics, so replacing a value in its old position is wrong. selectors.pop(key, None) selectors[key] = part for channel in GAME_DEBUG_CHANNELS: @@ -527,7 +497,6 @@ def game_debug_channels(*values): def apply_game_debug_logs(env, config, debug_dir): - """Apply app-owned diagnostics to a real game, without editing its launch command.""" if not isinstance(config, dict) or config.get("version") != 1 or config.get("gameDebugLogs") is not True: return env if (not isinstance(debug_dir, str) or "\0" in debug_dir or len(debug_dir) > 8192 @@ -599,10 +568,6 @@ SSBS_LIB = "/usr/local/lib/libssbs.so" def force_ssbs(command, env): - """DROIDDECK_FORCE_SSBS=1 (a game's or the shared environment): preload libssbs.so into this - Proton's Wine, which then keeps PSTATE.SSBS set across signal returns - speculative store - bypass stays allowed, as force_ssbs.diff does inside a rebuilt ntdll. ARM64 Protons only: the - library is aarch64 and only arm64 Wine has the signal path it fixes.""" if env.get("DROIDDECK_FORCE_SSBS", "").lower() not in ("1", "on", "true", "yes"): return env proton = os.path.dirname(command[0]) if command else "" @@ -623,10 +588,6 @@ EVALUATOR_SCRIPT = re.compile(r"(?:^|[\\/])evaluatorscript_[0-9]+\.vdf$", re.IGN def mark_install_scripts(command, env): - """The client is about to run a game's install scripts (iscriptevaluator.exe): mark every process - in them but a Vulkan runtime installer as already run first, so no redistributable installer - starts - on first launch the shared ones may still be downloading, and on ARM64 Proton the VC++ - ones stop with an error.""" if not any(arg.lower().replace("\\", "/").endswith("iscriptevaluator.exe") for arg in command): return [] root = env.get("BL_X64_STEAM_ROOT") or env.get("STEAM_COMPAT_CLIENT_INSTALL_PATH") or str(Path.home() / ".local/share/Steam") @@ -750,7 +711,6 @@ DIRT_SAVED = ".droiddeck-original" def dirt_telemetry_on(prefix, game): - """Whether the game's UDP/motion output is on, as In-game stats or another listener sets it ("dbox" is its default).""" try: folders = [f for f in (Path(prefix) / "pfx/drive_c/users/steamuser/Documents/My Games").iterdir() if f.name.lower().replace(" ", "") == game] @@ -785,7 +745,6 @@ WINE_DLL_DIRS = ("/lib/wine/i386-windows", "/lib/wine/x86_64-windows", "/lib/win def is_wine_dll_link(path): - """A link into a Wine DLL folder: the rule Proton uses to tell its own DLLs in a prefix (file_is_wine_builtin_dll).""" try: return os.path.islink(path) and os.path.dirname(os.readlink(path)).endswith(WINE_DLL_DIRS) except OSError: @@ -804,7 +763,6 @@ def is_dirt_stand_in(path): def dirt_ws2_wanted(command, env): - """The prefix to give the ws2_32 stand-in: a DiRT 3 / Showdown launch with telemetry on and a Proton without the fix.""" prefix = env.get("STEAM_COMPAT_DATA_PATH", "").rstrip("/") if len(command) < 3 or not prefix or not os.path.isfile(DIRT_WS2): return None @@ -815,7 +773,6 @@ def dirt_ws2_wanted(command, env): def swap_dirt_ws2(prefix): - """Puts the stand-in where the prefix's syswow64/ws2_32.dll is; that file is kept beside it.""" target = prefix_ws2(prefix) saved = target.with_name(target.name + DIRT_SAVED) data = Path(DIRT_WS2).read_bytes() @@ -829,11 +786,6 @@ def swap_dirt_ws2(prefix): def restore_dirt_ws2(prefix, proton): - """Puts a Wine syswow64/ws2_32.dll back if the stand-in is still there; true when it changed something. - - That is the prefix's own one, unless it was a link to a Wine DLL folder and the game ran on another Proton since - the swap: Proton then relinked the prefix's other DLLs to itself but left the stand-in, so a link to the running - Proton's ws2_32.dll goes back instead. Anything else (a native file, a link to a mod's DLL) goes back as it was.""" target = prefix_ws2(prefix) saved = target.with_name(target.name + DIRT_SAVED) own = proton_ws2(proton) @@ -868,10 +820,6 @@ def windows_core_count(env): def ego_core_plan(command, env): - """Codemasters EGO games (DiRT 3, DiRT Showdown, GRID, F1 ...) pick their thread plan by core count from - system/hardware_settings_restrictions.xml. A count with no plan (5 when a phone's slow cores are left out) puts - every worker on one core, and DiRT 3 then stops on a loading screen. Adds a plan for the count Windows sees, - copied from the nearest smaller one; the file as it was is kept beside it. Returns a note, or None.""" count = windows_core_count(env) if not count or len(command) < 3: return None diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-gog-maintenance b/tools/linuxfs/overlay/usr/local/bin/droiddeck-gog-maintenance index 4a56fd122..1fc6e0f37 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-gog-maintenance +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-gog-maintenance @@ -1,5 +1,4 @@ #!/usr/bin/python3 -"""GOG patch installers and the lease shared by maintenance and game launches.""" import fcntl import errno import json @@ -133,7 +132,6 @@ def runner(folder, appid, game, graphical=False): base = desktop_environment(os.environ) if graphical else dict(os.environ) env = run["environment"](base, target, tool, root, window=base.get("BL_DESKTOP") == "1") env["DROIDDECK_GOG_MAINTENANCE"] = "1" - # Installer copy routines use x87 integer loads/stores; reduced precision corrupts paths (FEX #5827). env["FEX_X87REDUCEDPRECISION"] = "0" env.pop("BL_STORE_EXIT", None) return str(Path(tool) / command), env, target.compatdata diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-gpu-window b/tools/linuxfs/overlay/usr/local/bin/droiddeck-gpu-window index 0c1c9818f..5fc4e20a0 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-gpu-window +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-gpu-window @@ -1,5 +1,4 @@ #!/usr/bin/python3 -"""Let GTK draw close buttons inside gamescope, which supplies no window decorations.""" import ctypes as c import os import signal diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-launcher-cores b/tools/linuxfs/overlay/usr/local/bin/droiddeck-launcher-cores index 3ea0c5681..034a910b0 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-launcher-cores +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-launcher-cores @@ -1,20 +1,4 @@ #!/usr/bin/python3 -"""Holds a game launcher's hidden web interface on one slow core while something else has focus. - -Rockstar's launcher keeps drawing its Chromium interface (SocialClubHelper.exe: a renderer, and a -GPU process compositing through DXVK) for as long as it runs, hidden behind the game too, and at a -higher priority than the game's (Wine passes Chromium's on: the GPU process ran at nice -10). With -Red Dead Redemption 2 in front on the RedMagic that was 1.3 to 2.5 cores under FEX, on a phone -already held back by its temperature, for a page nobody sees. Held, riding at the throttle limit -went from 46.5 to 48.7 fps, 1% lows from 27.5 to 32.6. So while the focused window belongs to -anything but the launcher, every helper thread runs on one core of the slowest cluster at nice 19; -when the launcher's own window has focus again (signing in, the game closed) each thread gets its -own mask and priority back. Stopping the helpers instead would not hold: proot, the tracer of every -guest process, resumes a stopped tracee. - -Focus comes from droiddeck-agent's focus.json (the path is the argument), which the agent rewrites -whenever gamescope's focus changes. droiddeck-session starts this for Steam sessions; -BL_LAUNCHER_CORES=0 leaves the launchers alone.""" import json import os import signal @@ -35,7 +19,6 @@ def log(message): def image(proc, pid): - """A process's program as Wine names it (argv[0]), lower case with backslashes, or None.""" try: with open(proc / str(pid) / "cmdline", "rb") as stream: first = stream.read(4096).split(b"\0", 1)[0] @@ -56,7 +39,6 @@ def tasks(proc, pid): class Cores: - """The kernel calls, apart so a test can stand in for them.""" def own(self): return os.sched_getaffinity(0) @@ -81,8 +63,6 @@ class Holder: self.core = None def slow_core(self): - """The last core of the slowest cluster the session has - not of the game's cores, which - the launcher inherits when BL_GAME_CPUS is set: the core the game misses least.""" def top(cpu): try: return int((self.cpufreq / f"cpu{cpu}/cpufreq/cpuinfo_max_freq").read_text()) @@ -98,8 +78,6 @@ class Holder: yield int(name) def hold(self): - """Every helper thread onto the slow core at NICE: new ones, and held ones the program has - moved since (Chromium sets its threads' priorities, and Wine passes that on).""" if self.core is None: self.core = self.slow_core() hold = ({self.core}, NICE) @@ -125,8 +103,6 @@ class Holder: return None def release(self): - """Every held thread back to its own mask and priority; one started during the hold gets - what its process had.""" for pid, before in self.held.items(): default = before.get(pid) or self.first() for tid in tasks(self.proc, pid): @@ -141,7 +117,6 @@ class Holder: def focused(snapshot): - """(app id, pid) of the window gamescope has focused, from droiddeck-agent's snapshot.""" window = snapshot.get("focusedWindow") for entry in snapshot.get("focusableWindows") or []: if entry.get("window") == window: diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-login1 b/tools/linuxfs/overlay/usr/local/bin/droiddeck-login1 index 5c04842c0..3ac444d6e 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-login1 +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-login1 @@ -1,10 +1,4 @@ #!/usr/bin/python3 -"""Steam's login1 sleep notifications, backed by DroidDeck's session pause. - -Android owns device power. Suspend here pauses the guest through SessionService; -the app's Resume action thaws it and completes PrepareForSleep(false). Steam -must see this service before startup or it can stay black after its idle timer. -""" import importlib.machinery import importlib.util import os diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-msi-install b/tools/linuxfs/overlay/usr/local/bin/droiddeck-msi-install index 87e741c46..681cf4031 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-msi-install +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-msi-install @@ -1,32 +1,4 @@ #!/usr/bin/python3 -"""Installs a Windows Installer package (.msi) the way Wine's own msiexec would, without running it. - -droiddeck-msi-install -droiddeck-msi-install --unpack

- -A .msi is a database: which files go to which folder, which registry values to write, which .NET -and side-by-side assemblies to register. This reads those tables (msiinfo), unpacks the package's -cabinets (cabextract, which knows every compression a cabinet can use) and lays the result out -for droiddeck-wincomponents, which puts it into a game's prefix at launch: - - /drive_c/... the files, at the paths they get on 64-bit Windows - /registry.json the registry values, in the view (32/64-bit) Windows uses - -An installer .exe is opened the same way when it is a wrapper around packages: a WiX bundle -(Visual C++ 2012 and later, the .NET Core and 5+ runtimes: the packages sit in a cabinet the -bundle's own header locates, named in its manifest), a self-extracting cabinet (Visual C++ 2005 -to 2010) or a self-extracting 7-Zip archive (.NET Framework 4.0 to 4.8). The packages inside are -laid out one after the other into the same component dir; the 32- and 64-bit ones both, an ARM64 -one never. An .exe that is its own setup program (NSIS, InnoSetup) holds no package: an error says so. - -What a package does by running its own code (custom actions such as DXSETUP, self-registering -DLLs) cannot be done this way; each is named in the notes, so nothing is left out silently. - ---unpack opens an archive or installer for the caller to take files out of (cabextract for a -cabinet or self-extracting cabinet, 7-Zip for the rest) and prints the files it found. - -Prints "progress " lines while it works and ends with "result ": the counts and notes. -""" import json import os import re @@ -47,7 +19,6 @@ SKIP_PLATFORMS = ("arm64", "arm") def tool_env(): - """The environment the tools run in: their own libraries (libmsi, libgsf, libgcab) beside them.""" env = dict(os.environ) env["LD_LIBRARY_PATH"] = ":".join(p for p in (str(TOOLS), env.get("LD_LIBRARY_PATH", "")) if p) return env @@ -95,7 +66,6 @@ class MsiError(Exception): def progress(text, fraction=None): - """A progress line: "progress ", or "progress NN% " when how far along is known.""" if fraction is None: print("progress " + text, flush=True) else: @@ -103,7 +73,6 @@ def progress(text, fraction=None): class Span: - """The slice of the whole job one package's progress maps into (a wrapper runs several).""" def __init__(self, lo=0.0, hi=1.0): self.lo, self.hi = lo, hi @@ -137,7 +106,6 @@ class Package: self.summary[name.strip().lower()] = value.strip() def table(self, name): - """The table's rows as dicts by column name; an absent table has none.""" if name in self._tables: return self._tables[name] rows = [] @@ -158,7 +126,6 @@ class Package: def long_name(value): - """A Directory/File name column: "short|long", the long one when there are two.""" return value.split("|")[-1] @@ -176,7 +143,6 @@ KEYWORDS = {"not", "and", "or", "xor", "eqv", "imp"} class Conditions: - """Windows Installer's condition language, enough of it for what packages ask at install time.""" def __init__(self, props, feature_state=None, component_state=None): self.props = props @@ -318,7 +284,6 @@ class Conditions: def win_to_rel(path): - """C:\\windows\\x\\ -> drive_c/windows/x; None for a path off drive C or a folder left out.""" if not path: return None match = re.match(r"^[Cc]:\\?(.*)$", path) @@ -341,7 +306,6 @@ def clean_version(version): def clr_version(path): - """The .NET runtime an assembly was built for ("v4.0.30319"), from its metadata header.""" try: data = Path(path).read_bytes() except OSError: @@ -389,7 +353,6 @@ class Installer: return 3 if name in self.included_components else -1 def directory(self, key): - """The folder a Directory key resolves to, as drive_c/..., or None when it is left out.""" if key in self._resolving: return None value = self.props.get(key) @@ -414,7 +377,6 @@ class Installer: return parent + "/" + default def format(self, text): - """Formatted text: [Property], [Folder], [#file], [$component], [%env], [\\x], [~].""" if not text: return text @@ -449,7 +411,6 @@ class Installer: def run_set_actions(self): - """The property/folder-setting custom actions, in sequence order; the rest go to the notes.""" actions = {row["Action"]: row for row in self.pkg.table("CustomAction")} sequence = {} for table in ("InstallUISequence", "InstallExecuteSequence"): @@ -506,7 +467,6 @@ class Installer: self.included_components.add(name) def assemblies(self): - """Component -> where its files go when it is a global assembly (the GAC or winsxs).""" names = {} for row in self.pkg.table("MsiAssemblyName"): names.setdefault(row["Component_"], {})[row["Name"].lower()] = row["Value"] @@ -535,7 +495,6 @@ class Installer: @staticmethod def sxs_names(ident): - """Wine's winsxs names for a side-by-side assembly: its folder and, for a policy, the policy folder.""" arch = ident.get("processorarchitecture", "x86").lower() or "x86" name = ident.get("name", "").lower() token = ident.get("publickeytoken", "").lower() @@ -548,7 +507,6 @@ class Installer: return "assembly", "%s_%s_%s_%s_none_deadbeef" % (arch, name, token, version), version def unpack(self, work): - """Every cabinet in the package, unpacked; returns File key -> unpacked file.""" found = {} media = [row for row in self.pkg.table("Media") if row.get("Cabinet", "")] for index, row in enumerate(media): @@ -656,7 +614,6 @@ class Installer: self.registry.append({"hive": hive, "key": key, "name": name, "type": kind, "data": data}) def registry_data(self, raw): - """A Registry table Value: (type, data) as droiddeck-wincomponents writes them.""" if raw is None or raw == "": return "sz", "" if raw.startswith("#x") or raw.startswith("#X"): @@ -773,7 +730,6 @@ class Installer: def sibling(path, name): - """A file next to [path] called [name], whatever its case; None when there is none.""" folder = path.parent candidate = folder / name if candidate.is_file(): @@ -786,7 +742,6 @@ def sibling(path, name): def kind_of(path): - """What a file is by its first bytes: msi, exe, cab or other.""" with open(path, "rb") as source: head = source.read(8) if head.startswith(MSI_MAGIC): @@ -806,9 +761,6 @@ def run_tool(args, timeout=1800): def burn_containers(data): - """A WiX bundle's containers as (offset, size) pairs: the .wixburn section of the stub names - their sizes; each is a cabinet laid out after the stub, found by its header since signing can - move them. None when the file is no bundle.""" try: pe = struct.unpack_from(" packages/vcRuntimeMinimum_amd64/vc_runtimeMinimum_x64.msi).""" data = path.read_bytes() names = {} for index, (start, size) in enumerate(containers): @@ -877,8 +827,6 @@ def unpack_burn(path, dest, containers): def unpack_installer(path, dest): - """Opens an installer or archive into [dest]: a bundle by its header, a cabinet (or - self-extracting one) with cabextract, anything else with 7-Zip.""" kind = kind_of(path) if kind == "exe": containers = burn_containers(path.read_bytes()) @@ -900,7 +848,6 @@ def unpack_installer(path, dest): def packages_in(root): - """The Windows Installer packages an unpacked installer holds, shallowest first, by name.""" found = [] for entry in sorted(root.rglob("*")): if entry.is_file() and kind_of(entry) == "msi": @@ -910,11 +857,6 @@ def packages_in(root): def choose_packages(found, root): - """Which packages make up the install: the ones at the top of the installer (a bundle keeps - each in its own folder, so there every level counts), for the x86 and x64 platforms. The .NET - Framework 4.5+ installers carry a complete product (netfx_Full_*) beside the 4.0 parts they - patch (netfx_Core_*, netfx_Extended_*) and per-Windows-version copies in NetFx* folders: the - complete product wins, the copies are left out.""" if not found: return [] depth = min(len(entry.relative_to(root).parts) for entry in found) @@ -933,7 +875,6 @@ def choose_packages(found, root): def install_wrapper(path, out): - """Lays out every package inside an installer wrapper; the result is the sum of them.""" total = {"product": "", "version": "", "counts": {"files": 0, "assemblies": 0, "registry": 0}, "notes": [], "packages": []} with tempfile.TemporaryDirectory(dir=str(out.parent), prefix=".unpack-") as work: dest = Path(work) / "x" @@ -968,9 +909,6 @@ NESTED_SUFFIXES = (".7z", ".zip", ".cab", ".exe", ".msi", ".rar", ".xz", ".gz", def open_nested(dest, depth=0): - """Archives inside an unpacked installer (QuickTime's cabinets, an NSIS pack's payloads) are - opened in place, one ".nested" folder per archive, two levels deep: the same layout the - component recordings (winlator-contents tools/snapshot/record.py) map their file sources to.""" if depth > 1: return 0 opened = 0 diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-open b/tools/linuxfs/overlay/usr/local/bin/droiddeck-open index 2fc023e04..0edd30212 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-open +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-open @@ -1,19 +1,4 @@ #!/usr/bin/python3 -"""Runs a program from the desktop: a Windows program through the Proton Steam has chosen, a Linux -program (x86 ones under FEX), an AppImage, or a script (under FEX beside x86 programs, as a game's -start script is). Shared storage is noexec: FEX runs x86 programs there where they lie (libblsession's -noexec.c), while a native one is copied - alone into the cache, or with its folder by the app once (as -Add an app does) when that holds native programs or libraries. A Steam library's games are Steam's. - - droiddeck-open [--window | --terminal] FILE [ARGS...] the file manager's Run entries - droiddeck-open --exec FILE [ARGS...] an exec the kernel refused (libblsession) - droiddeck-open --install the Run entries and default handlers - -The desktop composites in software, so a program started from it runs on the GPU in a window of its -own (droiddeck-gpu), as the menu's games do: --window keeps it on the desktop's software drawing -instead, --terminal runs it in Konsole. A plain script stays on the desktop, and an exec whose output is read (a terminal, -a pipe, a file) stays where it was started. -""" import fcntl import hashlib import os @@ -120,7 +105,6 @@ def for_x86(script): def runnable(path): - """The program itself, or a copy in the cache of a native one on noexec storage.""" if not noexec(path) or machine(head(path)) in EMULATED: return path st = os.stat(path) @@ -141,7 +125,6 @@ def runnable(path): def appimage(path): - """The image unpacked once into the cache, laid out as droiddeck-appimage-run expects.""" st = os.stat(path) real = os.path.realpath(path) key = hashlib.sha1(("%s\0%d\0%d" % (real, st.st_size, st.st_mtime_ns)).encode("utf-8")).hexdigest()[:16] @@ -150,7 +133,6 @@ def appimage(path): def once(key, make): - """[make]() for [key] one start at a time: a second start waits for the first, then finds it done.""" locks = os.path.join(CACHE, "locks") os.makedirs(locks, exist_ok=True) with open(os.path.join(locks, key), "w") as lock: @@ -203,8 +185,6 @@ def import_of(key, what): def imported(what, real, key, name): - """[real] added by the app instead, as Add an app adds it (to the menu too): an AppImage whose hard - links (sharun's) only the app's own proot can make, or a folder of native programs off noexec storage.""" launch = os.environ.get("BL_LAUNCH_DIR") if not launch or not os.path.isdir(launch): raise RuntimeError("%s cannot run from here; add it in DroidDeck with Add an app." % name) @@ -235,9 +215,6 @@ def imported(what, real, key, name): def game_folder(path): - """The folder of [path] when it runs only from a copy: on noexec storage, holding native programs or - libraries (besides [path]) that it starts or loads. Not the storage itself, one of Android's folders - or a Steam library.""" real = os.path.realpath(path) folder = os.path.dirname(real) if in_steam_library(real) or not noexec(folder): @@ -263,12 +240,10 @@ def game_folder(path): def in_steam_library(path): - """Whether [path] is in a Steam library: its games are Steam's to start, never copied in.""" return "steamapps" in os.path.realpath(path).lower().split(os.sep) def storage_root(folder): - """The top of the storage [folder] is on: the last folder above it on the same file system.""" top = folder try: dev = os.stat(folder).st_dev @@ -280,7 +255,6 @@ def storage_root(folder): def copied_in(path): - """The app's copy of [path]'s folder, added (once) for it.""" real = os.path.realpath(path) key = hashlib.sha1(("folder\0%s" % real).encode("utf-8")).hexdigest()[:16] name = os.path.basename(os.path.dirname(real)) @@ -288,7 +262,6 @@ def copied_in(path): def steam_title(path): - """The Steam game [path] is the program of: Steam starts it, with the client most games need.""" try: run = runpy.run_path(PROTON_RUN) root = os.environ.get("STEAM_COMPAT_CLIENT_INSTALL_PATH") or run["STEAM_ROOT"] @@ -299,7 +272,6 @@ def steam_title(path): def command(path, args, what, window=False, appid=None): - """(command, working directory) that runs [path] in place.""" folder = os.path.dirname(path) if what == "steam": return [STEAM_LAUNCH, "desktop", appid], folder @@ -321,7 +293,6 @@ def on_desktop(env): def unattended(env): - """Nobody reads the output: it goes to /dev/null or the desktop's log, as from Dolphin or the menu.""" try: out = os.fstat(1) except OSError: @@ -337,7 +308,6 @@ def unattended(env): def placed(cmd, what, mode, env): - """[cmd] where it shows: on the GPU from the desktop, else where it was started.""" if mode == "terminal" and what not in ("windows", "steam") and shutil.which("konsole"): return ["konsole", "--hold", "-e"] + cmd if mode == "window" or what in ("script", "steam") or not on_desktop(env): diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-open-url b/tools/linuxfs/overlay/usr/local/bin/droiddeck-open-url index 890833519..c83dcfbc8 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-open-url +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-open-url @@ -1,11 +1,4 @@ #!/usr/bin/python3 -"""A game's web links to Android: Wine's winebrowser starts this (named as the browser in the -prefix's HKCU\\Software\\Wine\\WineBrowser, and as xdg-open first on a game's PATH), and an -http(s) address goes to the app, which opens it in Android's browser - so an EOS sign-in step or a -store page can be finished on the phone. Anything else goes on to the system's own xdg-open. - -The session channel is $BL_LAUNCH_DIR/stores; Wine may not pass that variable on to what it -starts, so droiddeck-proton also leaves it in LAUNCH_DIR_FILE.""" import json import os @@ -25,7 +18,6 @@ def hand_over(url, launch_dir): def where(url): - """scheme://host only, for the log: no path, no query.""" from urllib.parse import urlsplit try: u = urlsplit(url) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-pad-defaults b/tools/linuxfs/overlay/usr/local/bin/droiddeck-pad-defaults index 04966c4b2..7d28a5d28 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-pad-defaults +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-pad-defaults @@ -89,7 +89,6 @@ def rpcs3_shader_mode(): def ini_section(lines, name): - """(start, end) line indices of [name]'s body, or None.""" start = None for i, l in enumerate(lines): t = l.strip() @@ -102,9 +101,6 @@ def ini_section(lines, name): def patch_ini_section(path, section, body, what, needle="SDL-", require=None, done="Player 1 on the controller"): - """Add body's lines to [section] of an ini the emulator wrote itself, unless the section - already binds a controller (needle). Missing file: nothing - the emulator writes its own - defaults on first run and this runs again.""" if not os.path.isfile(path): return with open(path, encoding="utf-8", errors="replace") as f: @@ -178,7 +174,6 @@ def ini_get(path, section, key): def ini_set(path, section, key, value): - """Sets one key (the first of that name) in [section], adding either as needed.""" with open(path, encoding="utf-8", errors="replace") as f: lines = f.read().splitlines() span = ini_section(lines, section) @@ -440,9 +435,6 @@ def armsx2(): def armsx2_bios(ini): - """PCSX2 boots the one BIOS file named in [Filenames] BIOS (inside [Folders] Bios); its first-time - setup is what names it. With that skipped, the first dump in the folder is named here - a US one - first - while none is named or the named one is gone. A choice made in ARMSX2 is kept.""" folder = ini_get(ini, "Folders", "Bios") or ps2_bios_folder() if not os.path.isabs(folder): folder = os.path.join(os.path.dirname(os.path.dirname(ini)), folder) @@ -518,12 +510,6 @@ DOLPHIN_DATA = "/usr/share/dolphin-emu" def dolphin_data(): - """The Dolphin build in the catalog finds its own data - the GameCube fonts, the per-game fixes - (GameSettings: GUT.ini puts Ultimate Spider-Man on the MMU), its icons and shaders - only at the - fixed /usr/share/dolphin-emu it was built for; there is no setting or variable to move it. Here it - runs from a copy unpacked under /tmp (no FUSE), so it found none of it: a GameCube game stopped - on "Windows-1252 fonts are not loaded", another on an invalid write the missing game fix would - have prevented. So the folder is unpacked from the AppImage to that path, once per AppImage.""" import shutil, subprocess, tempfile try: st = os.stat(DOLPHIN_APPIMAGE) @@ -716,7 +702,6 @@ def melonds_geometry(): def toml_get(lines, table, key): - """The raw value of key in [table], or None.""" inside = False for l in lines: t = l.strip() @@ -728,7 +713,6 @@ def toml_get(lines, table, key): def toml_set(lines, table, key, value): - """key = value in [table]; the table is added at the end when missing.""" start = None for i, l in enumerate(lines): t = l.strip() diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-pipewire b/tools/linuxfs/overlay/usr/local/bin/droiddeck-pipewire index 2677599bf..df776d664 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-pipewire +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-pipewire @@ -1,5 +1,4 @@ #!/bin/sh -# Audio stays on the session's PulseAudio, so WirePlumber runs its upstream video-only profile. set -u bundle=/usr/local/lib/droiddeck-pipewire.tzst diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-extra b/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-extra index 241ac290d..b7b1c27b0 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-extra +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-extra @@ -31,7 +31,6 @@ def fetch_json(url): def pick_asset(source): - """(release tag, asset url, sha512 url or None) for the newest full release with an ARM64 build.""" repo, pattern = SOURCES[source] for release in fetch_json(RELEASES % repo + "?per_page=15"): if release.get("draft") or release.get("prerelease"): @@ -58,8 +57,6 @@ def release_asset(source, tag): def download(url, path): - """Resumable, because this is hundreds of megabytes over a phone's connection. curl is used - rather than urllib so a retry continues rather than starting again.""" log("downloading %s" % os.path.basename(path)) rc = subprocess.call([ "curl", "-fL", "--retry", "5", "--retry-delay", "3", "--retry-connrefused", diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-run b/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-run index 624d42074..410f1654d 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-run +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-run @@ -1,25 +1,4 @@ #!/usr/bin/python3 -"""Runs a Windows program with the Proton the Steam client has chosen for it. - - droiddeck-proton-run [--window] [--prefix ID] [--verb run] [--game-id ID] [--desktop WxH] PROGRAM [ARGS...] - droiddeck-proton-run --app ID [--dir FOLDER] [--options TEXT] [PROGRAM [ARGS...]] - -The program belongs to an installed Steam game when it sits in one's folder, to a prefix when it -sits in one's drive_c, or to a non-Steam shortcut that names it; any other program gets a prefix of -its own, under the id the client gives a new shortcut. That title's CompatToolMapping entry in -config.vdf picks the tool, else the client's default for all other titles ("0"). The tool is -started as the client starts it - its toolmanifest's command line, the waitforexitandrun verb and -the STEAM_COMPAT_* environment - so its wrapper applies the game's settings, components and audio. ---window keeps the program in the desktop's own window, drawn with WineD3D on the desktop's software -GL (llvmpipe): KWin there composites in software, and the session's zink finds no device. ---prefix ID puts the program in compatdata/ID whatever it is. --verb run starts it beside what already -runs in the prefix, which waitforexitandrun would wait for; the wrapper's launch-time steps are skipped. ---game-id ID keeps the id of the launch it runs for: gamescope shows the windows that carry the id Steam launched. ---app starts one game without the Steam client (its Start without Steam): a Steam game's default -Windows launch option from appinfo.vdf when no program is given, else the program in that -shortcut's own prefix, started in --dir with the shortcut's launch options (--options). Without ---options it takes the launch options set in the client's Properties for that game or shortcut. -""" import json import os import re @@ -91,7 +70,6 @@ def mapping(root): def tools(root): - """Windows compatibility tools this runtime can start, by the name config.vdf uses.""" found = {} for base in [os.path.join(root, "compatibilitytools.d")] + list(SYSTEM_TOOLS): try: @@ -121,7 +99,6 @@ def state_default(): def choose_tool(appid, current, available): - """(tool name, why) from the client's mapping, else the session's own default.""" for key, why in ((appid, "Steam's choice for this title"), ("0", "Steam's default")): name = current.get(key) if name in available: @@ -144,7 +121,6 @@ class Target: def ancestors(path): - """(folder, the entry below it on the way to path) from the program's folder up to /.""" child, folder = path, os.path.dirname(path) while True: yield folder, os.path.basename(child) @@ -211,7 +187,6 @@ def same_file(a, b): def shortcut_entries(root): - """(appid, exe, start dir) of the client's non-Steam shortcuts and the app's added games.""" entries = [] try: tool = runpy.run_path(os.path.join(BIN_DIR, "droiddeck-steam-shortcuts")) @@ -239,7 +214,6 @@ def shortcut_entries(root): def prefix_folder(root, appid): - """The prefix folder the client made for [appid], under either spelling of the id.""" folder = os.path.join(root, "steamapps", "compatdata") try: names = sorted(os.listdir(folder)) @@ -267,7 +241,6 @@ def vdf_string(token): def steam_options(app, root): - """The launch options the client keeps for a game (localconfig.vdf) or a shortcut (shortcuts.vdf).""" try: tool = runpy.run_path(os.path.join(BIN_DIR, "droiddeck-steam-shortcuts")) account = tool["account_dir"](root) @@ -305,7 +278,6 @@ def shortcut(program, root): def own_id(program): - """The id the client gives a new shortcut: the CRC of its quoted target and its name.""" name = os.path.splitext(os.path.basename(program))[0] return zlib.crc32(('"%s"%s' % (program, name)).encode("utf-8")) | SHORTCUT_BIT @@ -337,8 +309,6 @@ def split_words(text): def split_options(text): - """A shortcut's launch options: NAME=value words before %command% go to the environment, the words - after it - or all of them, without %command% - are the program's arguments.""" words = split_words(text) if "%command%" not in words: return {}, words @@ -352,7 +322,6 @@ def split_options(text): def find_file(base, relative): - """base/relative, matching each part without regard to case as Windows does.""" path = base for part in [p for p in relative.replace("\\", "/").split("/") if p and p != "."]: exact = os.path.join(path, part) @@ -379,7 +348,6 @@ def steam_install(appid, root): def steam_launch(appid, root): - """(target, program, arguments) for a Steam game's default Windows launch option, or None.""" library, install = steam_install(appid, root) if not install or not os.path.isdir(install): return None @@ -406,7 +374,6 @@ def shortcut_target(appid, program, start, root): def steam_launches(target, program, root): - """Whether a launch option of the Steam game starts [program]: then Steam should start it.""" if target.kind != "steam": return False entry = compat()["appinfo_entries"](os.path.join(root, "appcache", "appinfo.vdf")).get(target.appid, {}) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists b/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists index 86d3f1c31..17a7418ec 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists @@ -144,8 +144,6 @@ unseed() { return 1 } -# The ledger lines whose values the registry still holds, in both views where we wrote both. A Proton -# update replaces its default_pfx system.reg but not our ledger beside it, and new prefixes copy both. present() { awk -F'\t' -v hive="$hive" ' NR == FNR { path = $1 diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session index a695fa311..7ad97e548 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session @@ -138,7 +138,6 @@ run_gamescope() ( local status_file child_status compositor_status status_file=$(mktemp "${XDG_RUNTIME_DIR:-/tmp}/droiddeck-steam-exit.XXXXXX") || exit 73 trap 'rm -f "$status_file"' EXIT - # Gamescope can return success when its primary child crashed. BL_STEAM_EXIT_FILE="$status_file" gamescope "$@" compositor_status=$? if read -r child_status < "$status_file" && [[ $child_status =~ ^[0-9]{1,3}$ ]] \ @@ -305,8 +304,6 @@ if [ -z "${BL_INSIDE:-}" ]; then echo "== gamescope: games at ${nested}x${nested_h} stretched to the ${width}x${height} output" fi fi - # Off unless Performance asks for it. Compositing the cursor here adds a second - # plane on the phone's compositor; a controller-driven cursor is the reason to turn it on. if [ "$mode" = steam ] && [ "${BL_CURSOR_COMPOSITE:-0}" = 1 ]; then export gamescope_cursor_composite=2 fi @@ -317,7 +314,6 @@ if [ -z "${BL_INSIDE:-}" ]; then fi if [ "${BL_REFRESH:-0}" -gt 0 ]; then args+=(-r "$BL_REFRESH") - # -r is the starting rate; gamescope follows the panel when it switches modes later. export GAMESCOPE_NESTED_REFRESH_FOLLOWS_HOST=1 fi [ "$mode" = steam ] && [ "${BL_STEAM_UI:-}" != desktop ] && args+=(-e) @@ -336,10 +332,8 @@ if [ -z "${BL_INSIDE:-}" ]; then args+=(--mangoapp) echo "== gamescope: mangoapp on ($(command -v mangoapp), config $MANGOHUD_CONFIGFILE)" fi - # gamescope writes the frame times the app's HUD shows here (patch 0125). mkdir -p /run/droiddeck-hud && export DROIDDECK_HUD_FRAMES=/run/droiddeck-hud/frames export BL_INSIDE=1 - # A fixed path: left to itself gamescope creates a new limiter state file on every start. rm -f "${XDG_RUNTIME_DIR:-/tmp}"/gamescope-limiter-* export GAMESCOPE_LIMITER_FILE="${XDG_RUNTIME_DIR:-/tmp}/droiddeck-gamescope-limiter" rm -f "$GAMESCOPE_LIMITER_FILE" @@ -469,7 +463,6 @@ case $mode in steam_cef_gpu=1 if [ "$steam_x64" = 1 ]; then step "checking the Steam client's arm64 parts (first run downloads them, ~1-2 min)" - # droiddeck-steam-install removes the repair request: read it first so Repair Steam covers both clients. _bl_x64_repair=0 [ -e "$HOME/.bl-steam-repair" ] && _bl_x64_repair=1 droiddeck-steam-install || steam_setup_failed $? @@ -554,7 +547,6 @@ case $mode in for _d in /proc/[0-9]*; do [ "${_d##*/}" != "$BASHPID" ] || continue _args=() - # Under FEX the program is FEX's first argument. AppId= is its own argument. while IFS= read -r -d '' _c; do _args+=("$_c"); done 2>/dev/null < "$_d/cmdline" || true [ "${#_args[@]}" -gt 0 ] || continue _name=${_args[0]} _fex=0 @@ -614,7 +606,6 @@ case $mode in _bl_rss=$_rss echo "$# processes, resident $((_rss / 1024)) MB, in zram $((_swap / 1024)) MB" } - # Only processes that loaded libblsession are the session's own and answer SIGURG. bl_session_others() { local _p _bl_session=() @@ -630,8 +621,6 @@ case $mode in _pids=("${_bl_pids[@]}") fi } - # Same protocol as SessionSuspendController: an empty ack when the page-out starts, a byte when it is done. - # A process whose SIGURG handler was replaced never acks, so it is not waited for after the first 2 s. bl_zram_wait() { local _p _left _start=$SECONDS while [ $((SECONDS - _start)) -lt 12 ]; do @@ -659,9 +648,6 @@ case $mode in bl_zram_pick "$_which" step "zRAM: done ($(bl_zram_usage "${_pids[@]}"))" } - # Proton starts its reaper before the game window; compressing during launch stalls it. - # Desktop Steam has no focus.json, so it still uses the game process as its trigger. - # A configured RAM cap can still force compression while a game is launching. bl_game_focused() { if [ "${BL_DESKTOP:-}" = 1 ] || [ -z "${BL_LAUNCH_DIR:-}" ]; then return 0 @@ -690,7 +676,6 @@ case $mode in bl_zram_scan if bl_cap_due; then if bl_over_cap; then - # A cap below the game's working set cannot be met; paging out again every 30 s only churns. if [ "$_bl_capped" -lt 0 ] || [ $((SECONDS - _bl_capped)) -ge "$_bl_backoff" ]; then bl_zram_usage "${_bl_pids[@]}" > /dev/null if [ "$_bl_rss" -ge $((512 * 1024)) ]; then @@ -879,14 +864,11 @@ case $mode in until [ "$(_bl_focused)" = 769 ] || [ "$SECONDS" -ge "$_bl_end" ]; do sleep 1 done - # Shortcut CGameIDs are 64-bit; Steam app ids fit in 32 bits and keep the old short delay. [ "${#game}" -gt 10 ] && shortcut=1 [ -n "$shortcut" ] || sleep 4 echo "== steam: interface up; launching the requested game $game" _bl_hand_off [ -n "$shortcut" ] || return 0 - # Steam can drop a shortcut launch it gets before the shortcut's overview exists (large libraries). - # A launch can be running before it creates its first window. while [ "$(_bl_focused)" = 769 ] && [ "$waited" -lt 20 ]; do sleep 1 waited=$((waited + 1)) @@ -978,13 +960,11 @@ case $mode in exit 69 fi - # The UI bridge launches SteamVR, which needs the guest agent's library environment. _bl_compat_env=(env -u LD_LIBRARY_PATH -u STEAM_RUNTIME -u STEAM_STEAMRT_RECURSION_GUARD) [ "${LD_LIBRARY_PATH+x}" ] && _bl_compat_env+=("LD_LIBRARY_PATH=$LD_LIBRARY_PATH") [ "${STEAM_RUNTIME+x}" ] && _bl_compat_env+=("STEAM_RUNTIME=$STEAM_RUNTIME") [ "${STEAM_STEAMRT_RECURSION_GUARD+x}" ] && _bl_compat_env+=("STEAM_STEAMRT_RECURSION_GUARD=$STEAM_STEAMRT_RECURSION_GUARD") export STEAM_RUNTIME=1 - # A containerized client re-executes itself through pressure-vessel-wrap (bwrap) unless told it is already inside. export STEAM_STEAMRT_RECURSION_GUARD=1 export LD_LIBRARY_PATH=$steam_root/steamrtarm64:$steam_root/steamrtarm64/panorama${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} export SDL_VIDEO_X11_DGAMOUSE=0 @@ -1238,7 +1218,6 @@ case $mode in compat_restarts=0 compat_settled() { local app m flags r card=/mnt/droiddeck-sd - # The arm64 card library may hold that client's copy of the same title. [ "$steam_x64" = 1 ] && card=/mnt/droiddeck-sd/x86-64 while read -r app; do [ -n "$app" ] || continue @@ -1355,7 +1334,6 @@ case $mode in lossless_left=0 elif ! pgrep -x wineserver >/dev/null 2>&1; then ( - # The probe reaches the x86-64 client through the arm64 client's steamclient.so, as the import watcher does. if [ "$steam_x64" = 1 ]; then export BL_STEAMCLIENT=$XDG_DATA_HOME/Steam/steamrtarm64/steamclient.so; fi answer=$(droiddeck-steam-games probe "$steam_root" 993090 2>/dev/null) || exit 0 case $answer in @@ -1415,7 +1393,6 @@ case $mode in BL_COMPAT_TOOL=$_bl_x64_tool droiddeck-steam-shortcuts "$steam_root" "$BL_ADDED_GAMES" || true fi fi - # The x86-64 client rewrote config.vdf from memory on exit, over the live passes' mapping. if [ "$steam_x64" = 1 ]; then [ -z "${_bl_x64_tool:-}" ] || BL_COMPAT_TOOL=$_bl_x64_tool steam-compatibility "$steam_root" --x64 >/dev/null 2>&1 || true diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat index 93ca33883..48b77da7f 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat @@ -233,10 +233,6 @@ SWITCH_JS = r""" })() """.replace("__BINDING__", SWITCH_BINDING) SESSION_SELECT = "/usr/bin/steamos-session-select" -# Steam opens its Steam Controller welcome tour at every start while the client setting -# controller_show_ibex_tour (CMsgClientSettings field 140014) is on, and only a finished tour turns it -# off; closing it leaves it on. The app's pad is an emulated controller, so the tour never applies. -# A pending firmware update still opens Steam's updater, which checks the update priority instead. TOUR_JS = r""" (() => { let key = 140014 * 8; @@ -247,13 +243,6 @@ TOUR_JS = r""" return "off"; })() """ -# Steam keeps the Quick Access Frame Limit as the client setting gamescope_app_target_framerate -# (CMsgClientSettings field 21002) and offers the values gamescope's display state lists in -# active_display_info.supported_frame_rates. When the panel's refresh rate changes, a remembered -# limit that is not one of the new values leaves the slider on its first value while the game runs -# at the old limit. The limit is kept when it is still offered and moved to the highest value -# otherwise. Choices are read once they have been unchanged for a moment, since a mode switch can -# report a passing rate first. FRAME_LIMIT_JS = r""" (() => { if (window.__droiddeckFrameLimit) return "present"; @@ -426,7 +415,6 @@ VR_JS = r""" def launch_steamvr(): - """Keep downloads/startup off the Steam UI bridge's event loop.""" say("SteamVR library launch: using the DroidDeck ARM64 helper") state = os.path.join(os.environ.get("XDG_RUNTIME_DIR", "/tmp"), "droiddeck-steamvr") try: @@ -780,7 +768,6 @@ class Helper: pass def sync_frame_limit(self): - # Steam can still be loading its settings and gamescope state when the helper attaches. if self.frame_limit_state in ("installed", "present") or time.monotonic() < self.frame_limit_retry: return try: @@ -806,8 +793,6 @@ class Helper: self.finish_chooser(ident, None) return command = [sys.executable, os.path.realpath(__file__), "--choose-file"] - # Gamescope shows the dialog over the client by the AppId on a reaper above it; the x86-64 client has no - # native reaper, so the arm64 client's is borrowed when it is installed. data = os.environ.get("XDG_DATA_HOME") or os.path.join(os.path.expanduser("~"), ".local", "share") for root in (self.steam_root, os.path.join(data, "Steam")): reaper = os.path.join(root, "steamrtarm64", "reaper") @@ -879,7 +864,6 @@ class Helper: try: page.evaluate(TOUR_JS) except RuntimeError: - # The optional tour setting must not block the compat bridge. pass if not self.steamos: page.call("Runtime.addBinding", {"name": SWITCH_BINDING}) @@ -916,7 +900,6 @@ class Helper: prior_auto = state.get("auto", {}) if not isinstance(prior_auto, dict): prior_auto = {} - # "native" from earlier builds also lists dual-platform games nobody chose to run natively. prior_native = state.get("native_choices", ()) native_choices = set(prior_native if isinstance(prior_native, (list, tuple, set)) else ()) picked = COMPAT["picked_apps"](state) @@ -948,7 +931,6 @@ class Helper: else: for app in set(native) | native_choices: if app in raw_mapping: - # The client shows an explicit native choice as the tool it inherits, and writes config.vdf late. if raw_mapping[app] == "" or current.get(app) not in self.known: current[app] = raw_mapping[app] elif app in prior_auto or app in native_choices or app in picked and ( @@ -981,7 +963,6 @@ class Helper: COMPAT["write_json"](state_file, state) def ensure_tools(self): - """Proton, FEX and the runtimes FEX needs are fetched as soon as the client is up, never left pending.""" now = time.monotonic() if self.tools_checked is not None and now - self.tools_checked < TOOL_CHECK_SECONDS: return @@ -1007,7 +988,6 @@ class Helper: return isinstance(request.get("seq"), int) and request["seq"] > int(state.get("applied", 0) or 0) def drain(self): - """The page's events; a switch to the desktop is acted on as it comes.""" events = self.page.drain() if any(called(e, SWITCH_BINDING) for e in events): switch_to_desktop() 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 68b0a2a3a..88d60dde6 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-desktop-ui +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-desktop-ui @@ -1,5 +1,4 @@ #!/usr/bin/python3 -"""Honor a desktop launch even when Steam remembers Big Picture from its last session.""" import os from pathlib import Path import runpy diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-games b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-games index ca9c1a1cc..18f2b3894 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-games +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-games @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -"""Resolve added games against the signed-in Steam client's licenses.""" import ctypes as C import json import os @@ -66,8 +65,6 @@ def source_stamp(game): def store_game(game): - """Games a store client installed belong to that store: the Steam copy of the same title is - another license with its own files, and an import would let Steam update over the store's.""" return bool(game.get("store")) @@ -150,7 +147,6 @@ def probe(steam_root, ids): def game_running(): - """Whether a Proton game is up: a wineserver is there for as long as any prefix is in use.""" try: pids = [p for p in os.listdir("/proc") if p.isdigit()] except OSError: @@ -173,8 +169,6 @@ def downloads_idle(steam_root): def request_restart(steam_root): - """The client reads manifests and shortcuts only at start, so an import needs one restart; - the session's loop runs the importer between the exit and the next start.""" Path(os.environ.get("HOME", "/root"), ".bl-restart-once").touch() command = shlex.split(os.environ.get("BL_STEAM_SHUTDOWN", "")) or [str(Path(steam_root) / "steamrtarm64/steam"), "-shutdown"] subprocess.run(command, @@ -182,7 +176,6 @@ def request_restart(steam_root): def ask_restart(names, timeout=600, clock=None, sleep=None): - """The app asks whether to restart the client now; anything but "now" leaves the import to the next start.""" clock, sleep = clock or time.monotonic, sleep or time.sleep folder = Path(os.environ.get("HOME", "/root"), ".cache/droiddeck") ask, answer = folder / "steam-import.json", folder / "steam-import.answer" @@ -296,8 +289,6 @@ def watch(steam_root, listing): def installed_build(folder, appid, installdir): - """The build another Steam client recorded for these files, so Steam updates from it - instead of verifying every file.""" try: text = (folder.parent / WINNATIVE / f"appmanifest_{appid}.acf").read_text() install = re.search(r'("installdir"\s*")([^"\\]*)(")', text, re.I) @@ -425,8 +416,6 @@ def seed_install(steam_root, game, appid): def release(steam_root, game, stale, reason): - """Undoes the imports of the game's folder as the Steam apps [stale] picks. Only our symlink and - manifest go; the folder stays where it is.""" steamapps = Path(steam_root) / "steamapps" sources = {game.get("folder", ""), legacy_path(game.get("folder", ""))} for target in sorted((steamapps / "common").glob("DroidDeck-*")): diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-install b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-install index 45bd96c25..f3098fd3e 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-install +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-install @@ -227,7 +227,6 @@ 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" -# sdk64 is where x86_64 Steamworks looks for steamclient.so; arm64 builds look in sdkarm64. ln -sfn "$steam_root/linux64" "$HOME/.steam/sdk64" ln -sfn "$steam_root/linuxarm64" "$HOME/.steam/sdkarm64" ln -sfn "$steam_root/steamrtarm64" "$HOME/.steam/binarm64" diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-language b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-language index f892e8e22..f29c91d29 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-language +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-language @@ -1,18 +1,4 @@ #!/usr/bin/python3 -"""Sets the Steam client's interface language to the app's, before the client starts. - - droiddeck-steam-language - - is one of the client's language names (english, spanish, latam, japanese, koreana, -schinese, tchinese, ...). The client reads it from /.steam/registry.vdf, under -HKCU/Software/Valve/Steam and again under .../Steamsteamglobal: that second key is copied over the -first at every start, so setting only the first is undone by the old value. Its update window -reads the first key alone, and nothing reads LANG. exportedsettings.json is what the client writes -beside them when its own setting changes. The session also passes -language, which the interface -honours over the file; the file is still what the update window shows itself in. - -Run only while the client is stopped: it keeps the registry in memory and writes it back on exit. -""" import json import os import re @@ -63,7 +49,6 @@ def serialize(tokens): def block_end(tokens, start): - """Index of the '}' closing the block whose '{' is at start.""" depth = 0 for i in range(start, len(tokens)): if tokens[i] == "{": @@ -76,8 +61,6 @@ def block_end(tokens, start): def child(tokens, start, end, name): - """Index of the '{' of the block called name directly inside tokens[start:end], or None. - Keys are matched without case, as the client does.""" i = start while i < end: if tokens[i] in ("{", "}"): @@ -93,7 +76,6 @@ def child(tokens, start, end, name): def ensure_block(tokens, path): - """The '{' of the block at path, creating it and any missing parent at the end of its parent.""" start, end = 0, len(tokens) for depth, name in enumerate(path): found = child(tokens, start, end, name) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-library b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-library index 171400b31..6bd6a8549 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-library +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-library @@ -9,9 +9,6 @@ MAIN_LABEL = "Internal Storage" def register(steam_root, library, label): - """Adds one library folder to the client's list; the client then offers it as an install - location as well as reading the games already in it. Registered even when empty: a new user - should be able to install straight into it.""" steamapps = os.path.join(library, "steamapps") if not os.path.isdir(steamapps): return 0 @@ -58,8 +55,6 @@ RENAMED = (("/mnt/bannerlator-sd", "/mnt/droiddeck-sd"),) def rename(steam_root, old, new): - """Moves a library registered under an earlier mount point to the current one, keeping its - entry (content id, app sizes) rather than registering it anew.""" path = os.path.join(steam_root, "steamapps", "libraryfolders.vdf") if not os.path.isfile(path): return @@ -77,8 +72,6 @@ def rename(steam_root, old, new): def forget(steam_root, library): - """Removes a library folder earlier builds registered and this one no longer binds, so the - client does not keep offering a location that is not there.""" path = os.path.join(steam_root, "steamapps", "libraryfolders.vdf") if not os.path.isfile(path): return @@ -107,10 +100,6 @@ def forget(steam_root, library): def relabel(steam_root, target, label): - """Names a library for what it is. The client writes its own entry with an empty label and - falls back to showing the mount point - "Local Drive (/)" - so the install dialog gives no - clue which of the two places is the phone and which is the card. A library registered on an - earlier run keeps whatever label it was given then, so both are named here every time.""" path = os.path.join(steam_root, "steamapps", "libraryfolders.vdf") if not os.path.isfile(path): return diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shaders b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shaders index da93ebec4..ccb8d1dc8 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shaders +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shaders @@ -1,12 +1,4 @@ #!/usr/bin/python3 -"""Sets the Steam client's shader pre-caching (Settings > Downloads), before the client starts. - - droiddeck-steam-shaders <0|1> ... - -The client stores the toggle as InstallConfigStore/Software/Valve/Steam/ShaderCacheManager/ -DisableShaderCache in config/config.vdf: "1" when off, "0" when on. It keeps the setting in memory -and writes config.vdf back on exit, so this runs only while the client is stopped. The launcher -passes a value only after its own toggle changes; otherwise the client's toggle stays the user's.""" from pathlib import Path import runpy import sys diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shortcuts b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shortcuts index 0910cf6fa..9ad14d838 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shortcuts +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shortcuts @@ -16,7 +16,6 @@ TAG = "droiddeck-app" def account_dir(steam_root): - """The most recent account's userdata folder, else the only one that is not 0.""" users = os.path.join(steam_root, "config", "loginusers.vdf") recent, best = None, None try: @@ -243,9 +242,6 @@ ART_SLOTS = (("p", "p"), ("header", ""), ("hero", "_hero"), ("logo", "_logo")) def install_art(acct, games): - """Copies each game's art into the client's grid folder under the names it looks for. A file - the client already has that this script did not write (art the user set in the client) is - left alone; ours are replaced when the source changed and removed when the game lost its art.""" grid = os.path.join(acct, "config", "grid") os.makedirs(grid, exist_ok=True) manifest_path = os.path.join(grid, ART_MANIFEST) @@ -354,7 +350,6 @@ def main(): if x64_tool: protect = {compat.first_key(os.path.join(tools, e, "compatibilitytool.vdf"), ["compatibilitytools", "compat_tools"]) or e: e for e in (os.listdir(tools) if os.path.isdir(tools) else [])} - # Its own state: the arm64 client's state.json lists which of that client's titles follow its default. own = {"default": x64_tool, "directory": os.path.join(os.path.dirname(os.path.abspath(steam_root)), "droiddeck-compat"), "linux_tool": "droiddeck-direct"} 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 af809d124..c1c2cc6eb 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-ui-scale +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-ui-scale @@ -1,16 +1,4 @@ #!/usr/bin/python3 -"""Initialize Steam's own display settings once; subsequent choices belong to Steam. - -The UI scale, and native resolution: the client treats our gamescope as an external display and, -unless "Enable Native Resolution on External Displays" is on, draws itself at 1920x1080 on any -larger output - a 2340x1080 session came up as 16:9 with bars. The session's size is already the -user's Resolution choice, so the client should use all of it. - -And the screensaver over a running game. The client's defaults (steamui.so's settings descriptor) -show screensavers while a game runs, after 300 s on AC power with no button pressed - and a stick or -trigger held is no press. Red Dead Redemption 2's cutscenes run longer than that: the slideshow -covered the game, and the button that woke it reached the game as well (B asked to quit to the -desktop). Customization > Screensaver > "Show screensavers when playing a game" still turns it on.""" import math import os from pathlib import Path @@ -116,8 +104,6 @@ def seed_text(text, scale): def seed_value(text, path, value): - """InstallConfigStore/ = value, unless the client already has a value there (the - user's), adding only the blocks that are missing.""" root = parse(text).get('InstallConfigStore') if not isinstance(root, Block): raise ValueError("missing InstallConfigStore") @@ -170,9 +156,6 @@ def initialize(steam_root, width, height): def seed_once(steam_root, marker_name, seed, message): - """Applies seed to config.vdf the first time only: the marker keeps a later choice - including - a return to the client's own default, which the client may store by dropping the key - the - user's.""" config = Path(steam_root) / 'config/config.vdf' marker = config.with_name(marker_name) if marker.exists(): diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-x64 b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-x64 index f6d7d1d92..2db8dce5a 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-x64 +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-x64 @@ -3,7 +3,6 @@ set -eu ROOTFS_URL=https://github.com/Droid-Deck/DroidDeck-Components/releases/download/fex-rootfs-ubuntu-24.04-2026-08-11/Ubuntu_24_04.sqsh ROOTFS_SHA256=2854b06d3ff1b8f6e526135bfb6dd5b7b30ab3ab73e79ae933a3d9fed959a178 CDN=https://client-update.fastly.steamstatic.com -# Must match the -clientbeta droiddeck-session starts this client with (steam_channel). X64_CHANNEL=steamdeck_publicbeta MANIFEST=steam_client_${X64_CHANNEL}_steamrt64 @@ -78,7 +77,6 @@ x64_sync_client() { rm -rf "$work" } -# The client rewrites config.vdf from memory as it exits: the mapping is written only while no x86 client runs. x64_map_direct() { local mode=write [ "${1:-}" = --live ] && mode=live @@ -317,7 +315,6 @@ print("droiddeck-steam-x64: %d Linux title(s) on DroidDeck direct (%d on the new PY } -# A resumable download that reports every tenth of its size as a step. x64_fetch() { local url=$1 out=$2 label=$3 total pid rc=0 last=-1 pct total=$(curl -fsSIL "$url" 2>/dev/null | tr -d '\r' | awk 'tolower($1) == "content-length:" { v = $2 } END { print v + 0 }') @@ -336,11 +333,9 @@ x64_fetch() { x64_prepare() { fex_ready || { step "FEX could not be installed; see $LOG"; exit 1; } step "FEX ready ($FEX_NOW, $FEX_FROM)" -# The client's opt-in to its SteamRT3 build; without it, it re-executes through steam.sh, whose checks fail under proot. mkdir -p "$STEAM_X64" && : > "$STEAM_X64/.steam-enable-steamrt64-client" if [ ! -f "$ROOTFS/.droiddeck-complete" ]; then - # Resumes a part-done download; a finished one answers 416, which is not a failure here. x64_fetch "$ROOTFS_URL" "$X64_HOME/rootfs.sqsh" "downloading FEX's Ubuntu 24.04 x86 rootfs" || { rc=$? [ "$rc" = 22 ] && [ -s "$X64_HOME/rootfs.sqsh" ] || { step "rootfs download failed (curl $rc)"; exit 1; } @@ -356,13 +351,9 @@ if [ ! -f "$ROOTFS/.droiddeck-complete" ]; then touch "$ROOTFS/.droiddeck-complete" fi echo "== x86-64 rootfs: $(sed -n 's|.*/download/\([^/]*\)/.*|\1|p' <<<"$ROOTFS_URL") (sha256 ${ROOTFS_SHA256:0:12})" >&2 -# FEX falls back to the guest's own /etc/ld.so.preload (arm64 libraries) when the rootfs has none. The x86 -# session shim provides System V IPC, which Android kernels lack and the client's tier0 needs to start. : > "$ROOTFS/etc/ld.so.preload" for lib in libblsession libfakeinput; do have=0 - # 32-bit libfakeinput crashes SDL2 games (a NULL udev attribute reaches strdup), so only the x86_64 copy - # is preloaded, by full path: with $LIB, FEX falls through to the session's own i386 copy. arches="x86_64 i386"; [ "$lib" = libfakeinput ] && arches=x86_64 rm -f "$ROOTFS/usr/local/lib/i386-linux-gnu/libfakeinput.so" for arch in $arches; do @@ -439,7 +430,6 @@ if [ -f "$scout/steam-runtime.tar.xz" ] && fi [ -L "$STEAM_X64/steamapps" ] && rm -f "$STEAM_X64/steamapps" mkdir -p "$STEAM_X64/steamapps" -# FEX looks in the rootfs first: an empty dev/dri there hides the session's KGSL node, which x86 programs spin on. mkdir -p "$ROOTFS/dev/dri" mkdir -p "$X64_HOME/.steam" ln -sfn "$STEAM_X64" "$X64_HOME/.steam/root" @@ -448,9 +438,7 @@ ln -sfn "$STEAM_X64/ubuntu12_32" "$X64_HOME/.steam/bin32" ln -sfn "$STEAM_X64/ubuntu12_64" "$X64_HOME/.steam/bin64" ln -sfn "$STEAM_X64/linux32" "$X64_HOME/.steam/sdk32" ln -sfn "$STEAM_X64/linux64" "$X64_HOME/.steam/sdk64" -# Valve's Steam Runtime container needs user namespaces and a session-bus launcher; steamrt-shim stands in for both. droiddeck-steamrt-shim "$STEAM_X64/steamrt64" "$X64_HOME/steamrt-shim" || rm -rf "$X64_HOME/steamrt-shim" -# Valve's platform libraries ship without soname links (pressure-vessel makes them); ldconfig adds them here. srt_libs=$X64_HOME/steamrt-libs rm -rf "$srt_libs" mkdir -p "$srt_libs" @@ -460,13 +448,11 @@ for d in "$STEAM_X64"/steamrt64/pv-runtime/steam-runtime-steamrt/steamrt3c_platf for f in "$d"/*.so*; do [ -e "$srt_libs/$(basename "$f")" ] || ln -s "$f" "$srt_libs/"; done done FEX_ROOTFS=$ROOTFS /usr/local/bin/FEX "$ROOTFS/usr/sbin/ldconfig.real" -n "$srt_libs" >>"$LOG" 2>&1 || step "ldconfig on $srt_libs failed" -# The rootfs's HarfBuzz needs a newer FreeType than some games bundle (Counter-Strike 2): use Valve's runtime one. sniper_compat=$X64_HOME/sniper-compat rm -rf "$sniper_compat" mkdir -p "$sniper_compat" hb=$(ls "$srt_libs"/libharfbuzz.so.0.* 2>/dev/null | head -n 1) if [ -n "$hb" ]; then ln -s "$hb" "$sniper_compat/libharfbuzz.so.0"; else step "warning: no HarfBuzz in Valve's runtime for the newer-runtime titles"; fi -# LD_LIBRARY_PATH beats the system folders whatever its order: Valve's older glibc must not shadow the rootfs's. for f in "$srt_libs"/*; do n=$(basename "$f") if [ -e "$ROOTFS/usr/lib/x86_64-linux-gnu/$n" ] || [ -e "$ROOTFS/lib/x86_64-linux-gnu/$n" ]; then rm -f "$f"; fi @@ -540,7 +526,6 @@ RUN fi x64_share x86-64 x64_map_direct - # Each wrapper execs droiddeck-x64-arm-proton through an arm64 binary, which FEX hands straight to the CPU. arm_tools=$ARM_STEAM/compatibilitytools.d mkdir -p "$X64_HOME/arm64" cp -f /usr/bin/env "$X64_HOME/arm64/env" @@ -552,7 +537,6 @@ RUN for src in "$arm_tools"/*/; do src=${src%/}; name=${src##*/} [ -f "$src/compatibilitytool.vdf" ] && [ -f "$src/toolmanifest.vdf" ] || continue - # A droiddeck-fex wrapper made the arm64 client install Valve's FEX when this client probed its tools after logon. case $name in droiddeck-direct|droiddeck-fex) continue ;; esac dst=$STEAM_X64/compatibilitytools.d/$name mkdir -p "$dst" @@ -585,7 +569,6 @@ VDF chmod 755 "$dst/run" done x64_sync_login - # Each client keeps its own libraries; written only while no x86 client runs, as it rewrites these on exit. if ! pgrep -f 'steamrt64/steam( |$)' >/dev/null 2>&1; then listing_ok=0 if BL_X64_SHARED=$X64_HOME/shared/arm64 python3 - "$STEAM_X64" "${BL_X64_STEAM_LIBRARIES:-}" "$STEAM_X64/config/libraryfolders.vdf" "$STEAM_X64/steamapps/libraryfolders.vdf" <<'PY'; then @@ -634,7 +617,6 @@ PY step "could not add the card to the x86-64 client's library folders" fi fi - # default-tool is the default this script set: titles still on it move along; a user's choice is kept. default=$(x64_default_dir) if [ -f "$default/compatibilitytool.vdf" ] && [ -f "$STEAM_X64/config/config.vdf" ] && ! pgrep -f 'steamrt64/steam( |$)' >/dev/null 2>&1; then @@ -709,7 +691,6 @@ s = s[:steam.end()] + movie.group(0) + s[steam.end():] open(path, "w", encoding="utf-8", errors="surrogateescape").write(s) PY fi - # FEX takes its rootfs from FEXServer, and one started without FEX_ROOTFS serves none: stop it while no x86 client runs. if ! pgrep -f 'steamrt64/steam( |$)' >/dev/null 2>&1; then FEX_ROOTFS=$ROOTFS /usr/local/bin/FEXServer -k >/dev/null 2>&1 || true fi @@ -724,7 +705,6 @@ x64_webhelper_gpu() { } x64_env() { -# droiddeck-x64-arm-proton restores this arm64 environment before Proton runs. export -p | grep -v -E '^declare -x (PWD|OLDPWD|SHLVL|_)=' > "$X64_HOME/arm-session.env.new" || true mv -f "$X64_HOME/arm-session.env.new" "$X64_HOME/arm-session.env" unset LD_LIBRARY_PATH LD_PRELOAD STEAM_RUNTIME STEAM_RUNTIME_PREFER_HOST_LIBRARIES @@ -746,7 +726,6 @@ if x64_gpu && [ "${BL_X64_CLIENT_SOFTWARE:-0}" != 1 ]; then else unset FEX_THUNKCONFIG $gpu_vars export LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe -# Over DRI3 the X server hands out the GPU node, and Mesa's software path loops on KMS dumb buffers on it. export LIBGL_DRI3_DISABLE=1 fi [ "${BL_X64_FP_OFF:-0}" = 1 ] && export PROOT_FP_OFF=1 @@ -759,12 +738,9 @@ export FEX_ROOTFS=$ROOTFS export STEAM_EXTRA_COMPAT_TOOLS_PATHS= export STEAM_RUNTIME=1 [ -d "$X64_HOME/steamrt-shim" ] && export STEAM_RUNTIME_STEAMRT=$X64_HOME/steamrt-shim -# The client starts commands via steam-runtime-launch-client; Valve's needs a session-bus service, the shim's runs them. [ -d "$X64_HOME/steamrt-shim/bin" ] && export PATH=$X64_HOME/steamrt-shim/bin:$PATH export LD_LIBRARY_PATH=$STEAM_X64/steamrt64:$STEAM_X64/steamrt64/panorama:$X64_HOME/steamrt-libs -# After an update the client re-executes itself through pressure-vessel-wrap (bwrap) unless this guard is set. export STEAM_STEAMRT_RECURSION_GUARD=1 -# proot is a tracer: tier0 takes it for a debugger and breaks into it with int3 on its first failed assertion. export BL_WEBHELPER_HIDE_TRACER=1 } @@ -774,7 +750,6 @@ x64_manifest_version() { awk -F'"' '$2 == "version" { print $4; exit }' "$STEAM_X64/package/steam_client_${chan}_$1.manifest" 2>/dev/null || echo "?" } -# What x86 GL and Vulkan look like through FEX's thunks, for a session whose menus failed on the GPU. x64_gpu_probe() { local fex=${BL_FEX:-/usr/local/bin/FEX} echo "== x86-64 GPU probe (x86 glxinfo and vulkaninfo through FEX):" @@ -786,8 +761,6 @@ x64_gpu_probe() { ) | sed 's/^/ /' } -# Steam's menus come from the web helper; when its GPU process keeps exiting, Chromium gives up on the GPU and -# the menus never appear. Says so once in the session log, with the probe above. x64_gpu_watch() { local log=$STEAM_X64/logs/console-linux.txt from n codes from=$(( $(wc -l 2>/dev/null < "$log" || echo 0) + 1 )) @@ -801,7 +774,6 @@ x64_gpu_watch() { x64_arm_webhelper_cpu_supported() { local status=0 - # Valve's native ARM web helper contains LSE atomics; missing CPU data cannot establish incompatibility. awk -F: ' $1 ~ /^[[:space:]]*Features[[:space:]]*$/ { if ($2 !~ /(^|[[:space:]])atomics([[:space:]]|$)/) unsupported = 1 @@ -814,8 +786,6 @@ x64_arm_webhelper_cpu_supported() { x64_exec() { x64_env cd "$STEAM_X64" - # The client checksums its files at start and reinstalls over edited ones, so the session shim (preload/net.c) - # swaps the web helper at exec. The arm64 helper only comes up against a client of the same build. if [ "${BL_X64_X86_WEBHELPER:-0}" != 1 ] && [ -x /usr/local/bin/droiddeck-x64-arm-webhelper ] && [ -x "$ARM_STEAM/steamrtarm64/steamwebhelper" ] && x64_arm_webhelper_cpu_supported; then local chan x86_build arm_build @@ -829,7 +799,6 @@ x64_exec() { fi fi if [ -z "${BL_X64_ARM_WEBHELPER:-}" ] && x64_webhelper_gpu; then - # Not --in-process-gpu (the preload's default): a GPU crash there takes the whole helper down. export BL_WEBHELPER_ARGS="--ignore-gpu-blocklist --use-gl=angle --use-angle=gl --ozone-platform=x11" [ -x /usr/local/bin/droiddeck-x64-webhelper ] && export BL_X64_ARM_WEBHELPER=/usr/local/bin/droiddeck-x64-webhelper elif [ -z "${BL_X64_ARM_WEBHELPER:-}" ]; then @@ -860,7 +829,6 @@ x64_share() { step "could not share the games between the clients" } -# Steam renews its token every few days and refuses old copies: copy the newest every start, never while a client runs. x64_sync_login() { pgrep -f 'steamrt64/steam( |$)' >/dev/null 2>&1 && return 0 pgrep -f 'steamrtarm64/steam( |$)' >/dev/null 2>&1 && return 0 @@ -996,7 +964,6 @@ if [ -n "${BL_FEXSERVER_LOG:-}" ]; then FEX_ROOTFS=$ROOTFS "$(dirname "${BL_FEX:-/usr/local/bin/FEX}")/FEXServer" -f -p 600 >"$BL_FEXSERVER_LOG" 2>&1 & sleep 1 fi -# The client's connection status comes from NetworkManager on the system bus; a desktop has no system bus. x64_bus= x64_nm= x64_pin= x64_cleanup() { [ -n "$x64_pin" ] && kill "$x64_pin" 2>/dev/null @@ -1035,7 +1002,6 @@ BUSCONF step "network: the system bus did not come up; the client will show no connection" fi fi -# Left by a killed client: .crash opens the update UI on its spinner; stale Singleton* locks can blank the UI. if ! pgrep -f 'steamrt64/steam( |$)' >/dev/null 2>&1; then for f in "$STEAM_X64/.crash" "$STEAM_X64/steam.pid" "$STEAM_X64/config/htmlcache/SingletonLock" \ "$STEAM_X64/config/htmlcache/SingletonCookie" "$STEAM_X64/config/htmlcache/SingletonSocket"; do @@ -1053,7 +1019,6 @@ if [ -n "${BL_CLIENT_CPUS:-}" ] && command -v taskset >/dev/null 2>&1; then x64_pin=$! fi for attempt in 1 2 3 4 5; do - # The branch on disk must match -clientbeta before every start, or the client loops on rc=42. echo "$X64_CHANNEL" > "$STEAM_X64/package/beta" step "starting the x86-64 SteamRT3 client under FEX (attempt $attempt): $*" rc=0 diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamrt-shim b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamrt-shim index feb4c5711..725db479d 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamrt-shim +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamrt-shim @@ -1,7 +1,4 @@ #!/bin/bash -# droiddeck-steamrt-shim -# Builds from the Steam Runtime container the client carries and exits 0, or removes it and exits 1 -# when the client carries none. set -eu client=$1 shim=$2 @@ -25,13 +22,11 @@ for e in "$srt_real"/bin/*; do [ -e "$e" ] || continue ln -s "$e" "$shim/bin/$(basename "$e")" done -# The client's bwrap probe (steam-runtime-check-requirements) needs user namespaces, which no Android app has. rm -f "$shim/bin/steam-runtime-check-requirements" printf '#!/bin/sh exit 0 ' > "$shim/bin/steam-runtime-check-requirements" chmod 755 "$shim/bin/steam-runtime-check-requirements" -# Valve's launcher asks a service on the session bus; proot is already the container, so the command runs here. rm -f "$shim/bin/steam-runtime-launch-client" cat > "$shim/bin/steam-runtime-launch-client" <<'LAUNCH' #!/bin/sh diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr index 1de7cb89e..176f92aad 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr @@ -26,7 +26,6 @@ quest_settings() { configure_graphics() { case "$(cat /sys/class/kgsl/kgsl-3d0/gpu_model 2>/dev/null)" in Adreno840*) - # The session's forced sysmem mode loses the Vulkan device in SteamVR. export TU_DEBUG="$( IFS=', ' set -f @@ -41,8 +40,6 @@ configure_graphics() { esac } -# Any OpenXR probe (Steam runs one through Proton at every client start) starts vrserver -# while a runtime is registered, so SteamVR is registered only during a DroidDeck VR session. unregister() { if grep -qF "\"$steamvr\"" "$HOME/.config/openvr/openvrpaths.vrpath" 2>/dev/null; then rm -f "$HOME/.config/openvr/openvrpaths.vrpath" @@ -78,7 +75,6 @@ stop() { case "${1:-status}" in launch) - # Library/URL entry point: use the same installer and start path as the app. exec 8>"$state/launch.lock" flock -n 8 || exit 0 if ! installed; then @@ -221,7 +217,6 @@ JSON quest_settings apply || exit 1 startup_handed_off=0 trap '[ "$startup_handed_off" = 1 ] || quest_settings restore' EXIT - # The pinned Frame package's CVP constructor exits on Quest's unsupported SoC. cvp=$steamvr/drivers/cv/bin/linuxarm64/libArcturusPerception.so if [ -f "$cvp" ]; then disabled=$cvp.droiddeck-disabled diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr-flat b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr-flat index a07462d07..ee061811a 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr-flat +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr-flat @@ -1,10 +1,6 @@ #!/bin/sh -# Exit 0 when this game run should open as a SteamVR panel: SteamVR is running and Steam -# lists no VR support for the app, optional or required. Under SteamVR, Steam stacks a flat game's window behind -# its own, so without the panel the game plays unseen. [ "${1:-}" = waitforexitandrun ] || exit 1 [ -z "${DROIDDECK_STEAMVR_PANEL:-}" ] || exit 1 -# Non-Steam shortcuts run with SteamAppId=0 and their CGameID in SteamGameId. app=${SteamAppId:-} [ "$app" = 0 ] && app=${SteamGameId:-} case $app in '' | 0 | *[!0-9]*) exit 1 ;; esac diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr-game b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr-game index 0b7d40538..6d2ca116b 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr-game +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr-game @@ -6,13 +6,11 @@ pgrep -x vrserver >/dev/null 2>&1 || { echo 'Start SteamVR before launching a game with droiddeck-steamvr-game.' >&2 exit 1 } -# droiddeck-steamvr-flat skips a run that a launch option already wrapped. export DROIDDECK_STEAMVR_PANEL=1 vrdir=${DROIDDECK_VR_DIR:-/usr/local/lib/droiddeck-vr} export VK_LAYER_PATH="$vrdir/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~}" -# Quest's guest GPU images use Vulkan sharing rather than DMA-BUF modifiers. if [ -S "/run/droiddeck/droiddeck-xr-frames" ]; then export gamescope_vr_use_modifiers=${gamescope_vr_use_modifiers:-false} fi diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr-settings b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr-settings index 28f93cf97..723a2b6d5 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr-settings +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr-settings @@ -1,5 +1,4 @@ #!/usr/bin/python3 -"""Preserve SteamVR preferences across Quest's required compositor overrides.""" import json import os from pathlib import Path diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-store-launch b/tools/linuxfs/overlay/usr/local/bin/droiddeck-store-launch index 682cfc0ed..0ac9d47e5 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-store-launch +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-store-launch @@ -1,34 +1,4 @@ #!/usr/bin/python3 -r"""Before Proton starts a store game, asks the app for what that store needs at launch. - -Run by droiddeck-proton / droiddeck-proton-wrap (steam-compatibility) for every real game launch, -whoever started it: the Games tab, a pinned shortcut, or the Steam client's own Play button. It -looks for the store launcher DroidDeck wrote (.droiddeck-launch.bat) among the arguments; beside it -.droiddeck-store.json says which store and game it is. - -For an Epic game, once the Epic Games Launcher is installed (droiddeck-epic), the game is started -through it: the hidden launcher signs the game in and hands its arguments back in -.droiddeck-epic-args. Without the launcher, or when it cannot, the game needs a fresh exchange code -to sign in, which only the app can mint. The request goes through files under $BL_LAUNCH_DIR/stores (the session root, the same path on both -sides): - - req/.json {"op": "epic-code", "store": "epic", "id": ""} (tmp + rename) - resp/.json {"ok": true, "code": true|false, "reason": "..."} - -The app writes .droiddeck-epic-code into the game's folder itself - the code never passes through -here - and the launcher .bat reads and deletes it. No answer within TIMEOUT seconds: the game starts -with its offline identity. A code file older than STALE_SECONDS is removed first, so a code left by -an earlier launch is never used. - -The game's prefix gets Wine's browser set to droiddeck-open-url, which hands web addresses to -Android: an EOS sign-in step that falls back to a browser (a consent Epic asks for once per product) -is finished there. An EOS overlay pointer (HKCU\Software\Epic Games\EOS "OverlayPath") an earlier -build wrote is removed - with it the game exited while loading. - -Battle.net.exe in Battle.net's prefix is handed to droiddeck-battlenet, which runs the whole launch. - - droiddeck-store-launch -""" import base64 import binascii @@ -77,12 +47,6 @@ def sidecar_at(folder): def find_store_game(argv): - """ - The (folder, sidecar) of a store game among [argv], or None when this is not one. Steam passes - the shortcut's target: the launcher .bat when the game has one (Epic, Amazon, a GOG game with - arguments), else the game's exe itself (most GOG games), which may sit in a subfolder - so the - folders above it are looked through too, up to MAX_DEPTH, for the sidecar. - """ for raw in argv: arg = raw.strip().strip('"').strip("'").replace("\\", "/") low = arg.lower() @@ -100,7 +64,6 @@ def find_store_game(argv): def drop_stale(folder, now=None): - """Removes a code file older than STALE_SECONDS; True when one was removed.""" code = folder / EPIC_CODE try: if (now if now is not None else time.time()) - code.stat().st_mtime > STALE_SECONDS: @@ -136,7 +99,6 @@ def through_client(folder, sidecar, timeout=CLIENT_TIMEOUT): def ask(channel, request, timeout=TIMEOUT): - """Writes [request] under channel/req and waits for channel/resp; the answer, or None on timeout.""" req_dir, resp_dir = channel / "req", channel / "resp" req_dir.mkdir(parents=True, exist_ok=True) resp_dir.mkdir(parents=True, exist_ok=True) @@ -161,7 +123,6 @@ def ask(channel, request, timeout=TIMEOUT): def windows_path(unix): - """A guest path as Wine names it through Z: (the whole filesystem).""" return "Z:" + unix.replace("/", "\\") @@ -170,12 +131,6 @@ def reg_escape(value): def set_value(reg, key, name, value, now=None): - """ - Appends `key` with `"name"="value"` to a prefix's user.reg unless the last word on it already - says so. Appended rather than edited in place, as DirectAudio's key is: Wine reads later sections - over earlier ones, and nothing of the file is rewritten under a wineserver that might hold it. - False when there is no user.reg yet (Proton creates the prefix after this; the next launch has it). - """ if not reg.is_file(): return False line = '"%s"="%s"' % (name, reg_escape(value)) @@ -193,7 +148,6 @@ def set_value(reg, key, name, value, now=None): def remove_value(reg, key, name): - """Drops every `"name"=` line under `key` in user.reg; True when one was there.""" if not reg.is_file(): return False rows, inside, dropped = [], False, False @@ -212,13 +166,11 @@ def remove_value(reg, key, name): def options(sidecar): - """An Epic game's launch choices from its sidecar, as the Games tab's Epic card sets them.""" e = sidecar.get("epic") if isinstance(sidecar.get("epic"), dict) else {} return {"eos": e.get("eos", True) is not False, "offline": e.get("offline", False) is True} def server_socket(pfx): - """The wineserver socket for prefix [pfx], as Wine names it (server-- under /tmp/.wine-).""" try: st = os.stat(pfx) except OSError: @@ -228,11 +180,6 @@ def server_socket(pfx): def wait_for_server(pfx, timeout=SERVER_WAIT): - """ - Waits for a wineserver still holding this prefix (an earlier verb's, or the last launch's) to - exit: it rewrites user.reg from its own memory when it does, which drops a line appended - meanwhile. True when none is left. - """ sock = server_socket(pfx) if sock is None: return True @@ -243,7 +190,6 @@ def wait_for_server(pfx, timeout=SERVER_WAIT): def has_value(reg, key, name, value=None): - """Whether the last word on `key` in user.reg sets "name" (to [value], when given).""" if not reg.is_file(): return False inside, last = False, None @@ -258,7 +204,6 @@ def has_value(reg, key, name, value=None): def provision_prefix(compat_data, wait=SERVER_WAIT): - """Wine's browser set to the hand-off, and an earlier build's EOS overlay pointer removed.""" if not compat_data: return "no-prefix" pfx = Path(compat_data) / "pfx" @@ -318,12 +263,10 @@ def battlenet(argv, env=os.environ): def cloud_on(sidecar): - """Cloud saves for this game (the game card's switch; on unless turned off).""" return str(sidecar.get("store")) in CLOUD_STORES and sidecar.get("cloud", True) is not False def exited(argv, code=0): - """After the game: ask the app to upload what changed, without waiting for it.""" found = find_store_game(argv) launch_dir = os.environ.get("BL_LAUNCH_DIR", "") if found is not None and str(found[1].get("store")) == "epic" and os.environ.get(EPIC_HANDLED) != "1" \ @@ -344,7 +287,6 @@ COMET_DIR = Path("/usr/local/lib/droiddeck-comet") def provision_galaxy(compat_data, proton, timeout=90): - """Proton creates the prefix and registers Galaxy before the first game runs.""" if not compat_data or not proton or not Path(proton).is_file(): return "no-proton" data = Path(compat_data) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-wincomponents b/tools/linuxfs/overlay/usr/local/bin/droiddeck-wincomponents index d98e6d66a..d7c3f61af 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-wincomponents +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-wincomponents @@ -1,27 +1,4 @@ #!/usr/bin/python3 -"""Windows components (OpenAL, d3dx, XAudio, ...) for a game's Proton prefix. - -The app installs each component under STORE/: its files in system32/ (64-bit) and syswow64/ -(32-bit) as Windows lays them out, and component.json naming the overrides that make Wine load -them instead of its own ("name" for native-then-builtin, or "name=n" with Wine's own letters) and -the components it bundles ("requires"). The ids are Bannerlator's catalog names (xaudio2.7, -oalinst_dll...). The user picks components per game; the picks are in SELECTION as -{"version": 1, "games": {"": ["oalinst_dll", ...]}}. - -A component installed from a Windows Installer package (droiddeck-msi-install) also has drive_c/, -its files at their Windows paths (Program Files, the .NET GAC, winsxs...), and registry.json, the -values its package writes. - -droiddeck-game-env calls apply() before every game launch. It copies what is missing or changed -into compatdata//pfx, writes the components' registry values into the prefix's registry -files (system.reg, user.reg) and returns the overrides for WINEDLLOVERRIDES, so Wine never has to -be running. A prefix Proton has not made yet gets the registry values on the launch after. - -Proton leaves the files alone afterwards: its builtin refresh (update_builtin_libs) replaces only -files that are its own builtins, and a new prefix is built around files already there (copy_pfx -skips them). The one case that removes them is a launch on an older Proton than the prefix's, -which deletes every file Proton once placed - and the next launch's apply() puts ours back. -""" import json import os import re @@ -62,7 +39,6 @@ def wanted(app_id, selection=SELECTION): def component(store, cid): - """The component's overrides, its files as (relative path, source) and the ids it bundles, or None if not installed.""" root = Path(store) / cid try: with open(root / "component.json", encoding="utf-8") as source: @@ -87,7 +63,6 @@ def component(store, cid): def registry_values(root): - """The component's registry values (registry.json), the ones that are well formed.""" try: with open(Path(root) / "registry.json", encoding="utf-8") as source: values = json.load(source).get("values", []) @@ -102,7 +77,6 @@ def registry_values(root): def expand(store, ids, saved=None): - """The picks with everything they bundle, each once, a bundle's parts before it.""" out, seen = [], set() saved = saved if isinstance(saved, dict) else {} @@ -122,7 +96,6 @@ def expand(store, ids, saved=None): def spelled(path): - """path with each folder that exists spelled as the prefix spells it (Windows names ignore case).""" parts = path.parts current = Path(parts[0]) for part in parts[1:]: @@ -141,7 +114,6 @@ def spelled(path): def existing(path): - """The file at path as the prefix spells it: Windows names ignore case, the filesystem may not.""" folder = spelled(path.parent) path = folder / path.name try: @@ -165,7 +137,6 @@ def same(src, dst): def builtin(path): - """Proton's own: a symlink into its files, or a Wine DLL that says so in its DOS stub.""" if path.is_symlink(): return True try: @@ -177,7 +148,6 @@ def builtin(path): def place(src, target, backup=None): - """Copies src over target unless it is already there; a file of someone else's goes to backup first.""" target = spelled(target.parent) / target.name target.parent.mkdir(parents=True, exist_ok=True) found = existing(target) @@ -195,7 +165,6 @@ def place(src, target, backup=None): def reg_escape(text, extra=""): - """A string as Wine's registry files write it.""" out = [] for ch in text: if ch == "\\" or ch == '"' or ch in extra: @@ -240,7 +209,6 @@ def reg_key_line(key): def reg_value_line(name, kind, data): - """name=data as one registry file line; None for a kind that has no line of its own.""" head = "@" if name == "" else '"%s"' % reg_escape(name) if kind == "sz": return '%s="%s"' % (head, reg_escape(str(data))) @@ -258,13 +226,11 @@ def reg_value_line(name, kind, data): def reg_string(line): - """The text of a string value line (plain or str(2)), or None for any other kind.""" match = re.match(r'^(?:@|"(?:[^"\\]|\\.)*")=(?:str\(2\):)?"((?:[^"\\]|\\.)*)"\s*$', line) return reg_unescape(match.group(1)) if match else None class RegistryFile: - """One of a prefix's registry files, as Wine keeps them: [key] sections of name=value lines.""" def __init__(self, path): self.path = Path(path) @@ -291,7 +257,6 @@ class RegistryFile: self.links[current.lower()] = target def link_target(self, number, root): - """Where the link key whose "#link" line is at number leads, relative to this file's root.""" for at in range(number + 1, min(number + 8, len(self.lines))): line = self.lines[at] if line.startswith("["): @@ -312,7 +277,6 @@ class RegistryFile: return None def resolve(self, key): - """The key Wine really keeps key in: a path through a link key leads to the link's target.""" key = key.strip("\\") for _ in range(8): lower = key.lower() @@ -338,7 +302,6 @@ class RegistryFile: return None if end is None else reg_unescape(line[1:end]) def section(self, key): - """(start, end) line numbers of the key's values, or None.""" start = self.index.get(self.resolve(key).lower()) if start is None: return None @@ -355,7 +318,6 @@ class RegistryFile: return reg_unescape(match.group(1)).lower() if match else None def find(self, key, name): - """(first, last + 1) of the value's lines, or None.""" span = self.section(key) if span is None: return None @@ -389,7 +351,6 @@ class RegistryFile: self.changed = True def put(self, key, name, raw): - """Sets the value's line(s) to raw, or removes the value when raw is None; True if that changed it.""" found = self.find(key, name) if raw is None: if found is None: @@ -432,8 +393,6 @@ class RegistryFile: def list_line(name, current, item, separator, add, at_end=True): - """current (a string value line) with item added to or taken out of its list, None if it had - nothing else; a value that is not a string is left as it is.""" text = reg_string(current) if current is not None else "" if text is None: return current @@ -447,9 +406,6 @@ def list_line(name, current, item, separator, add, at_end=True): def apply_registry(pfx, values, state, held=()): - """Writes the picked components' registry values into the prefix and takes out the ones of - components turned off: a value goes back to what it was before, an entry added to a list such - as PATH just leaves the list. Returns (values written, notes).""" files = {} for hive, name in HIVES.items(): if (pfx / name).is_file(): @@ -518,7 +474,6 @@ def save_state(compat, state): def pending_for(app_id, pending=PENDING): - """The component ids the app is still getting ready for app_id; empty when none (or no file).""" try: with open(pending, encoding="utf-8") as source: ids = json.load(source).get("games", {}).get(str(app_id), []) @@ -528,9 +483,6 @@ def pending_for(app_id, pending=PENDING): def wait_for_pending(app_id, pending=PENDING, store=STORE, timeout=PENDING_WAIT, sleep=time.sleep, clock=time.monotonic): - """Waits (at most timeout seconds) for the components still being got ready for app_id to be in - the store or dropped from the pending list. No wait at all when nothing is pending. Returns a - note for the log, or None.""" def missing(): return [cid for cid in pending_for(app_id, pending) if component(store, cid) is None] left = missing() @@ -548,7 +500,6 @@ def wait_for_pending(app_id, pending=PENDING, store=STORE, timeout=PENDING_WAIT, def apply(compat_path, app_id, selection=SELECTION, store=STORE, pending=PENDING, wait=wait_for_pending): - """Brings the prefix in line with the game's picks. Returns (overrides, notes).""" compat = Path(compat_path) pfx = compat / "pfx" waited = wait(app_id, pending, store) if wait else None @@ -642,7 +593,6 @@ def apply(compat_path, app_id, selection=SELECTION, store=STORE, pending=PENDING def overrides_value(names): - """WINEDLLOVERRIDES entries: native-then-builtin unless the component says otherwise; the last word on a DLL wins.""" modes = {} for entry in names: name, _, mode = entry.partition("=") diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-x64-arm-proton b/tools/linuxfs/overlay/usr/local/bin/droiddeck-x64-arm-proton index 4a074be13..1f82a0691 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-x64-arm-proton +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-x64-arm-proton @@ -1,9 +1,7 @@ #!/bin/bash -# Runs natively: droiddeck-steam-x64's x86 wrappers start it through an arm64 binary, which FEX hands to the CPU. tool=$1; shift x64_home=$HOME -# The game overlay tags the game's windows with the Steam app ID; without it gamescope never shows them. overlay=0 case ${LD_PRELOAD:-} in *gameoverlayrenderer.so*) overlay=1 ;; esac @@ -20,7 +18,6 @@ else export HOME=/root fi [ -z "$game_display" ] || export DISPLAY=$game_display -# Proton takes its Steam client parts from the client's install folder; the x86-64 client's are x86. export BL_X64_STEAM_ROOT=${STEAM_COMPAT_CLIENT_INSTALL_PATH:-} export STEAM_COMPAT_CLIENT_INSTALL_PATH=$HOME/.local/share/Steam arm_overlay=$STEAM_COMPAT_CLIENT_INSTALL_PATH/steamrtarm64/gameoverlayrenderer.so diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-x64-arm-webhelper b/tools/linuxfs/overlay/usr/local/bin/droiddeck-x64-arm-webhelper index 646dbd01b..bd89a4ddb 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-x64-arm-webhelper +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-x64-arm-webhelper @@ -1,5 +1,4 @@ #!/bin/bash -# The arm64 client's web helper for the x86-64 client, run natively: FEX hands this arm64 program to the CPU. x64_home=$HOME arm=/root/.local/share/Steam/steamrtarm64 diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-x64-webhelper b/tools/linuxfs/overlay/usr/local/bin/droiddeck-x64-webhelper index 10abdf2a4..8c830ab9b 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-x64-webhelper +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-x64-webhelper @@ -1,8 +1,5 @@ #!/bin/bash -# Steam's compositor draws each browser window from its X pixmap: frames must reach X, not gamescope's WSI layer. -# gamescope sets vk_xwayland_wait_ready=false for its children; kopper swapchains flashed black even with the wait. unset BL_X64_ARM_WEBHELPER -# The shim already added the GPU flags (preload/webhelper.c) to these arguments. export BL_WEBHELPER_ARGS=stock DISABLE_GAMESCOPE_WSI=1 vk_xwayland_wait_ready=true LIBGL_KOPPER_DISABLE=true if [ -x ./steamwebhelper ]; then exec ./steamwebhelper "$@" diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-xbox b/tools/linuxfs/overlay/usr/local/bin/droiddeck-xbox index efd0b707f..304e12655 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-xbox +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-xbox @@ -1,5 +1,4 @@ #!/usr/bin/python3 -"""Licensed Xbox launches through Xodus and the game's selected ARM64 Proton.""" import hmac import json import os diff --git a/tools/linuxfs/overlay/usr/local/bin/steam-compatibility b/tools/linuxfs/overlay/usr/local/bin/steam-compatibility index 4d81df93e..6e876a506 100755 --- a/tools/linuxfs/overlay/usr/local/bin/steam-compatibility +++ b/tools/linuxfs/overlay/usr/local/bin/steam-compatibility @@ -248,8 +248,6 @@ bl_idtech3() { } """ -# Source games report VAC-secure or insecure only in their console, written to console.log only with -condebug. -# The app keeps the switch and the records in /root: the x86-64 client runs games with its own HOME. BL_VACDIAG_SETUP = """ bl_vacdiag() { [ -f /root/.droiddeck-vac-diagnostics ] || return 0 @@ -276,10 +274,6 @@ bl_vacdiag() { } """ -# Steam's shader pre-caching runs its D3D driver query through every Proton tool at each start, as -# "run /bin/d3ddriverquery64.exe" in compatdata/0. Here it has never succeeded (exit 1 every time), and each -# run started Wine in compatdata/0, where the two Protons upgrade the prefix back and forth for many seconds while the -# game starts. Steam gets the same failure without starting Wine; other verbs and prefixes are left alone. BL_DRIVER_QUERY_SKIP = """ case "$verb:${1##*/}:${STEAM_COMPAT_DATA_PATH%/}" in run:d3ddriverquery64.exe:*/compatdata/0 | run:d3ddriverquery.exe:*/compatdata/0) @@ -551,7 +545,6 @@ def tokenize(text): def find_block(tokens, path): - """Index of the '{' opening the block at path, or None.""" depth, want, i = 0, 0, 0 while i < len(tokens): t = tokens[i] @@ -602,10 +595,6 @@ def serialize(tokens): def write_changed(path, text, mode=None): - """Writes `text` to `path` only when it differs, through a rename so a reader never sees half a - file. The session runs this script every 15 s for as long as the client does; rewriting files - that have not changed cost a python start's worth of writes each time, and config.vdf is the - client's own, which it may be writing at that moment. Returns whether anything was written.""" try: with open(path, encoding="utf-8", errors="replace") as f: same = f.read() == text @@ -633,8 +622,6 @@ def display_name(base): def first_key(path, section): - """The single key naming a block inside `section`, or None. Used to keep a third-party tool's - own internal name, so a mapping the user already made against it keeps working.""" try: with open(path, encoding="utf-8", errors="replace") as f: tokens = tokenize(f.read()) @@ -652,7 +639,6 @@ def first_key(path, section): def manifest_entry_point(path): - """The script a toolmanifest's commandline runs, relative to the tool directory.""" try: with open(path, encoding="utf-8", errors="replace") as f: tokens = tokenize(f.read()) @@ -667,9 +653,6 @@ def manifest_entry_point(path): def adopt_extras(tools): - """Makes every other Proton in compatibilitytools.d launchable here, and returns the tool names - to leave alone in CompatToolMapping - the whole point of installing one is to pick it per game, - so register_default must not repoint it back at ours.""" adopted = {} try: entries = sorted(os.listdir(tools)) @@ -743,7 +726,6 @@ def libraries(steam_root): def installed_apps(steam_root): - """App ids of the titles the client has installed, tools and runtimes excluded.""" apps = set() for library in libraries(steam_root): steamapps = os.path.join(library, "steamapps") @@ -763,7 +745,6 @@ STEAM_PLAY_MANIFESTS = 891390 def launch_programs(config): - """The Windows and Linux programs an app's launch options start, relative to its folder (lowercase, "/").""" launch = config.get("launch") if isinstance(config, dict) else None found = [] for option in launch.values() if isinstance(launch, dict) else (): @@ -778,7 +759,6 @@ def launch_programs(config): def launch_options(config): - """An app's launch options as Steam lists them: executable, arguments, working folder, type and oslist.""" launch = config.get("launch") if isinstance(config, dict) else None found = [] for key, option in launch.items() if isinstance(launch, dict) else (): @@ -956,7 +936,6 @@ def manifest_flags(tokens): def tool_downloads(steam_root): - """(app, "install" or "update") for each tool the client has yet to install or bring up to date.""" wanted = [] for app in REQUIRED_TOOLS: tokens = app_manifest(steam_root, app) @@ -1252,7 +1231,6 @@ def applied_game_requests(requests, overrides, state): def requested_games(requests, catalog, runnable=None): - """App id -> requested tool, None for inherit, or DEFERRED_GAME_TOOL while unavailable.""" runnable = None if runnable is None else set(runnable) resolved = {} for app, request in requests.items(): @@ -1356,7 +1334,6 @@ def read_mapping(tokens, mapping): def read_config_mapping(config_path): - """Read per-app choices without creating or rewriting the client's mapping block.""" try: with open(config_path, encoding="utf-8", errors="replace") as f: tokens = tokenize(f.read()) @@ -1442,9 +1419,6 @@ def live_helper_running(directory=None): def register_default(config_path, apps=(), protect=(), default=None, catalog=None, directory=None, settle=False, picks=(), linux_tool=FEX_TOOL): - """Writes the mapping: the "0" default and every title that follows it (see plan_mapping). - With settle, the default itself is decided first (see choose_default): before the client - starts, config.vdf holds what the client last kept, menu changes included.""" with locked(directory): state = read_json(state_path("state.json", directory)) with open(config_path, encoding="utf-8", errors="replace") as f: @@ -1508,7 +1482,6 @@ def register_default(config_path, apps=(), protect=(), default=None, catalog=Non def depot_build(depot): - """The build number a depot's version file starts with; 0 when there is none.""" try: with open(os.path.join(depot, "version"), encoding="utf-8", errors="replace") as f: first = f.read().split(None, 1) @@ -1518,7 +1491,6 @@ def depot_build(depot): def find_source(steam_root, sources=SOURCES): - """The first of `sources` the client has downloaded, newest across the libraries holding it.""" for name in sources: found = [os.path.join(root, "steamapps", "common", name) for root in libraries(steam_root) if os.path.isdir(os.path.join(root, "steamapps", "common", name, "files", "bin-arm64"))] @@ -1528,7 +1500,6 @@ def find_source(steam_root, sources=SOURCES): def build_tool(tool_dir, depot, sources=SOURCES, name=TOOL): - """Our own manifests and the launcher, named after `depot`. The depot is run at launch time.""" 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): @@ -1557,7 +1528,6 @@ def remove_tool(tool_dir): def local_tools(steam_root): - """The names of the compatibility tools installed in steam_root/compatibilitytools.d.""" names = set() tools = os.path.join(steam_root, "compatibilitytools.d") try: @@ -1576,11 +1546,6 @@ def local_tools(steam_root): def x64_repoint(steam_root, tool, live=False): - """The x86-64 client (droiddeck-steam-x64): its tools are arm64 wrappers that script makes and - its Linux titles run as Linux builds (FEX), so only the entries the client aimed at a Proton it - cannot start are repointed, to `tool`. Tools in its own compatibilitytools.d are kept: the - wrappers include proton-cachyos-*-a64, which starts with "proton" like Valve's own. - Returns the app ids repointed.""" config = os.path.join(steam_root, "config", "config.vdf") if not tool or not os.path.isfile(config): return [] @@ -1597,7 +1562,6 @@ def x64_repoint(steam_root, tool, live=False): set_mapping(tokens, mapping, "0", tool) for app, name in current.items(): named = name.lower() - # A tool named arm64 is never this client's (its wrappers are -a64), so this legacy name is always repointed. legacy = named == "droiddeck-proton-arm64" if (named.startswith("proton") or legacy) and named != tool.lower() \ and (legacy or named not in keep): @@ -1608,7 +1572,6 @@ def x64_repoint(steam_root, tool, live=False): if text != serialize(tokenize(original)): write_changed(config, text) print("steam-compatibility: x86-64 client's Proton entries point at %s" % tool) - # Steam reads a mapping changed while it runs only at its next start; the session restarts it (.bl-compat-pending). if moved and live: with open(os.path.join(os.path.expanduser("~"), ".bl-compat-pending"), "a") as f: f.write("".join("%s\n" % a for a in moved)) diff --git a/tools/linuxfs/preload/inputudev.c b/tools/linuxfs/preload/inputudev.c index 4960c382d..eb78ce1ef 100644 --- a/tools/linuxfs/preload/inputudev.c +++ b/tools/linuxfs/preload/inputudev.c @@ -178,10 +178,6 @@ static int caller_is_winebus(void *caller) { return strstr(info.dli_fname, "winebus.so") != NULL; } -/* Wine's HID haptics give every XInput rumble a 1000 ms cutoff and xinput only writes on a change, - * but XInput motors hold their speed until the next XInputSetState; evdev's length 0 holds until - * stopped. Only this library, preloaded first, sees winebus as the caller. The length is put back - * because winebus compares its copy with the next state to decide whether to upload again. */ __attribute__((visibility("hidden"))) int bl_winebus_rumble_ioctl(int (*real)(int, unsigned long, void *), int fd, unsigned long request, void *arg, void *caller, int *rc) { diff --git a/tools/linuxfs/preload/pulseguard.c b/tools/linuxfs/preload/pulseguard.c index c3011df67..d9b4e7ef0 100644 --- a/tools/linuxfs/preload/pulseguard.c +++ b/tools/linuxfs/preload/pulseguard.c @@ -1,5 +1,3 @@ -/* In Deck mode (-steamos3) the x86-64 client's SteamOS audio manager passes libpulse the NULL operation of a - * failed request, and libpulse aborts on it ("Assertion 'o' failed ... pa_operation_get_state()"). */ #if defined(__x86_64__) || defined(__i386__) #define _GNU_SOURCE #include @@ -9,7 +7,6 @@ typedef struct pa_operation pa_operation; #define PA_OPERATION_CANCELLED 2 -/* A libpulse loaded with RTLD_LOCAL (Wine's winepulse.so) is not in RTLD_NEXT's scope. */ static void *libpulse(const char *name) { void *fn = dlsym(RTLD_NEXT, name); if (!fn) { diff --git a/tools/linuxfs/preload/robust.c b/tools/linuxfs/preload/robust.c index 8ab149c81..377b89567 100644 --- a/tools/linuxfs/preload/robust.c +++ b/tools/linuxfs/preload/robust.c @@ -23,7 +23,6 @@ #define BL_RESOLVE_BENEATH 0x08 #define BL_RESOLVE_IN_ROOT 0x10 -// The kernel's MAXSYMLINKS. #define BL_MAX_SYMLINKS 40 #define BL_PROC_ROOT_INO 1 @@ -107,7 +106,6 @@ void *dlopen(const char *filename, int flags) { } const char *basename = filename ? strrchr(filename, '/') : NULL; basename = basename ? basename + 1 : filename; - // Steam's deep binding bypasses the robust-list shim under PRoot. if (basename && strcmp(basename, "vrclient.so") == 0) { flags &= ~RTLD_DEEPBIND; } @@ -148,7 +146,6 @@ static struct robust_list_head *thread_robust_head(void) { return head; } -// Magic links are procfs links below a process's directory; procfs's root holds only plain ones (self, thread-self, mounts). static int is_magic_link(int dirfd, char *link, size_t parent_len) { int fd = (int)path_open(dirfd, link, O_PATH | O_NOFOLLOW | O_CLOEXEC, 0L); if (fd < 0) { @@ -168,7 +165,6 @@ static int is_magic_link(int dirfd, char *link, size_t parent_len) { return result != 0 || parent.st_ino != BL_PROC_ROOT_INO; } -// RESOLVE_IN_ROOT walked one component at a time: absolute symlinks restart at dirfd and ".." stops there. static long openat_in_root(int dirfd, const char *path, int flags, long mode) { char resolved[PATH_MAX]; char pending[PATH_MAX]; @@ -177,7 +173,6 @@ static long openat_in_root(int dirfd, const char *path, int flags, long mode) { int have_root = 0; int root_on_procfs = 0; dev_t root_dev = 0; - // O_NOFOLLOW and O_CREAT | O_EXCL leave a final symlink alone, unless a trailing slash follows it. int nofollow = (flags & O_NOFOLLOW) || (flags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL); const char *rest = path; for (;;) { @@ -241,7 +236,6 @@ static long openat_in_root(int dirfd, const char *path, int flags, long mode) { have_root = 1; } if ((st.st_dev != root_dev || root_on_procfs) && is_magic_link(dirfd, resolved, resolved_len)) { - // RESOLVE_IN_ROOT refuses magic links. errno = EXDEV; return -1; } diff --git a/tools/linuxfs/preload/rptouch.c b/tools/linuxfs/preload/rptouch.c index 7d39f286e..b8eb5ea90 100644 --- a/tools/linuxfs/preload/rptouch.c +++ b/tools/linuxfs/preload/rptouch.c @@ -1,6 +1,3 @@ -/* Steam's Linux Remote Play host has no touch injection: client touches arrive as an XTest mouse (absolute - * motion plus button 1). While a stream holds the encoder, those become touches into gamescope's own touch - * input, as a finger on the device screen would. */ #define _GNU_SOURCE #include #include diff --git a/tools/linuxfs/preload/udevmon.c b/tools/linuxfs/preload/udevmon.c index 33baf2bda..f0e171a86 100644 --- a/tools/linuxfs/preload/udevmon.c +++ b/tools/linuxfs/preload/udevmon.c @@ -195,8 +195,8 @@ static unsigned murmur_hash2(const char *key, size_t len) { len -= 4; } switch (len) { - case 3: h ^= data[2] << 16; /* fallthrough */ - case 2: h ^= data[1] << 8; /* fallthrough */ + case 3: h ^= data[2] << 16; __attribute__((fallthrough)); + case 2: h ^= data[1] << 8; __attribute__((fallthrough)); case 1: h ^= data[0]; h *= m; } h ^= h >> 13; diff --git a/tools/linuxfs/preload/venc.c b/tools/linuxfs/preload/venc.c index 9480e8812..92c60c3bf 100644 --- a/tools/linuxfs/preload/venc.c +++ b/tools/linuxfs/preload/venc.c @@ -1,4 +1,3 @@ -/* /dev/video-enc0 for Steam's Remote Play V4L2 encoder stage, backed by the app's MediaCodec encoder. */ #define _GNU_SOURCE #include #include @@ -587,7 +586,6 @@ __attribute__((visibility("hidden"))) void bl_venc_closed(int fd) { vn_free_queue(&v->raw); vn_free_queue(&v->coded); v->used = 0; - /* A forked child closing its inherited copy must not end the stream for the parent. */ if (__atomic_sub_fetch(&vn_open_count, 1, __ATOMIC_RELEASE) == 0 && v->owner == getpid()) vn_marker(0); pthread_mutex_unlock(&vn_lock); } diff --git a/tools/linuxfs/preload/zram.c b/tools/linuxfs/preload/zram.c index 8f31d19eb..af62c2941 100644 --- a/tools/linuxfs/preload/zram.c +++ b/tools/linuxfs/preload/zram.c @@ -126,8 +126,6 @@ static void zr_walk(int compress) { zr_sys(SYS_close, maps); } -// Before it stops a session for suspend, the app waits for .ack. to appear (the handler -// runs here, not one a program installed over it) and then to hold a byte (the page-out finished). static void zr_ack(int done) { static const char suffix[] = ".ack."; char path[sizeof(ZR_CTL) + sizeof(suffix) + 20]; diff --git a/tools/packs/build.sh b/tools/packs/build.sh index b6089111c..41dc7e03c 100755 --- a/tools/packs/build.sh +++ b/tools/packs/build.sh @@ -1,13 +1,4 @@ #!/usr/bin/env bash -# Builds the add-on packs the app downloads on first use instead of shipping in the APK: -# proot.tar.zst the patched proot and its loader (built by tools/proot) -# directaudio.tar.zst the Wine DirectAudio drivers (fetched by tools/directaudio/fetch.sh) -# tracking.tar.zst hand-tracking models and the OpenCV/MediaPipe native libraries -# xbox.tar.zst the Xbox backend (built by tools/xbox) for /usr/local/lib/droiddeck-xbox -# comet.tar.zst the GOG Galaxy companion (built by tools/comet) for /usr/local/lib/droiddeck-comet -# Only CI publishes these (.github/workflows/publish-packs.yml); the APK built from the same -# directory pins each pack's SHA-256. -# Usage: tools/packs/build.sh set -euo pipefail here=$(cd "$(dirname "$0")" && pwd) diff --git a/tools/packs/proot/release.env b/tools/packs/proot/release.env index 7a23b2799..6d88f886a 100644 --- a/tools/packs/proot/release.env +++ b/tools/packs/proot/release.env @@ -1,6 +1,2 @@ -# Published proot pack for a build that did not compile libproot.so into the APK. -# CI builds embed the binary and publish their own pack; this pin is what a source -# build downloads. Tag on DroidDeck-Components: pack-proot-. -# Update both lines when source builds should pick up a newer proot. PROOT_PACK_SHA256=5a2b7284e6357f88a14a790abd54e61effbe5f202c63b6aaba9b974c559649c3 PROOT_PACK_SIZE=96643 diff --git a/tools/pipewire/assemble.sh b/tools/pipewire/assemble.sh index a9adb6c26..224b75c39 100755 --- a/tools/pipewire/assemble.sh +++ b/tools/pipewire/assemble.sh @@ -1,6 +1,4 @@ #!/usr/bin/env bash -# assemble.sh -# Packs the video side of SteamOS's PipeWire and WirePlumber: no audio plugins, no JACK. set -euo pipefail src=$1 out=$2 diff --git a/tools/prepare_windows_runtime.py b/tools/prepare_windows_runtime.py index 831fc7d7d..886deb2a0 100644 --- a/tools/prepare_windows_runtime.py +++ b/tools/prepare_windows_runtime.py @@ -37,8 +37,6 @@ DONOR_CERT_SHA256 = 'b241ea7d77822d39b89d3227e06f3ecf62adec6cbfd38ad06196787991b DONOR_URL = 'https://github.com/Droid-Deck/DroidDeck-CI/releases/download/pr-732/DroidDeck-PR.732-bdf876d.apk' AUDITED_SOURCE = '1dc66bf9be64a404c80400049c0803614814461b' LOCAL_BUILD_PLUMBING = {'app/build.gradle', 'tools/release/runtime_inputs.py'} -# These Android-only inputs are compiled by Gradle, never taken from the donor. -# Keep this list explicit so unrelated native or Linux runtime edits still fail. LOCAL_ANDROID_SOURCES = {'app/src/main/cpp/CMakeLists.txt', 'app/src/main/cpp/viture/sdk_bridge.cpp', 'app/src/main/cpp/viture/display_modes.h', @@ -51,8 +49,6 @@ LOCAL_ANDROID_SOURCES = {'app/src/main/cpp/CMakeLists.txt', 'app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c', 'app/src/main/cpp/waylandcomp/src/spatialpose.c', 'app/src/main/cpp/waylandcomp/CMakeLists.txt'} -# These audited helper updates are copied from this checkout by runtime.record(). -# Permit them only while source_assets() still guarantees that staging path. LOCAL_SESSION_HELPERS = {'tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat', 'tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr'} VR_PREFIX = 'assets/linuxfs/usr/local/lib/droiddeck-vr/' @@ -83,7 +79,6 @@ def changed(base, target=None): def check_sources(): - # A branch label alone is insufficient: CI merged this main commit into it. if git('merge-tree', '--write-tree', DONOR_PR_SOURCE, DONOR_MAIN_SOURCE).decode().splitlines()[0] != DONOR_TREE: raise ValueError('The donor CI merge does not match the audited source tree') helper_sources = {path.relative_to(ROOT).as_posix() for path in source_assets()} @@ -187,8 +182,6 @@ def validate_tracked_assets(package, llvm_strip): if prefix != 'app/src/main/jniLibs/': raise ValueError('Tracked bytes differ from the donor: ' + name + ' (text inputs must be checked out with LF)') - # AGP strips prebuilt JNI libraries during packaging. Verify that - # deterministic transformation on a temporary copy, never the source. with tempfile.TemporaryDirectory(prefix='droiddeck-strip-check-') as temp: stripped = Path(temp) / 'library.so' subprocess.run([str(llvm_strip), '--strip-unneeded', '-o', str(stripped), @@ -264,7 +257,6 @@ def prepare(apk, rebuilt_dir, gamescope, llvm_strip=None, rebuilt_vr=None, vk_in checked_output(target) plan[target] = data origins[target] = 'VR rebuilt from current source; source and output checksums verified' - # Do not silently retain unrelated files from a prior preparation. expected = set(plan) | {'app/src/main/' + name for name in helper_assets} for tree in ('linuxfs', 'directaudio', 'droiddeck-esync'): base = ROOT / 'app/src/main/assets' / tree diff --git a/tools/proot/bench/checks.h b/tools/proot/bench/checks.h index 2b0f9064a..be60c7cb7 100644 --- a/tools/proot/bench/checks.h +++ b/tools/proot/bench/checks.h @@ -1,7 +1,6 @@ #ifndef DROIDDECK_BENCH_CHECKS_H #define DROIDDECK_BENCH_CHECKS_H -/* Benchmark assertions execute the operations being measured. */ #ifdef NDEBUG #undef NDEBUG #endif diff --git a/tools/proot/bench/equiv2.py b/tools/proot/bench/equiv2.py index b267ad121..d8eb956a5 100644 --- a/tools/proot/bench/equiv2.py +++ b/tools/proot/bench/equiv2.py @@ -1,9 +1,3 @@ -"""Fast path equivalence, part two: symlinked directories (Wine's dosdevices), "..", trailing -slashes, realpath/getcwd, the write family, Unix sockets, xattrs, statfs, inotify and fstat. - -Run under proot with and without the fast path (run-device.sh fpoff / fastpath) and diff the -output: every line must match. Builds its own tree under /root/fx and /mnt/droiddeck-sd/fx. -""" import ctypes import errno import os diff --git a/tools/proot/bench/equiv3.py b/tools/proot/bench/equiv3.py index b40febf8e..d8a22f32b 100644 --- a/tools/proot/bench/equiv3.py +++ b/tools/proot/bench/equiv3.py @@ -1,13 +1,3 @@ -"""Fast path equivalence, part three: /proc (self, fd and exe links, readlink as proot answers it, -realpath through it, opening through fd links), link text naming host paths, creating in -directories whose host path is long, glue above a binding proot cannot look into, files the -caller may not stat, trailing slashes, shm_open, accept, passwd lookups and the _FORTIFY_SOURCE -entry points. - -Run under proot with and without the fast path (run-device.sh fpoff / fastpath) and diff the -output: every line must match. The rig's rootfs is ROOT; run-device.sh binds /cache/fp/perf and -SHARED at /opt/fpx. -""" import ctypes import errno import os diff --git a/tools/proot/fastpath/fastpath.c b/tools/proot/fastpath/fastpath.c index f768fb073..0a9d3f22b 100644 --- a/tools/proot/fastpath/fastpath.c +++ b/tools/proot/fastpath/fastpath.c @@ -913,7 +913,6 @@ static void fill_statx(struct statx *out, const struct stat *st) { out->stx_dev_minor = dev_minor(st->st_dev); } -/* Emulate statx on older kernels < 4.11 */ static long statx_call(int dirfd, const char *path, int flags, unsigned mask, struct statx *out) { struct stat st; long rs; @@ -1374,7 +1373,6 @@ int inotify_add_watch(int fd, const char *path, uint32_t mask) { } -/* The kernel reports a socket named through /proc/self/fd/N/ under that name: map it back, per socket. */ enum { LONG_SELF, LONG_PEER }; static struct { unsigned long ino; int kind; char name[sizeof ((struct sockaddr_un *)0)->sun_path]; char guest[sizeof ((struct sockaddr_un *)0)->sun_path]; } long_names[64]; static unsigned long_next; @@ -1453,7 +1451,6 @@ static long socket_address(const struct sockaddr *addr, socklen_t len, int follo memset(out, 0, sizeof *out); out->sun_family = AF_UNIX; if (hlen >= room) { - /* proot would bind it to a temporary name the fast path cannot see: name it through its directory. */ char *slash = strrchr(r.host, '/'); if (!slash || slash == r.host) return FP_SLOW; *slash = 0; diff --git a/tools/proot/fastpath/svc-patch.h b/tools/proot/fastpath/svc-patch.h index 300302f58..6d19da315 100644 --- a/tools/proot/fastpath/svc-patch.h +++ b/tools/proot/fastpath/svc-patch.h @@ -89,7 +89,6 @@ long fp_svc_dispatch(const long *r) { } void fp_svc_entry(void) __attribute__((visibility("hidden"))); -/* A replaced svc may have live values in every register, including NZCV and SIMD. */ __asm__( ".text\n.p2align 4\n.hidden fp_svc_entry\n.type fp_svc_entry,%function\nfp_svc_entry:\n" ".cfi_startproc\n.cfi_def_cfa sp, 16\n.cfi_return_column 16\n" @@ -144,11 +143,11 @@ static int svc_emit(uint32_t *code, uint32_t *site, uint32_t *branch) { uint32_t back; if (!svc_branch((uintptr_t)site, (uintptr_t)code, branch) || !svc_branch((uintptr_t)(code + 5), (uintptr_t)(site + 1), &back)) return 0; - code[0] = 0xa9bf47f0U; /* stp x16, x17, [sp, #-16]! */ - code[1] = 0x580000f0U; /* ldr x16, literal at +32 */ - code[2] = 0x10000051U; /* adr x17, +8 */ - code[3] = 0xd61f0200U; /* br x16 */ - code[4] = 0xa8c147f0U; /* ldp x16, x17, [sp], #16 */ + code[0] = 0xa9bf47f0U; + code[1] = 0x580000f0U; + code[2] = 0x10000051U; + code[3] = 0xd61f0200U; + code[4] = 0xa8c147f0U; code[5] = back; uintptr_t entry = (uintptr_t)fp_svc_entry; uintptr_t resume = (uintptr_t)(site + 1); diff --git a/tools/proot/fastpath/svc-stack.h b/tools/proot/fastpath/svc-stack.h index d3a4ff528..0739bebc6 100644 --- a/tools/proot/fastpath/svc-stack.h +++ b/tools/proot/fastpath/svc-stack.h @@ -78,6 +78,5 @@ static int svc_stack_room(uintptr_t sp) { if (grow < svc_stack_gap) grow = svc_stack_gap; if (grow < low) low = grow; } - /* Path translation nests several PATH_MAX-sized frames. */ return sp - low >= 96 * 1024; } diff --git a/tools/proot/patches/0015-lost-fork-events.patch b/tools/proot/patches/0015-lost-fork-events.patch index d3912abf4..dc1a4a2db 100644 --- a/tools/proot/patches/0015-lost-fork-events.patch +++ b/tools/proot/patches/0015-lost-fork-events.patch @@ -15,19 +15,17 @@ diff --git a/src/tracee/tracee.c b/src/tracee/tracee.c #include /* ptrace(2), PTRACE_*, */ #include /* E*, */ #include /* PRI*, */ -+#include /* getpid(2), */ -+#include /* fopen(3), fgets(3), snprintf(3), */ -+#include /* opendir(3), readdir(3), closedir(3), */ -+#include /* INT_MAX, */ ++#include ++#include ++#include ++#include #include "tracee/tracee.h" #include "tracee/reg.h" -@@ -402,16 +406,128 @@ +@@ -402,16 +406,119 @@ } } -+/* Read host process IDs, not PRoot's guest /proc view. /proc//status -+ * works for non-leader threads too, even though /proc lists only leaders. */ +static int read_task_ids(pid_t pid, long *tgid, long *ppid, long *tracer) +{ + char path[64], line[256]; @@ -54,13 +52,6 @@ diff --git a/src/tracee/tracee.c b/src/tracee/tracee.c + return status; +} + -+/* Recover only a delivered creation event whose GETEVENTMSG succeeded but -+ * returned zero. The creator is still stopped at that event, and its real -+ * clone flags have already been read. Require exactly one uninitialized, -+ * PRoot-traced candidate of the right kind and relationship. Never infer -+ * clone flags from PPid, substitute a process for a thread, or choose by PID -+ * order. Include tracees whose initial SIGSTOP was handled before this event. -+ * Missing events and ambiguous candidates need separate recovery logic. */ +static int find_event_child(Tracee *parent, word_t clone_flags, unsigned long *result) +{ + char path[64]; @@ -153,7 +144,7 @@ diff --git a/src/tracee/tracee.c b/src/tracee/tracee.c int status; /* If the tracee calls clone(2) with the CLONE_VFORK flag, -@@ -425,24 +541,62 @@ +@@ -425,24 +532,60 @@ * order to get the right flags. */ status = fetch_regs(parent); @@ -185,8 +176,6 @@ diff --git a/src/tracee/tracee.c b/src/tracee/tracee.c + static bool warned; + + status = flags_known ? find_event_child(parent, clone_flags, &pid) : -EIO; -+ /* A thread creator can reach clone's exit while its child is held. -+ * vfork cannot: its creator waits for the child, so do not defer it. */ + if (status == -EAGAIN && (clone_flags & CLONE_THREAD) != 0 && + (clone_flags & CLONE_VFORK) == 0) { + parent->pending_clone_flags = clone_flags; @@ -218,14 +207,10 @@ diff --git a/src/tracee/tracee.c b/src/tracee/tracee.c child = get_tracee(parent, (pid_t) pid, true); if (child == NULL) { -@@ -628,6 +782,52 @@ +@@ -628,6 +771,46 @@ return 0; } -+/* A clone event occurs before the creating thread returns from clone. For an -+ * ambiguous thread lookup, obtain the exact TID at that syscall's exit instead -+ * of guessing from the order of /proc entries. Called before normal syscall -+ * exit handling so the registers and inherited tracee state are still intact. */ +void finish_pending_child(Tracee *parent) +{ + word_t flags = parent->pending_clone_flags; @@ -243,8 +228,6 @@ diff --git a/src/tracee/tracee.c b/src/tracee/tracee.c + status = -ESRCH; + goto failed; + } -+ /* Validate the host TID before setup. This also rejects an unmappable -+ * PID from a different PID namespace instead of adopting an unrelated task. */ + status = read_task_ids(parent->pid, &ptgid, &ppid, &tracer); + if (status < 0) + goto failed; @@ -295,17 +278,16 @@ diff --git a/src/tracee/event.c b/src/tracee/event.c diff --git a/src/tracee/tracee.h b/src/tracee/tracee.h --- a/src/tracee/tracee.h +++ b/src/tracee/tracee.h -@@ -172,6 +172,9 @@ +@@ -172,6 +172,8 @@ SIGSTOP_ALLOWED, /* Allow SIGSTOP (once the parent is known). */ SIGSTOP_PENDING, /* Block SIGSTOP until the parent is unknown. */ } sigstop; + -+ /* Nonzero only while resolving an ambiguous thread clone at its exit. */ + word_t pending_clone_flags; /* True if next SIGSYS caused by seccomp should be silently dropped * without affecting state of any registers. */ -@@ -310,6 +313,7 @@ +@@ -310,6 +312,7 @@ bool only_with_pevent, word_t wait_options); extern bool has_ptracees(const Tracee *ptracer, pid_t pid, word_t wait_options); extern int new_child(Tracee *parent, word_t clone_flags); diff --git a/tools/proot/statx_mnt_id.h b/tools/proot/statx_mnt_id.h index deb1789b8..257707657 100644 --- a/tools/proot/statx_mnt_id.h +++ b/tools/proot/statx_mnt_id.h @@ -72,11 +72,6 @@ static inline bool droiddeck_proot_statx_fill_mnt_id(pid_t pid, int fd, 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; diff --git a/tools/proot_profile.py b/tools/proot_profile.py index bfe4418f4..aa09e23d0 100644 --- a/tools/proot_profile.py +++ b/tools/proot_profile.py @@ -1,4 +1,3 @@ -"""Read cumulative PRoot handler counters from a session log without double counting snapshots.""" from __future__ import annotations import json diff --git a/tools/quest/build-driver.py b/tools/quest/build-driver.py index e72b2991e..43c7c5a3b 100644 --- a/tools/quest/build-driver.py +++ b/tools/quest/build-driver.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -"""Build the Quest allocation adapter around the unchanged bundled Android Turnip.""" import io import json from pathlib import Path @@ -34,7 +33,6 @@ def pack(entries, 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) diff --git a/tools/quest/test_first_frame.c b/tools/quest/test_first_frame.c index f58da48bb..b0f0bac2a 100644 --- a/tools/quest/test_first_frame.c +++ b/tools/quest/test_first_frame.c @@ -1,5 +1,3 @@ -/* Exercise the production vkp_render entrypoint without Android/Vulkan hardware. - * The Python runner extracts that function verbatim into the included file. */ #include #include #include @@ -18,8 +16,6 @@ static void vkp_signal_first_frame(void) { first_frames++; } static int vkp_base_black(int w, int h) { base_calls++; return base_result; } static int sc_layer_present_pass(const struct vkp_draw *draws, int n, int w, int h) { pass_calls++; - /* Production sc_layer_present_pass signals only after a content submission. - * Its no-op results (off-screen geometry or pool exhaustion) also return 0. */ if (pass_result == 0 && pass_shown) vkp_signal_first_frame(); return pass_result; } @@ -40,12 +36,10 @@ int main(void) { for (int i = 0; i < 3; i++) assert(vkp_render(1280, 720, NULL, 0) == 0); assert(base_calls == 3 && pass_calls == 0 && first_frames == 0); - /* An accepted layer operation is not necessarily a displayed client frame. */ reset(); assert(vkp_render(1280, 720, &draw, 1) == 0); assert(pass_calls == 1 && base_calls == 1 && first_frames == 0); - /* Content is the sole owner of the notification, with no second backdrop event. */ reset(); pass_shown = 1; assert(vkp_render(1280, 720, &draw, 1) == 0); assert(first_frames == 1); @@ -59,7 +53,6 @@ int main(void) { assert(vkp_render(1280, 720, NULL, 0) == -1); assert(first_frames == 0); - /* Existing routes remain responsible for their own notifications. */ for (int mode = 0; mode < 3; mode++) { reset(); if (mode == 0) quest = 0; diff --git a/tools/quest/test_presentation.c b/tools/quest/test_presentation.c index dcbad6224..9ef5a826a 100644 --- a/tools/quest/test_presentation.c +++ b/tools/quest/test_presentation.c @@ -1,4 +1,3 @@ -/* Exercise the production backdrop with mock Android buffers/transactions. */ #include "../../app/src/main/cpp/waylandcomp/src/sc_layer.c" #include "../../app/src/main/cpp/waylandcomp/src/quest_device.h" #include @@ -40,7 +39,6 @@ static void tx_delete(ASurfaceTransaction *t) { assert(t == (ASurfaceTransaction static void tx_apply(ASurfaceTransaction *t) { (void)t; applies++; } static void set_buffer(ASurfaceTransaction *t, ASurfaceControl *s, AHardwareBuffer *b, int fd) { (void)t; (void)s; assert(b == &buffer && fd == -1); - /* No garbage, including padded row bytes, reaches the display. */ for (unsigned i=0; i 0); buffers++; } @@ -79,7 +77,7 @@ int main(void) { assert(visible==ASC_VISIBILITY_SHOW && zorder==0 && opacity==ASC_TRANSPARENCY_OPAQUE); assert(destination.right==1920 && destination.bottom==1080); assert(allocs==1 && locks==1 && buffers==1 && creates==1); - assert(sc_layer_base_black(1920,1080)==0 && applies==1); /* retained, not redrawn */ + assert(sc_layer_base_black(1920,1080)==0 && applies==1); assert(sc_layer_base_black(1440,1080)==0 && destination.right==1440 && allocs==1); sc_layer_hide_base(); assert(!visible && !g_base_shown); assert(sc_layer_base_black(1440,1080)==0 && visible && allocs==1); diff --git a/tools/quick_deploy.sh b/tools/quick_deploy.sh index 5216ad740..1310a5bcf 100755 --- a/tools/quick_deploy.sh +++ b/tools/quick_deploy.sh @@ -1,5 +1,4 @@ #!/usr/bin/env bash -# Kotlin/resource-only loop: reuses the assets build_local.sh already prepared, then signs and installs. set -euo pipefail repo_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) cd "${repo_root}" @@ -23,7 +22,6 @@ while IFS= read -r dir; do done < <(ls -d "${sdk_dir}"/build-tools/* 2>/dev/null | sort -rV) [[ -n "${build_tools}" ]] || { echo "No build-tools with aapt, zipalign and apksigner under ${sdk_dir}." >&2; exit 1; } -# Runtime assets from another branch are fine for a Kotlin/UI loop; CI and releases stay strict. ./gradlew -q -PallowStaleRuntime=true :app:assembleRelease ( cd "$(dirname "${signing_env}")" diff --git a/tools/release/check_authorship.py b/tools/release/check_authorship.py index 37e2a716a..1b8542471 100644 --- a/tools/release/check_authorship.py +++ b/tools/release/check_authorship.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -"""Reject bot authors and bot co-author trailers without executing reviewed code.""" import argparse import json import re @@ -11,7 +10,6 @@ PRODUCT = re.compile( r"Amazon Q|Tabnine|github-actions|dependabot|renovate)(?![a-z0-9])|\[bot\]", re.IGNORECASE, ) -# These are also people's names, so they only count alone or followed by a product word. ASSISTANT = r"(?:Claude|Cursor|Devin|Cody|Gemini)" ASSISTANT_ALONE = re.compile(ASSISTANT, re.IGNORECASE) ASSISTANT_PRODUCT = re.compile( diff --git a/tools/release/check_session_assets.py b/tools/release/check_session_assets.py index 8556f83fe..2042e470f 100644 --- a/tools/release/check_session_assets.py +++ b/tools/release/check_session_assets.py @@ -1,4 +1,3 @@ -"""Reject an APK with missing or stale session helpers, including cached local bundles.""" from pathlib import Path import re import sys diff --git a/tools/release/install_authorship_hooks.py b/tools/release/install_authorship_hooks.py index dae4eb517..9f7dda1a1 100644 --- a/tools/release/install_authorship_hooks.py +++ b/tools/release/install_authorship_hooks.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -"""Install shared worktree hooks while preserving existing executable hooks.""" from pathlib import Path import shutil import subprocess diff --git a/tools/release/runtime_inputs.py b/tools/release/runtime_inputs.py index 9122707be..9682ef46a 100644 --- a/tools/release/runtime_inputs.py +++ b/tools/release/runtime_inputs.py @@ -1,4 +1,3 @@ -"""Attest prepared runtime inputs and reject stale local or CI APK payloads.""" import hashlib import gzip import json @@ -13,7 +12,6 @@ from zipfile import ZipFile ROOT = Path(__file__).resolve().parents[2] MANIFEST = Path('app/build/runtime-inputs.json') PACKS = Path('app/build/packs') -# Runtime trees shipped as add-on packs (tools/packs/build.sh) rather than inside the APK. PACKED = {'assets/linuxfs/usr/local/lib/droiddeck-xbox/': 'xbox', 'assets/linuxfs/usr/local/lib/droiddeck-comet/': 'comet', 'assets/directaudio/': 'directaudio'} PACKED_FILES = {'lib/arm64-v8a/libproot.so': ('proot', 'libproot.so'), diff --git a/tools/release/update_catalog.py b/tools/release/update_catalog.py index 808e576a0..94ecd1ce5 100755 --- a/tools/release/update_catalog.py +++ b/tools/release/update_catalog.py @@ -1,9 +1,4 @@ #!/usr/bin/env python3 -"""Build and publish the one-file update catalog consumed by DroidDeck. - -The catalog is derived from GitHub releases, but the Android app never has to infer -channels, parse release notes, dereference tags, or spend unauthenticated API quota. -""" from __future__ import annotations @@ -230,7 +225,6 @@ def release_entry( def preview_history_entry(release: dict, preview: dict, variants: list[tuple[str, str]]): - """Return compact history metadata for one complete, signed main build.""" tag = release.get("tag_name") or "" tag_match = MAIN_TAG.fullmatch(tag) commit = (preview.get("commit") or "").lower() @@ -259,7 +253,6 @@ def preview_history_entry(release: dict, preview: dict, variants: list[tuple[str def recent_previews(entries: list[dict], limit: int = 10) -> list[dict]: - """Keep the newest distinct Preview commits, newest first.""" result = [] seen = set() for entry in sorted(entries, key=lambda item: item["publishedAt"], reverse=True): @@ -288,8 +281,6 @@ def main_commit(release: dict) -> str | None: def stable_app_release(releases: list[dict]) -> dict | None: - """The newest published app release. The repo also publishes gamescope and desktop-package - releases that GitHub may mark latest, so an app release is one shipping DroidDeck-.apk.""" apps = [ r for r in releases if not r.get("draft") and not r.get("prerelease") diff --git a/tools/release/validate_apk.py b/tools/release/validate_apk.py index 5969b1ae8..3ff90c2e2 100644 --- a/tools/release/validate_apk.py +++ b/tools/release/validate_apk.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -"""Publish the APK result only after trusted source and metadata validation.""" import json import os import subprocess diff --git a/tools/syncthing/build.sh b/tools/syncthing/build.sh index 4b253c7ca..ca9338eb6 100755 --- a/tools/syncthing/build.sh +++ b/tools/syncthing/build.sh @@ -33,10 +33,8 @@ EPOCH=$(git -C "$SRC" show -s --format=%ct HEAD) cache=${DROIDDECK_BUILD_CACHE:-"${HOME}/.cache/droiddeck-build"} WORK=$(mktemp -d) trap 'rm -rf "$WORK"' EXIT -# build.go writes its output into the source tree, so build from a copy. cp -a "$SRC" "$WORK/syncthing" rm -f "$WORK/syncthing/.git" -# -checklinkname=0: the fork's anet dependency links against internal net symbols. ( cd "$WORK/syncthing" export GOTOOLCHAIN="go${GO_VERSION}" GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOFLAGS=-buildvcs=false diff --git a/tools/tests/cursor_composite_harness.c b/tools/tests/cursor_composite_harness.c index f4dede88c..e1f267794 100644 --- a/tools/tests/cursor_composite_harness.c +++ b/tools/tests/cursor_composite_harness.c @@ -29,15 +29,12 @@ int main(void) { assert(cursor_scene_positions(3840, CARDBOARD_SOURCE_SBS, 1920, 0, out) == 2); assert(out[0] == 1920 && out[1] == 0); - /* A hotspot that pushes the image across the seam must not move the mirror to the - * other eye: the pointer decides the eye, the hotspot only shifts the image. */ assert(cursor_scene_positions(1920, CARDBOARD_SOURCE_SBS, 960, 32, out) == 2); assert(out[0] == 928 && out[1] == -32); assert(cursor_scene_positions(1920, CARDBOARD_SOURCE_SBS, 959, 32, out) == 2); assert(out[0] == 927 && out[1] == 1887); - /* Each copy is cropped to its own eye, so nothing bleeds into the other eye's half. */ assert(cursor_clip_to_eye(1920, -1, 100, 32, &dx, &sx, &dw) == 1); assert(dx == 100 && sx == 0 && dw == 32); diff --git a/tools/tests/pointer_release_harness.c b/tools/tests/pointer_release_harness.c index 8369eb377..aaf58841f 100644 --- a/tools/tests/pointer_release_harness.c +++ b/tools/tests/pointer_release_harness.c @@ -23,8 +23,6 @@ static void wl_pointer_send_button(struct wl_resource *r, uint32_t serial, r->held &= ~(1u << (button - 0x110)); r->releases++; } -/* The old dispatcher calls this even for releases, emitting stale coordinates. - * Recording this boundary detects that unwanted motion/focus routing path. */ static void pointer_input(double x, double y, int relative, uint32_t button, int pressed) { assert(x == 0 && y == 0 && relative == 1 && button >= 0x110 && button <= 0x112); assert(pressed == 1); diff --git a/tools/tests/test_client_core_mask.py b/tools/tests/test_client_core_mask.py index 59178b4db..9462c43af 100644 --- a/tools/tests/test_client_core_mask.py +++ b/tools/tests/test_client_core_mask.py @@ -1,4 +1,3 @@ -"""Single-core Steam affinity is rechecked even if its thread count stayed the same.""" import subprocess import tempfile import unittest diff --git a/tools/tests/test_cursor_composite.py b/tools/tests/test_cursor_composite.py index 5edf706c8..b3788d348 100644 --- a/tools/tests/test_cursor_composite.py +++ b/tools/tests/test_cursor_composite.py @@ -1,4 +1,3 @@ -"""Exercise the compositor's real cursor placement rule for glasses output.""" import os from pathlib import Path import re diff --git a/tools/tests/test_desktop_open.py b/tools/tests/test_desktop_open.py index ef299963b..39eee38cf 100644 --- a/tools/tests/test_desktop_open.py +++ b/tools/tests/test_desktop_open.py @@ -316,7 +316,6 @@ class GameFolderTest(Temp): def appinfo(apps): - """appinfo.vdf as the client writes it (format 29: keys in a string table).""" strings = [] def key(name): diff --git a/tools/tests/test_droiddeck_agent.py b/tools/tests/test_droiddeck_agent.py index 7989afa9d..02e53f960 100644 --- a/tools/tests/test_droiddeck_agent.py +++ b/tools/tests/test_droiddeck_agent.py @@ -1,4 +1,3 @@ -"""The guest agent: its request files, exec, errors and the DevTools client, without a session.""" import base64 import hashlib import importlib.machinery @@ -26,7 +25,6 @@ loader.exec_module(agent) class FakeDevTools(threading.Thread): - """GET /json lists one target; its WebSocket answers Runtime.evaluate the way Chromium does.""" def __init__(self, answer): super().__init__(daemon=True) @@ -857,7 +855,6 @@ class VrFocusTest(unittest.TestCase): self.assertEqual(focus.order([42, 9, 7], [7, 42], None), [9, 7]) self.assertIsNone(focus.order([9, 7], [7], 42)) self.assertEqual(focus.order([9, 7], [7, 42], 42), [42, 9, 7]) - # Do not overwrite a different focus order installed after our last write. self.assertIsNone(focus.order([123, 7], [7, 42], None)) def test_scene_requires_live_server_and_game_environment(self): diff --git a/tools/tests/test_droiddeckctl.py b/tools/tests/test_droiddeckctl.py index 82ff7daa0..f3cc0e620 100644 --- a/tools/tests/test_droiddeckctl.py +++ b/tools/tests/test_droiddeckctl.py @@ -1,4 +1,3 @@ -"""droiddeckctl and droiddeck-scenario against a fake adb: the JSON contract and exit codes.""" import base64 import contextlib import importlib.machinery @@ -88,8 +87,6 @@ def timestats_record(name, frames, histogram, hz): f"present2present histogram is as below:\n{bins}\nlatch2present histogram is as below:\n8ms={frames}\n") -# A capture where the panel went from 60 to 120 Hz: the game layer is split in two records. The -# activity's own window and an unrelated app draw too, and must not count. TIMESTATS_SPLIT = "SurfaceFlinger TimeStats:\ntotalFrames = 2000\n" + "".join([ timestats_record("droiddeck_wayland_game#6110", 900, {16: 700, 33: 199}, 60), timestats_record("droiddeck_wayland_game#6110", 900, {16: 850, 25: 49}, 120), @@ -99,7 +96,6 @@ TIMESTATS_SPLIT = "SurfaceFlinger TimeStats:\ntotalFrames = 2000\n" + "".join([ timestats_record("StatusBar#106", 1, {}, 60), ]) -# A build that presents the game on the session's SurfaceView instead of a layer of its own. TIMESTATS_SURFACEVIEW = "SurfaceFlinger TimeStats:\n" + "".join([ timestats_record("a1b2c3 SurfaceView[com.droiddeck.launcher/com.droiddeck.launcher.SessionActivity]#77", 1800, {16: 1796, 33: 3}, 120), @@ -155,7 +151,7 @@ class DroiddeckctlTest(FakeAdbTest): with unittest.mock.patch.dict(os.environ, {"DROIDDECK_PACKAGE": "com.antutu.benchmark.full"}): variant = load("droiddeckctl") self.assertEqual("content://com.antutu.benchmark.full.agent", variant.AUTHORITY) - with self.assertRaises(variant.ControlError): # the fake adb does not answer `am start` + with self.assertRaises(variant.ControlError): variant.start_agent_activity(str(self.adb), "fake-1", {"mode": "steam"}) started = [e["args"] for e in self.calls() if e["args"][:3] == ["shell", "am", "start"]] self.assertIn("com.antutu.benchmark.full/com.droiddeck.launcher.agent.AgentStartActivity", started[0]) diff --git a/tools/tests/test_frame_limit_sync.py b/tools/tests/test_frame_limit_sync.py index 0cbdc18fb..b14b0771a 100644 --- a/tools/tests/test_frame_limit_sync.py +++ b/tools/tests/test_frame_limit_sync.py @@ -1,4 +1,3 @@ -"""The Quick Access Frame Limit follows the panel when its refresh rate changes.""" from pathlib import Path import runpy import subprocess diff --git a/tools/tests/test_game_environment.py b/tools/tests/test_game_environment.py index d27520dd4..fb3317b3d 100644 --- a/tools/tests/test_game_environment.py +++ b/tools/tests/test_game_environment.py @@ -246,7 +246,6 @@ class DirtTelemetryFixTest(unittest.TestCase): class EgoCorePlanTest(unittest.TestCase): - """DiRT 3's restrictions as shipped: plans for 2, 3, 4, 6 and 8+ cores, one core for anything else.""" @staticmethod def rule(op, value, worker_map): @@ -460,7 +459,6 @@ class GameDebugLoggingTest(unittest.TestCase): result = launch([str(probe), verb], {**env, "STEAM_COMPAT_DATA_PATH": str(home / "compatdata" / prefix)}) self.assertEqual(result["env"]["PROTON_LOG"], "0") self.assertEqual(result["env"]["WINEDEBUG"], "-all") - # Non-Steam shortcuts use the same normalized unsigned compatdata identity. result = launch(command, {**env, "STEAM_COMPAT_DATA_PATH": str(home / "compatdata/-1")}) self.assertEqual(result["env"]["PROTON_LOG"], "1") self.assertFalse((home / "never-created").exists()) diff --git a/tools/tests/test_gamescope_cursor_shm.py b/tools/tests/test_gamescope_cursor_shm.py index 35ca27acb..e88b19975 100644 --- a/tools/tests/test_gamescope_cursor_shm.py +++ b/tools/tests/test_gamescope_cursor_shm.py @@ -106,9 +106,6 @@ def main(): surface = alloc() send(comp, 0, U(surface)) if not args.unassigned_control: - # wlroots 0.20.2 assigns the cursor role before forwarding this serial - # to Gamescope, so zero avoids needing pointer focus but still tests the - # role guard in xwayland_surface_commit. send(pointer, 0, U(0, surface, 0, 0)) roundtrip() before = count() diff --git a/tools/tests/test_hud_frames.py b/tools/tests/test_hud_frames.py index 381c18e37..e7af1aa5e 100644 --- a/tools/tests/test_hud_frames.py +++ b/tools/tests/test_hud_frames.py @@ -14,7 +14,7 @@ READER = ROOT / "app/src/main/cpp/waylandcomp/src/hudframes.c" def patch_writer(): added = [line[1:] for line in PATCH.read_text().splitlines() if line.startswith("+") and not line.startswith("+++")] text = "\n".join(added) - start = text.index("// DroidDeck's own HUD reads") + start = text.index("static constexpr uint32_t k_uDroidDeckHudFramesMagic") end = text.index("\n}\n", text.index("static void publish_droiddeck_hud_info()", start)) + 3 return text[start:end] diff --git a/tools/tests/test_launcher_cores.py b/tools/tests/test_launcher_cores.py index 4f05df242..b3e9fc929 100644 --- a/tools/tests/test_launcher_cores.py +++ b/tools/tests/test_launcher_cores.py @@ -19,7 +19,6 @@ ALL = set(range(8)) class FakeCores: - """Per-thread masks and priorities, with every change recorded.""" def __init__(self): self.state = {} diff --git a/tools/tests/test_login1.py b/tools/tests/test_login1.py index e257ccf81..fe69c209e 100644 --- a/tools/tests/test_login1.py +++ b/tools/tests/test_login1.py @@ -1,4 +1,3 @@ -"""Real login1 sleep handshakes; run with dbus-run-session and python3-gi.""" import os from pathlib import Path import subprocess diff --git a/tools/tests/test_msi_install.py b/tools/tests/test_msi_install.py index dda3eeea5..60d8fc8df 100644 --- a/tools/tests/test_msi_install.py +++ b/tools/tests/test_msi_install.py @@ -10,7 +10,6 @@ COMPONENTS = runpy.run_path(str(BIN / "droiddeck-wincomponents")) class FakePackage: - """A package's tables in memory, as msiinfo would give them.""" def __init__(self, tables, summary=None): self.path = "fake.msi" diff --git a/tools/tests/test_netmanager.py b/tools/tests/test_netmanager.py index 69159fdfc..5f65d97d4 100644 --- a/tools/tests/test_netmanager.py +++ b/tools/tests/test_netmanager.py @@ -1,7 +1,3 @@ -"""NetworkManager contract and real D-Bus handovers (requires python3-gi and dbus). - -Run on Linux: dbus-run-session -- python3 -m unittest discover -s tools/tests -p test_netmanager.py -""" import importlib.machinery import importlib.util import json diff --git a/tools/tests/test_openat2_preload.py b/tools/tests/test_openat2_preload.py index da0a792d3..cbaefaab2 100644 --- a/tools/tests/test_openat2_preload.py +++ b/tools/tests/test_openat2_preload.py @@ -9,8 +9,6 @@ import unittest PRELOAD = Path(__file__).resolve().parents[1] / "linuxfs/preload" FASTPATH = Path(__file__).resolve().parents[1] / "proot/fastpath/fastpath.c" -# Opens each "\t\t" case with openat2(RESOLVE_IN_ROOT) as FEX does: first through the kernel's own -# openat2 on argv[1] when it has one, then, with openat2 failing ENOSYS as under proot, through the preload on argv[2]. PROGRAM = r""" #define _GNU_SOURCE #include @@ -225,7 +223,6 @@ int main(int argc, char **argv) { O_PATH = getattr(os, "O_PATH", 0o10000000) -# (dirfd: r = the tree, o = "/"), flags, path, expected: a path in the tree, an errno name, or "proc" for a file outside it. CASES = [ ("r", os.O_RDONLY, "etc/alternatives/hn", "etc/hostname"), ("r", os.O_RDONLY, "/lib64/x86_64-linux-gnu/libfoo.so.1", "usr/lib/x86_64-linux-gnu/libfoo.so.1.2"), @@ -256,7 +253,6 @@ CASES = [ ("o", os.O_RDONLY, "proc/self/status", "proc"), ] -# Only the preload: an absolute link to a path missing in the tree but present outside it, as droiddeck-fex's rootfs has. HOST_CASES = [ ("r", os.O_RDONLY, "lib/x86_64-linux-gnu/libbar.so.2", "host:libbar.so.2"), ("r", os.O_RDONLY, "hostdir/sub/data", "host:sub/data"), diff --git a/tools/tests/test_picture_position_mapping.py b/tools/tests/test_picture_position_mapping.py index 597fec938..2c9eb8e32 100644 --- a/tools/tests/test_picture_position_mapping.py +++ b/tools/tests/test_picture_position_mapping.py @@ -1,4 +1,3 @@ -"""Test the renderer's actual map and inverse map without a Vulkan device.""" import os from pathlib import Path import shutil diff --git a/tools/tests/test_pointer_release.py b/tools/tests/test_pointer_release.py index 9da5672e3..37e096c7a 100644 --- a/tools/tests/test_pointer_release.py +++ b/tools/tests/test_pointer_release.py @@ -1,4 +1,3 @@ -"""Exercise the compositor's real button dispatcher with a recording Wayland sink.""" import os from pathlib import Path import shutil diff --git a/tools/tests/test_preload_snapshots.py b/tools/tests/test_preload_snapshots.py index 464b24e7b..6629d1ab2 100644 --- a/tools/tests/test_preload_snapshots.py +++ b/tools/tests/test_preload_snapshots.py @@ -30,7 +30,6 @@ class PreloadSnapshotTest(unittest.TestCase): directory = tempfile.TemporaryDirectory() self.addCleanup(directory.cleanup) self.root = Path(directory.name) - # Real repository inputs exercise the transitive include walker and build recipe. for group in snapshots.INPUTS: for name in snapshots.source_files(group): if name.startswith('@'): diff --git a/tools/tests/test_quest_presentation.py b/tools/tests/test_quest_presentation.py index 9c87926e4..81b726a56 100644 --- a/tools/tests/test_quest_presentation.py +++ b/tools/tests/test_quest_presentation.py @@ -10,8 +10,6 @@ ROOT = Path(__file__).resolve().parents[2] @unittest.skipUnless(shutil.which('cc'), 'needs a C compiler') class QuestBackdropTest(unittest.TestCase): def test_first_frame_requires_client_content(self): - # Compile the real entrypoint with controlled layer outcomes, including the - # successful no-op that previously dismissed the loading UI on a black frame. source = (ROOT / 'app/src/main/cpp/waylandcomp/src/vk_present.c').read_text() start = source.index('\nint vkp_render(') end = source.index('\nint vkp_render_hdr(', start) diff --git a/tools/tests/test_run_windows.py b/tools/tests/test_run_windows.py index 143fba925..ac1be22f4 100644 --- a/tools/tests/test_run_windows.py +++ b/tools/tests/test_run_windows.py @@ -1,4 +1,3 @@ -"""Exercise the Windows launcher without building an APK or contacting a phone.""" import os from pathlib import Path import shutil diff --git a/tools/tests/test_session_cursor_routing.py b/tools/tests/test_session_cursor_routing.py index 60d28edc4..dd2c77d94 100644 --- a/tools/tests/test_session_cursor_routing.py +++ b/tools/tests/test_session_cursor_routing.py @@ -1,4 +1,3 @@ -"""Gamescope composites the cursor only when a Steam session asks for it.""" import os from pathlib import Path import re diff --git a/tools/tests/test_session_guest_command.py b/tools/tests/test_session_guest_command.py index d6f352a8e..5938b8c45 100644 --- a/tools/tests/test_session_guest_command.py +++ b/tools/tests/test_session_guest_command.py @@ -1,9 +1,3 @@ -"""Exercise the literal launch prefix from SessionService without an Android runtime. - -This is a focused source/exec regression check, not a Kotlin or PRoot integration -test. PRoot executes the first guest argument directly; KEY=VALUE is meaningful -only after /usr/bin/env. Read the production prefix rather than duplicate it. -""" import json from pathlib import Path import re diff --git a/tools/tests/test_session_pacing.py b/tools/tests/test_session_pacing.py index 84962fe9e..67b616669 100644 --- a/tools/tests/test_session_pacing.py +++ b/tools/tests/test_session_pacing.py @@ -1,4 +1,3 @@ -"""Steam owns game caps; the nested compositor follows the output refresh rate.""" import os from pathlib import Path import subprocess diff --git a/tools/tests/test_sign_apk_renames.py b/tools/tests/test_sign_apk_renames.py index 4859f4a2e..bd111fa0f 100644 --- a/tools/tests/test_sign_apk_renames.py +++ b/tools/tests/test_sign_apk_renames.py @@ -9,8 +9,6 @@ PACKAGE = 'com.droiddeck.launcher' class SignApkRenamesTest(unittest.TestCase): def test_variants_rename_every_authority_and_task_named_after_the_package(self): - # sign-apk.sh renames a fixed list of names for the pubg/antutu/ludashi variants and fails the - # main build on any authority or task affinity it leaves on the base package; PR builds do not sign. manifest = (ROOT / 'app/src/main/AndroidManifest.xml').read_text() names = set() for attribute in ('authorities', 'taskAffinity'): diff --git a/tools/tests/test_steam_cold_launch.py b/tools/tests/test_steam_cold_launch.py index 162037eba..1695ad0e1 100644 --- a/tools/tests/test_steam_cold_launch.py +++ b/tools/tests/test_steam_cold_launch.py @@ -1,4 +1,3 @@ -"""Retry a dropped shortcut without duplicating a launch that is still preparing its first window.""" import os from pathlib import Path import subprocess @@ -29,7 +28,6 @@ class SteamColdLaunchTest(unittest.TestCase): self.agent_count = self.root / "agent-count" self.focus_count = self.root / "focus-count" - # FOCUS lists the focused app per xprop call; the last value repeats. xprop = self.bin / "xprop" xprop.write_text( "#!/bin/sh\n" diff --git a/tools/tests/test_steam_compat_x64.py b/tools/tests/test_steam_compat_x64.py index 3d4b069e0..9f9abc478 100644 --- a/tools/tests/test_steam_compat_x64.py +++ b/tools/tests/test_steam_compat_x64.py @@ -50,14 +50,12 @@ CONFIG = '''"InstallConfigStore" class X64RepointTest(unittest.TestCase): - """steam-compatibility --x64: the x86-64 client's Proton entries go to its arm64 wrapper.""" def setUp(self): self.home = tempfile.TemporaryDirectory() self.root = Path(self.home.name) / "Steam" (self.root / "config").mkdir(parents=True) (self.root / "config" / "config.vdf").write_text(CONFIG) - # A wrapper of its own whose name starts with "proton": kept, never repointed. own = self.root / "compatibilitytools.d" / "proton-cachyos-a64" own.mkdir(parents=True) (own / "compatibilitytool.vdf").write_text('"compatibilitytools"\n{\n\t"compat_tools"\n\t{\n\t\t"proton-cachyos-a64"\n\t\t{\n\t\t}\n\t}\n}\n') @@ -82,12 +80,10 @@ class X64RepointTest(unittest.TestCase): self.assertEqual(m["550"], "GE-Proton11-7-a64") self.assertEqual(m["620"], "proton-cachyos-a64") self.assertEqual(m["730"], "steamlinuxruntime") - # The arm64 client's own Proton name, which this client ignores. self.assertEqual(m["880"], "GE-Proton11-7-a64") self.assertEqual(m["0"], "GE-Proton11-7-a64") def test_repoints_arm64_name_even_with_its_stray_tool(self): - # An arm64 compat pass that ran against this client also left its tool folder here. stray = self.root / "compatibilitytools.d" / "droiddeck-proton-arm64" stray.mkdir(parents=True) (stray / "compatibilitytool.vdf").write_text('"compatibilitytools"\n{\n\t"compat_tools"\n\t{\n\t\t"droiddeck-proton-arm64"\n\t\t{\n\t\t}\n\t}\n}\n') diff --git a/tools/tests/test_steam_desktop_launch.py b/tools/tests/test_steam_desktop_launch.py index 37e1a3ed8..7d5064087 100644 --- a/tools/tests/test_steam_desktop_launch.py +++ b/tools/tests/test_steam_desktop_launch.py @@ -1,4 +1,3 @@ -"""Desktop Steam stays on KDE; explicit Big Picture still switches sessions.""" import fcntl import os from pathlib import Path diff --git a/tools/tests/test_steam_install.py b/tools/tests/test_steam_install.py index 6a02bcb0a..81e2724cb 100644 --- a/tools/tests/test_steam_install.py +++ b/tools/tests/test_steam_install.py @@ -280,8 +280,6 @@ done(28 if end < len(data) else 0) self.assertFalse(self.requests.exists()) def publish(self, **parts): - """The CDN's client as these parts (block name -> byte count of incompressible content), - listed with sizes as Valve's manifest lists them.""" files = {} blocks = '' for block, length in parts.items(): diff --git a/tools/tests/test_store_launch.py b/tools/tests/test_store_launch.py index 5dadcfb2f..fcdcbb21e 100644 --- a/tools/tests/test_store_launch.py +++ b/tools/tests/test_store_launch.py @@ -1,6 +1,3 @@ -"""droiddeck-store-launch: a launch from anywhere - the Steam client's own Play button included - -asks the app for an Epic game's sign-in code before Proton starts. These pin how it recognises a -store game, how it asks, and that both Proton launchers call it.""" import importlib.machinery import importlib.util import json diff --git a/tools/tests/test_store_shortcuts.py b/tools/tests/test_store_shortcuts.py index 082011e8f..1c454fa77 100644 --- a/tools/tests/test_store_shortcuts.py +++ b/tools/tests/test_store_shortcuts.py @@ -1,6 +1,3 @@ -"""The shortcuts writer and a store install: the app registers a game GOG, Epic or Amazon -installed through the same listing as an added folder, pointing the shortcut at the game's -launcher .bat when it has one. These pin the parts of the writer that contract relies on.""" import importlib.machinery import importlib.util import json @@ -28,8 +25,6 @@ LAUNCHER = FOLDER + '/.droiddeck-launch.bat' def app_id(exe, name): - """The appid the app computes for a shortcut (AddedGames.scanGame): CRC32 of the quoted exe - and the name, high bit set - what Steam itself derives for a shortcut it is given.""" return (zlib.crc32(('"%s"' % exe + name).encode()) | 0x80000000) & 0xFFFFFFFF diff --git a/tools/tests/test_termux_jni.py b/tools/tests/test_termux_jni.py index 6fd3d9787..94fe29fd3 100644 --- a/tools/tests/test_termux_jni.py +++ b/tools/tests/test_termux_jni.py @@ -12,7 +12,6 @@ TERMUX_DIR = TESTS.parents[1] / "app/src/main/cpp/termux" def jni_include_dir(): - """A directory holding jni.h: JNI_INCLUDE, then the JDK, then the Android NDK.""" candidates = [os.environ.get("JNI_INCLUDE")] java_home = os.environ.get("JAVA_HOME") if java_home: @@ -29,7 +28,6 @@ def jni_include_dir(): def include_flags(): - """-I flags for jni.h, plus the JDK's per-platform directory that holds jni_md.h.""" base = jni_include_dir() flags = ["-I", base] for sub in glob.glob(os.path.join(base, "*")): diff --git a/tools/tests/test_winebus_rumble_preload.py b/tools/tests/test_winebus_rumble_preload.py index 82e3b0112..abd60c274 100644 --- a/tools/tests/test_winebus_rumble_preload.py +++ b/tools/tests/test_winebus_rumble_preload.py @@ -8,7 +8,6 @@ import unittest PRELOAD = Path(__file__).resolve().parents[1] / "linuxfs/preload" -# Stands in for libfakeinput.so, which ld.so.preload lists after libblsession.so. SHIM = r""" #include #include diff --git a/tools/ubuntu/patches/kwin/0004-keep-tranches-of-the-main-device.patch b/tools/ubuntu/patches/kwin/0004-keep-tranches-of-the-main-device.patch index 5e4050e62..9165715ed 100644 --- a/tools/ubuntu/patches/kwin/0004-keep-tranches-of-the-main-device.patch +++ b/tools/ubuntu/patches/kwin/0004-keep-tranches-of-the-main-device.patch @@ -10,7 +10,7 @@ Subject: [PATCH] Keep dmabuf tranches that target the main device diff --git a/src/wayland-client/linuxdmabuf.cpp b/src/wayland-client/linuxdmabuf.cpp --- a/src/wayland-client/linuxdmabuf.cpp +++ b/src/wayland-client/linuxdmabuf.cpp -@@ -130,15 +130,17 @@ +@@ -130,15 +130,16 @@ drmFreeDevice(&mainDevice); drmFreeDevice(&trancheDevice); }); @@ -30,7 +30,6 @@ diff --git a/src/wayland-client/linuxdmabuf.cpp b/src/wayland-client/linuxdmabuf + return; + } + if (!drmDevicesEqual(mainDevice, trancheDevice)) { -+ // TODO take multi GPU handling into account as well + qCDebug(KWIN_WAYLAND_CLIENT, "Ignoring tranche from non-main device"); + return; + } diff --git a/tools/vr/build_windows.py b/tools/vr/build_windows.py index 06a1dde74..af8fa4d46 100644 --- a/tools/vr/build_windows.py +++ b/tools/vr/build_windows.py @@ -129,8 +129,6 @@ def build(folder, include, root=ROOT, image=IMAGE): workspace.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix='vr-build-', dir=workspace) as temp: snapshot = Path(temp).resolve() - # build.sh's recursive output cleanup and TemporaryDirectory cleanup are - # confined to this new, verified directory, never the real checkout. if not snapshot.is_relative_to(workspace): raise ValueError('Temporary VR workspace escapes the checkout') inspect = subprocess.run([docker, 'image', 'inspect', '--format', '{{.Id}}', image], @@ -159,7 +157,6 @@ def build(folder, include, root=ROOT, image=IMAGE): manifest = {'version': 1, 'source_commit': revision, 'sources': hashes(input_files), 'headers': hashes(input_headers), 'compiler_image': image_id, 'outputs': hashes(files), 'native_tests': 'passed in tools/vr/build.sh'} - # Publish only after the complete build and native regression tests pass. for name, data in files.items(): target = folder / name if not target.resolve().is_relative_to(folder): diff --git a/tools/vr/src/driver_droiddeck.cpp b/tools/vr/src/driver_droiddeck.cpp index 1398a1d05..2535e04cc 100644 --- a/tools/vr/src/driver_droiddeck.cpp +++ b/tools/vr/src/driver_droiddeck.cpp @@ -281,7 +281,6 @@ public: const uint32_t create = 0x8 ; for (int i = 0; i < 3; i++) { SharedTextureHandle_t h = 0; - // Compute access makes SteamVR substitute UNORM for sRGB, darkening direct compositor output. if (!VRIPCResourceManager()->NewSharedVulkanImage(d->nFormat, d->nWidth, d->nHeight, true, false, false, 1, 1, create, usage, &h)) { Log("NewSharedVulkanImage failed"); @@ -1276,7 +1275,6 @@ public: } private: - // Face buttons, menus and the stick, for both the game pad and optical hands. void UpdatePadButtons(uint32_t b, float stickX, float stickY, bool stickToGame) { auto in = VRDriverInput(); const bool touch = stickToGame && stickX * stickX + stickY * stickY > 0.01f; @@ -1289,7 +1287,6 @@ private: in->UpdateBooleanComponent(padClick_, (b & (right_ ? (PAD_A | PAD_R3) : PAD_X)) != 0, 0); return; } - // Touch and Index: face buttons are their own inputs and the stick is a thumbstick. const bool primary = b & (right_ ? PAD_A : PAD_X), secondary = b & (right_ ? PAD_B : PAD_Y); const bool both = (b & PAD_L3) && (b & PAD_R3); if (menu_) in->UpdateBooleanComponent(menu_, !right_ && (b & (PAD_START | PAD_SELECT)), 0); @@ -1754,7 +1751,6 @@ public: void SetHands(CardboardController *left, CardboardController *right) { hands_[0] = left; hands_[1] = right; } void SetSticksMoveHands(bool on) { sticksMoveHands_ = on; } - // Called only from the pose thread: a replaced pair is disconnected there, so it never races an update. CardboardController *ActiveHand(int side) { CardboardController *wanted = hands_[side].load(); if (wanted != active_[side]) { @@ -1908,10 +1904,7 @@ private: target.frequency = std::max(pulse.fFrequency, 0.f); __atomic_store_n(&haptics_->seq, (sequence | 1u) + 1u, __ATOMIC_RELEASE); } - // SteamVR fixes a device's input profile when it activates, so each type gets its own pair, - // added the first time it is chosen; devices cannot be removed, only disconnected. void ApplyPadConfig(bool initial) { - // The file is a few bytes, so compare what it says; two quick writes can share an mtime. const PadControllerConfig pads = read_pad_controller_config("/run/droiddeck/" DROIDDECK_VR_CONTROLLERS_FILE); if (!initial && pads.profile == configProfile_ && pads.sticksMoveHands == configSticks_) return; configProfile_ = pads.profile; diff --git a/tools/vr/src/fence_tracker.h b/tools/vr/src/fence_tracker.h index d016d9ded..273ecbd4d 100644 --- a/tools/vr/src/fence_tracker.h +++ b/tools/vr/src/fence_tracker.h @@ -4,8 +4,6 @@ #include #include -/* All pool operations require the caller's tracker lock. Reserved fences are - * invisible to the completer until vkQueueSubmit has returned successfully. */ struct tracked { struct tracked *next; VkDevice dev; @@ -33,8 +31,6 @@ static struct tracked *track_pool_take(struct tracked **pool, VkDevice dev, PFN_ t.destroy = (PFN_vkDestroyFence)gdpa(dev, "vkDestroyFence"); PFN_vkCreateFence create = (PFN_vkCreateFence)gdpa(dev, "vkCreateFence"); if (!t.wait || !t.reset || !t.destroy || !create) return NULL; - /* Never wait for a slot: the next submission may signal a semaphore that - * every outstanding fence is waiting on. Grow only at the high-water mark. */ struct tracked *slot = idle ? idle : calloc(1, sizeof(*slot)); if (!slot) return NULL; VkFenceCreateInfo fci = {VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, NULL, 0}; @@ -69,8 +65,6 @@ static int track_pool_reap(struct tracked *pool, uint32_t issued, uint32_t *done uint32_t through = issued; for (struct tracked *t = pool; t; t = t->next) { if (t->state != TRACK_SUBMITTED) continue; - /* Scan all fences without blocking behind an earlier submission on a - * different queue. Device loss retires fences as in the previous path. */ if (t->wait(t->dev, 1, &t->fence, VK_TRUE, 0) == VK_TIMEOUT) { pending = 1; if ((int32_t)(t->mark - through) <= 0) through = t->mark - 1; @@ -79,9 +73,6 @@ static int track_pool_reap(struct tracked *pool, uint32_t issued, uint32_t *done t->state = TRACK_IDLE; } } - /* done is a contiguous watermark, not a count of completed fences. Later - * work may free its slot but cannot release an unfinished presenter frame. - * Signed differences also handle the uint32_t counter wrapping. */ *done = through; return pending; } @@ -92,7 +83,6 @@ static int track_pool_device_busy(struct tracked *pool, VkDevice dev) { return 0; } -/* The caller must first wait for all this device's reservations and work. */ static void track_pool_forget(struct tracked **pool, VkDevice dev) { for (struct tracked **p = pool; *p;) { struct tracked *t = *p; diff --git a/tools/vr/src/hand_skeleton.h b/tools/vr/src/hand_skeleton.h index 8ade4a609..0bb9f1e78 100644 --- a/tools/vr/src/hand_skeleton.h +++ b/tools/vr/src/hand_skeleton.h @@ -1,5 +1,4 @@ -// Adapted from Valve's OpenVR handskeletonsimulation driver sample (BSD-3-Clause, see OPENVR_LICENSE), -// which follows Moshi Turner's hand simulation in Monado. +// Adapted from Valve's OpenVR handskeletonsimulation driver sample (BSD-3-Clause, see OPENVR_LICENSE), which follows Moshi Turner's hand simulation in Monado. #pragma once #include "arm_model.h" diff --git a/tools/vr/src/pad_controller_config.h b/tools/vr/src/pad_controller_config.h index 387832b3a..fd1e08d52 100644 --- a/tools/vr/src/pad_controller_config.h +++ b/tools/vr/src/pad_controller_config.h @@ -11,7 +11,6 @@ struct PadControllerConfig { bool sticksMoveHands = false; }; -// One key=value per line, written by the app before SteamVR starts. inline PadControllerConfig pad_controller_config(const char *text) { PadControllerConfig config; if (!text) return config; diff --git a/tools/vr/src/presenter.cpp b/tools/vr/src/presenter.cpp index 20b0ad63c..efc110640 100644 --- a/tools/vr/src/presenter.cpp +++ b/tools/vr/src/presenter.cpp @@ -606,7 +606,6 @@ private: im.height = p.height; im.producerPid = p.producerPid; im.format = (VkFormat)p.format; - // Dedicated OPAQUE_FD imports must match SteamVR's exported image description. SharedEyeImage description(im.format, p.width, p.height); VkResult r = vk_.CreateImage(dev_, &description.info, nullptr, &im.image); if (r != VK_SUCCESS) { Log("presenter: import image %d", r); close(p.fd); return; } diff --git a/tools/vr/src/scheduling.c b/tools/vr/src/scheduling.c index df8919650..381397cea 100644 --- a/tools/vr/src/scheduling.c +++ b/tools/vr/src/scheduling.c @@ -28,7 +28,6 @@ int setrlimit(__rlimit_resource_t resource, const struct rlimit *limit) { struct rlimit current; if (!getrlimit(resource, ¤t) && (limit->rlim_cur < current.rlim_cur || limit->rlim_max < current.rlim_max)) { - // SDL's failed RTKit fallback leaves a fatal RT budget on Android GPU submissions. errno = EPERM; return -1; } diff --git a/tools/vr/src/spatial_tracking.h b/tools/vr/src/spatial_tracking.h index 20842c887..1f9f65a7c 100644 --- a/tools/vr/src/spatial_tracking.h +++ b/tools/vr/src/spatial_tracking.h @@ -78,7 +78,6 @@ struct OpticalPose { } auto target = filtered; if (predict) { - // Inference already consumes most capture age; receipt-relative prediction advances between results. float seconds = std::clamp(float(now-receivedAt)/1e9f,0.f,.08f); float speed2 = 0; for (float v : velocity) speed2 += v*v; diff --git a/tools/vr/src/vk_layer_droiddeck_vr.c b/tools/vr/src/vk_layer_droiddeck_vr.c index 5b315f243..be74139e5 100644 --- a/tools/vr/src/vk_layer_droiddeck_vr.c +++ b/tools/vr/src/vk_layer_droiddeck_vr.c @@ -981,8 +981,6 @@ static int submit_drop_wait(VkSemaphore s, uint64_t value) { for (;;) { uint64_t local = 0; if (get(dev, s, &local) == VK_SUCCESS && local >= value) return 0; - /* Only the poller mirrors shared values. A second host signal here can - * race its signal and submit the same timeline value twice. */ if (now_us() >= until) break; usleep(100); } @@ -1073,7 +1071,7 @@ static void *completer(void *arg) { if (pending) { struct timespec until; clock_gettime(CLOCK_REALTIME, &until); - until.tv_nsec += 1000000; /* Poll pending GPU work at most every 1 ms. */ + until.tv_nsec += 1000000; if (until.tv_nsec >= 1000000000) { until.tv_sec++; until.tv_nsec -= 1000000000; } pthread_cond_timedwait(&g_trk_cond, &g_trk_lock, &until); } else { @@ -1345,8 +1343,6 @@ static VkResult VKAPI_CALL CreateImage(VkDevice d, const VkImageCreateInfo *ci, VkImageCreateInfo compatible = *ci; VkExternalMemoryImageCreateInfo shared; const VkBaseInStructure *first = ci->pNext; - // SteamVR's OpenXR stereo arrays omit a format list, while DXVK adds one on import. - // Leave single-layer OpenVR images alone: the application owns their export layout. if (dev_for(d)->quest_images && ci->arrayLayers > 1 && (ci->flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) && ci->tiling == VK_IMAGE_TILING_OPTIMAL && first && first->sType == VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO) { const VkBaseInStructure *second = first->pNext; diff --git a/tools/vr/src/wsi_capture.cpp b/tools/vr/src/wsi_capture.cpp index 11f4d4e2b..ee7a7266c 100644 --- a/tools/vr/src/wsi_capture.cpp +++ b/tools/vr/src/wsi_capture.cpp @@ -395,7 +395,6 @@ extern "C" VkResult droiddeck_wsi_present(VkDevice device, VkQueue queue, const VkResult result = it->second->Capture(queue, present->pImageIndices[0], *present, &completion); if (result) return result; if (!completion) return next(queue, present); - // Reacquiring this swapchain image also proves its previous present wait semaphore is reusable. VkPresentInfoKHR copy = *present; copy.waitSemaphoreCount = 1; copy.pWaitSemaphores = &completion; return next(queue, ©); } catch (...) { diff --git a/tools/vr/src/xr_frame_math.h b/tools/vr/src/xr_frame_math.h index a78de7987..473c02847 100644 --- a/tools/vr/src/xr_frame_math.h +++ b/tools/vr/src/xr_frame_math.h @@ -9,7 +9,6 @@ struct DroidDeckXrRenderView { inline void xr_frame_projection_raw(const float fov[4], float *left, float *right, float *top, float *bottom) { *left = std::tan(fov[0]); *right = std::tan(fov[1]); - // OpenVR's top/bottom parameters are the lower/upper Y bounds in its projection formula. *top = std::tan(fov[3]); *bottom = std::tan(fov[2]); } diff --git a/tools/vr/src/xr_gpu_channel.h b/tools/vr/src/xr_gpu_channel.h index 8735d3dc5..52fb5d9d3 100644 --- a/tools/vr/src/xr_gpu_channel.h +++ b/tools/vr/src/xr_gpu_channel.h @@ -31,7 +31,6 @@ inline XrGpuFrameState xr_gpu_frame_state(const XrGpuFrame &frame, uint32_t widt return now - frame.header.captured_ns > 200000000LL ? XrGpuFrameState::Stale : XrGpuFrameState::Ready; } -// SEQPACKET keeps descriptor transfer and its metadata in one indivisible message. inline bool xr_gpu_send(int socket, const void *data, size_t size, const int *fds = nullptr, unsigned count = 0) { if (count > XR_GPU_SLOTS) return false; iovec bytes{const_cast(data), size}; diff --git a/tools/vr/tests/test_fence_tracker.c b/tools/vr/tests/test_fence_tracker.c index 8eecc3936..b747304b9 100644 --- a/tools/vr/tests/test_fence_tracker.c +++ b/tools/vr/tests/test_fence_tracker.c @@ -66,8 +66,6 @@ static void saturated_pool_and_out_of_order_completion(void) { struct tracked *pool = NULL, *pending[130]; VkDevice dev = (VkDevice)(uintptr_t)1; uint32_t issued = 0, done = 0; - /* These waits can all depend on a signal from a later queue submission. - * A fixed 64-slot pool blocks that signal before it reaches the driver. */ for (unsigned i = 0; i < 130; i++) pending[i] = submit(&pool, dev, &issued); assert(issued == 130); assert(track_pool_reap(pool, issued, &done) && done == 0); @@ -79,7 +77,6 @@ static void saturated_pool_and_out_of_order_completion(void) { unsigned before = created; struct tracked *signal = submit(&pool, dev, &issued); assert(signal == pending[129] && created == before && issued == 131); - /* Freeing the latest fence must not mark the older work as drawn. */ for (unsigned i = 0; i < 129; i++) fake(pending[i]->fence)->result = VK_SUCCESS; assert(track_pool_reap(pool, issued, &done) && done == 130); fake(signal->fence)->result = VK_SUCCESS; diff --git a/tools/vr/tests/test_presenter_image.cpp b/tools/vr/tests/test_presenter_image.cpp index baf36bddf..2cf58ce2f 100644 --- a/tools/vr/tests/test_presenter_image.cpp +++ b/tools/vr/tests/test_presenter_image.cpp @@ -6,7 +6,6 @@ int main() { droiddeck::SharedEyeImage image(VK_FORMAT_R8G8B8A8_SRGB, 1728, 1080); const auto &info = image.info; - // SteamVR's shared-eye creation trace on the Fold7: format 43, flags 8, usage 0x97, list 43/37. assert(info.format == 43 && info.flags == 8 && info.usage == 0x97); assert(info.extent.width == 1728 && info.extent.height == 1080 && info.extent.depth == 1); assert(info.samples == VK_SAMPLE_COUNT_1_BIT && info.mipLevels == 1 && info.arrayLayers == 1); diff --git a/tools/vr/tests/test_semaphore_layer.c b/tools/vr/tests/test_semaphore_layer.c index 89f4ce8ae..45b387cfa 100644 --- a/tools/vr/tests/test_semaphore_layer.c +++ b/tools/vr/tests/test_semaphore_layer.c @@ -39,7 +39,6 @@ static void check_shared_images(VkDevice device) { image.flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; image.tiling = VK_IMAGE_TILING_OPTIMAL; VkImage output; - /* Other targets, nonmutable, linear and DMA-BUF images retain their caller's chain. */ expected_image_chain = &list; assert(CreateImage(device, &image, NULL, &output) == VK_ERROR_FORMAT_NOT_SUPPORTED); g_dev[0].quest_images = 1; @@ -58,7 +57,6 @@ static void check_shared_images(VkDevice device) { expected_image_chain = &external; assert(CreateImage(device, &image, NULL, &output) == VK_ERROR_FORMAT_NOT_SUPPORTED); assert(image.pNext == &list && list.pNext == &external && external.pNext == &tail); - /* The reverse chain order preserves its tail without modifying caller memory. */ external.pNext = &list; list.pNext = &tail; image.pNext = &external; expect_cloned_external = 1; expected_external_tail = &tail; assert(CreateImage(device, &image, NULL, &output) == VK_ERROR_FORMAT_NOT_SUPPORTED); diff --git a/tools/vr/tests/test_xr_shared.cpp b/tools/vr/tests/test_xr_shared.cpp index fae4efde6..2e60bc215 100644 --- a/tools/vr/tests/test_xr_shared.cpp +++ b/tools/vr/tests/test_xr_shared.cpp @@ -58,7 +58,6 @@ int main() { assert(!absent.SendShared(path, header, buffer)); assert(!absent.SharedLeaseLost()); XrFrameReceiver receiver; assert(receiver.Start(path)); XrFrameSender invalid; - // An ordinary file is not a DMA-BUF and must never be acknowledged as one. assert(!invalid.SendShared(path, Header(), buffer)); assert(invalid.SharedLeaseLost() && receiver.Rejected() == 1); DroidDeckXrPixels frame; assert(!receiver.Take(&frame)); diff --git a/tools/xbox/droiddeck-cloud-token.patch b/tools/xbox/droiddeck-cloud-token.patch index 606410a66..97493f4a0 100644 --- a/tools/xbox/droiddeck-cloud-token.patch +++ b/tools/xbox/droiddeck-cloud-token.patch @@ -37,11 +37,7 @@ SubCommand::Achievements { title_id, json } => commands::achievements::run(&tokens, title_id, json).await, --- /dev/null +++ b/crates/xodus-cli/src/commands/cloud_token.rs -@@ -0,0 +1,237 @@ -+//! Xbox Cloud Gaming streaming token for the stored account (DroidDeck). Same bearer -+//! RPS -> Xbox user -> XSTS exchange as the profile read, for the cloud gaming relying party, -+//! then the streaming service's user login (as xbox.com and Greenlight's xal-node do). -+//! Never provisions a device or opens login. +@@ -0,0 +1,226 @@ +use std::{process::ExitCode, time::Duration}; +use serde_json::{Value, json}; +use xodus::{models::{live::ExchangeUserTokenOutcome, secrets::Token, soap}, tokens::TokenManager}; @@ -49,7 +45,6 @@ +const GSSV_RELYING_PARTY: &str = "http://gssv.xboxlive.com/"; +const OFFERINGS: [&str; 2] = ["xgpuweb", "xgpuwebf2p"]; + -+/// The stored account's compact Microsoft account ticket for one service target and policy. +async fn ticket(tokens: &TokenManager, client: &reqwest::Client, target: &str, policy: soap::PolicyReference) -> Result<(String, String), &'static str> { + let (account, device, user) = super::session::prepare(tokens, client).await.map_err(|stage| { + super::session::diagnostic(stage); stage @@ -63,9 +58,6 @@ + ExchangeUserTokenOutcome::Issued(soap::BodyContent::RequestSecurityTokenResponse(response)) => *response, + _ => return Err("msa-response"), + }; -+ // Only accept compact forms here and reject incomplete token responses. The cloud -+ // transfer purpose answers with a login proof token, which is what /connect takes (Greenlight -+ // sends the MSAL response's "lpt"). + let kind = response.token_type.clone(); + if kind == "urn:passport:loginprooftoken" { + let shape = match (&response.requested_security_token.binary_security_token, &response.requested_security_token.encrypted_data) { @@ -97,8 +89,6 @@ + Ok((account, xsts.token)) +} + -+/// The Microsoft account token a cloud session's /connect step takes (xbox.com asks MSAL for -+/// "service::http://Passport.NET/purpose::PURPOSE_XBOX_CLOUD_CONSOLE_TRANSFER_TOKEN"). +async fn transfer_token(tokens: &TokenManager, client: &reqwest::Client) -> Result { + let policy = soap::PolicyReference { uri: "PURPOSE_XBOX_CLOUD_CONSOLE_TRANSFER_TOKEN".into(), val: String::default() }; + Ok(ticket(tokens, client, "http://Passport.NET/purpose", policy).await?.1) @@ -153,7 +143,6 @@ + let xsts = &xsts; + async move { request_offering(client, &endpoint, xsts, offering).await } + }).await?; -+ // Diagnostic lines are letters and dashes only (the app's allow-list). + eprintln!("NEXUS_SESSION_DIAGNOSTIC_V1 gssv-login-ok-{}", if offering == "xgpuweb" { "gamepass" } else { "free" }); + let transfer = transfer_token(tokens, &client).await; + eprintln!("NEXUS_SESSION_DIAGNOSTIC_V1 transfer-token-{}", if transfer.is_ok() { "ok" } else { "missing" }); diff --git a/tools/xbox/droiddeck-credential-diagnostics.patch b/tools/xbox/droiddeck-credential-diagnostics.patch index 67576e1f5..f1d441d3b 100644 --- a/tools/xbox/droiddeck-credential-diagnostics.patch +++ b/tools/xbox/droiddeck-credential-diagnostics.patch @@ -2,11 +2,10 @@ diff --git a/crates/xodus/src/tokens/backend/keychain.rs b/crates/xodus/src/toke index 19480da..dc8f136 100644 --- a/crates/xodus/src/tokens/backend/keychain.rs +++ b/crates/xodus/src/tokens/backend/keychain.rs -@@ -13,29 +13,24 @@ pub struct KeychainBackend; +@@ -13,29 +13,22 @@ pub struct KeychainBackend; // keyed loopback port the app also listens on is used for the rest of the process. static LOOPBACK: AtomicBool = AtomicBool::new(false); static REPORTED: AtomicBool = AtomicBool::new(false); -+// The app seals credentials with the Android Keystore, which took over 10 s on some phones. +const REPLY_TIMEOUT: Duration = Duration::from_secs(30); -/// Letters and dashes only: the app passes `NEXUS_CREDENTIAL_ERROR_V1 [a-z-]{1,48}` lines to its log. @@ -33,7 +32,6 @@ index 19480da..dc8f136 100644 +fn exchange(stream: S, prefix: Option<&str>, value: &Value) -> Result { + let route = if prefix.is_some() { "loopback" } else { "socket" }; + transfer(stream, prefix, value).inspect_err(|error| { -+ // Kind Other is the host's own refusal, already reported. + if let TokenStoreError::Io(error) = error + && error.kind() != std::io::ErrorKind::Other + { @@ -47,7 +45,7 @@ index 19480da..dc8f136 100644 if let Some(key) = prefix { stream.write_all(key.as_bytes())?; stream.write_all(b"\n")?; -@@ -45,6 +40,7 @@ fn exchange(mut stream: S, prefix: Option<&str>, value: &Value) +@@ -45,6 +38,7 @@ fn exchange(mut stream: S, prefix: Option<&str>, value: &Value) let mut bytes = Vec::new(); std::io::BufReader::new(stream).take(2_097_153).read_until(b'\n', &mut bytes)?; if bytes.len() > 2_097_152 || bytes.last() != Some(&b'\n') { @@ -55,7 +53,7 @@ index 19480da..dc8f136 100644 return Err(std::io::Error::other("Invalid Android credential response").into()); } let response: Value = serde_json::from_slice(&bytes)?; -@@ -56,28 +52,36 @@ fn exchange(mut stream: S, prefix: Option<&str>, value: &Value) +@@ -56,28 +50,36 @@ fn exchange(mut stream: S, prefix: Option<&str>, value: &Value) } fn loopback(value: &Value) -> Result { @@ -183,13 +181,11 @@ diff --git a/crates/xodus/src/tokens/store.rs b/crates/xodus/src/tokens/store.rs index 78b73d0..15cbff0 100644 --- a/crates/xodus/src/tokens/store.rs +++ b/crates/xodus/src/tokens/store.rs -@@ -50,3 +50,51 @@ impl TokenStoreError { +@@ -50,3 +50,49 @@ impl TokenStoreError { eprintln!("{self}"); } } + -+/// Letters and dashes only: DroidDeck passes `NEXUS_CREDENTIAL_ERROR_V1 [a-z-]{1,48}` lines to its -+/// log. Unlisted errno values are spelled with letters, a = 0 ... j = 9 (`os-dh` = errno 37). +pub(crate) fn io_reason(error: &std::io::Error) -> String { + let known = match error.raw_os_error() { + Some(111) => "refused", diff --git a/tools/xbox/droiddeck-credential-fallback.patch b/tools/xbox/droiddeck-credential-fallback.patch index 859afd4f6..4dec0d7e6 100644 --- a/tools/xbox/droiddeck-credential-fallback.patch +++ b/tools/xbox/droiddeck-credential-fallback.patch @@ -2,7 +2,7 @@ diff --git a/crates/xodus/src/tokens/backend/keychain.rs b/crates/xodus/src/toke index 1903638..19480da 100644 --- a/crates/xodus/src/tokens/backend/keychain.rs +++ b/crates/xodus/src/tokens/backend/keychain.rs -@@ -1,20 +1,46 @@ +@@ -1,20 +1,45 @@ use crate::tokens::store::{TokenBackend, TokenStoreError}; use serde_json::{Value, json}; use std::io::{BufRead, Read, Write}; @@ -22,7 +22,6 @@ index 1903638..19480da 100644 - stream.set_read_timeout(Some(Duration::from_secs(10)))?; - stream.set_write_timeout(Some(Duration::from_secs(10)))?; - serde_json::to_writer(&mut stream, &value)?; -+// Some Android devices refuse the abstract socket from inside PRoot; after one failure the +// keyed loopback port the app also listens on is used for the rest of the process. +static LOOPBACK: AtomicBool = AtomicBool::new(false); +static REPORTED: AtomicBool = AtomicBool::new(false); @@ -57,7 +56,7 @@ index 1903638..19480da 100644 stream.write_all(b"\n")?; let mut bytes = Vec::new(); std::io::BufReader::new(stream).take(2_097_153).read_until(b'\n', &mut bytes)?; -@@ -22,10 +48,52 @@ fn request(value: Value) -> Result { +@@ -22,10 +47,52 @@ fn request(value: Value) -> Result { return Err(std::io::Error::other("Invalid Android credential response").into()); } let response: Value = serde_json::from_slice(&bytes)?; diff --git a/tools/xbox/droiddeck-xbox-ticket.patch b/tools/xbox/droiddeck-xbox-ticket.patch index 78093d74d..bb8f5d08f 100644 --- a/tools/xbox/droiddeck-xbox-ticket.patch +++ b/tools/xbox/droiddeck-xbox-ticket.patch @@ -1,14 +1,12 @@ --- a/crates/xodus/src/models/secrets.rs +++ b/crates/xodus/src/models/secrets.rs -@@ -80,9 +80,17 @@ +@@ -80,9 +80,13 @@ } } -/// Store and licensing services reject delegation-compact tickets in the "d=" -/// form that Xbox Live sign-in needs (InvalidMsaTicketFormat) and accept the -/// same value as "t=". Convert only where a ticket is sent to those services. -+// Xbox accepts compact JWE tickets without the delegation prefix; opaque -+// delegation tickets keep their original wire representation. +pub fn xbox_ticket(ticket: String) -> String { + match ticket.strip_prefix("d=") { + Some(raw) if is_compact_jwe(raw) => raw.to_owned(), @@ -16,8 +14,6 @@ + } +} + -+/// Store services accept delegation-compact tickets in the "t=" form. -+/// Convert only where a ticket is sent to those services. pub fn store_ticket(ticket: String) -> String { match ticket.strip_prefix("d=") { Some(value) => format!("t={value}"), diff --git a/tools/xbox/prepare.py b/tools/xbox/prepare.py index 8d435d34b..ef2b23024 100755 --- a/tools/xbox/prepare.py +++ b/tools/xbox/prepare.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -"""Reproduce the pinned Nexus source and DroidDeck credential transport.""" import hashlib import json from pathlib import Path diff --git a/tools/xbox/runtime.py b/tools/xbox/runtime.py index cbe3589ff..38bb10239 100755 --- a/tools/xbox/runtime.py +++ b/tools/xbox/runtime.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -"""Extract only pinned Windows GDK modules; Linux Wine stays ARM64.""" import hashlib import json from pathlib import Path @@ -35,7 +34,6 @@ def download(name, expected, algorithm): return target archive = download(lock['archive'], lock['sha512'], 'sha512') -# Keep corresponding source available beside the build cache for redistribution. source = lock['source'] download(source['name'], source['sha256'], 'sha256') remaining = set(lock['dlls']) @@ -49,8 +47,6 @@ with tarfile.open(archive) as tar: data = tar.extractfile(member).read() if hashlib.sha256(data).hexdigest() != lock['dlls'][name] or data[:2] != b'MZ': raise RuntimeError('Xbox Windows runtime module checksum mismatch') - # Wine rejects builtin-tagged PE files when WINEDLLOVERRIDES selects native. - # These standalone Windows modules must use the native x64 loader on ARM64EC. if data[64:80] == b'Wine builtin DLL': data = data[:64] + b'DroidDeck native' + data[80:] (output / name).write_bytes(data) diff --git a/tools/xcloud-player/build.sh b/tools/xcloud-player/build.sh index 8839c5693..0246e96bb 100644 --- a/tools/xcloud-player/build.sh +++ b/tools/xcloud-player/build.sh @@ -1,5 +1,4 @@ #!/usr/bin/env bash -# Bundles the cloud player page script with esbuild 0.24.0 (ESBUILD=/path/to/esbuild, default: esbuild on PATH). set -euo pipefail here=$(cd "$(dirname "$0")" && pwd) repo=$(cd "$here/../.." && pwd) diff --git a/tools/xcloud-player/entry.ts b/tools/xcloud-player/entry.ts index 9237be3dd..d135074c6 100644 --- a/tools/xcloud-player/entry.ts +++ b/tools/xcloud-player/entry.ts @@ -1,5 +1,3 @@ -// DroidDeck cloud player page: runs one Xbox Cloud Gaming session. Every request to Microsoft goes -// through the app (DDCloud), which holds the tokens; this page only does WebRTC, video, sound and input. import xCloudPlayer from './greenlight/lib/player' import Gamepad from './greenlight/lib/input/gamepad' import { EffectRenderer, Effects, needsRenderer } from './renderer' @@ -9,21 +7,19 @@ import { capVideoBitrate, preferH264Profile } from './sdp-tweaks' interface Reply { status: number, body: string } declare const DDCloud: { call(id: number, op: string, arg: string): void, status(text: string): void, failed(stage: string, detail: string): void, playing(): void, log(text: string): void, settings(): string } -/** The player settings the app hands over (XboxCloudSettings.toJson); stream-start ones are applied by the app. */ interface Settings extends Effects { - bitrateMbps: number // 0 = Microsoft's own limit - codec: string // 'default' | 'high' | 'normal' | 'low' + bitrateMbps: number + codec: string preventDrops: boolean - statsLevel: number // 0 off, 1 fps, 2 basic, 3 detailed, 4 full - statsLayout: string // 'horizontal' | 'vertical' - statsCorner: string // 'right' | 'left' - server: string // the region's host, for the full overlay - volume: number // 100..300 % - rumble: number // 0..100 % + statsLevel: number + statsLayout: string + statsCorner: string + server: string + volume: number + rumble: number } let settings: Settings = JSON.parse(DDCloud.settings()) -// Codec profile and bitrate cap go into our offer (Better xCloud's approach). const nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription RTCPeerConnection.prototype.setLocalDescription = function (this: RTCPeerConnection, description?: RTCLocalSessionDescriptionInit) { if (description?.sdp) { @@ -34,7 +30,6 @@ RTCPeerConnection.prototype.setLocalDescription = function (this: RTCPeerConnect } as typeof RTCPeerConnection.prototype.setLocalDescription ;(window as any).__ddRumble = settings.rumble / 100 -// Page errors go to the app's Xbox log (no tokens ever reach this page). window.addEventListener('error', event => DDCloud.log(`error: ${event.message} @${event.lineno}`)) window.addEventListener('unhandledrejection', event => DDCloud.log(`rejection: ${String(event.reason).slice(0, 200)}`)) window.addEventListener('gamepadconnected', event => DDCloud.log(`controller: ${event.gamepad.id.slice(0, 60)} mapping=${event.gamepad.mapping || 'none'} buttons=${event.gamepad.buttons.length} axes=${event.gamepad.axes.length}`)) @@ -112,8 +107,6 @@ async function run() { player.init() const pc = player._peerConnection - // "Stop quality drops": report a short decode time so the server does not lower the resolution - // for a slow decoder (Better xCloud's patchStreamMetadata). const input = player._channels.input as any const queueMetadata = input.queueMetadataFrame.bind(input) input.queueMetadataFrame = (frame: any) => { @@ -125,7 +118,6 @@ async function run() { queueMetadata(frame) } - // Volume above 100 %: the stream's sound goes through a gain node instead of the